pax_global_header00006660000000000000000000000064150056437130014516gustar00rootroot0000000000000052 comment=3f8be847ddd19286bdfb573f46b8191e70d7c538 litestar-2.16.0/000077500000000000000000000000001500564371300134335ustar00rootroot00000000000000litestar-2.16.0/.all-contributorsrc000066400000000000000000001460761500564371300173020ustar00rootroot00000000000000{ "files": [ "README.md" ], "imageSize": 100, "commit": false, "contributors": [ { "login": "Goldziher", "name": "Na'aman Hirschfeld", "avatar_url": "https://avatars.githubusercontent.com/u/30733348?v=4", "profile": "https://www.linkedin.com/in/nhirschfeld/", "contributions": [ "maintenance", "code", "doc", "test", "ideas", "example", "bug" ] }, { "login": "peterschutt", "name": "Peter Schutt", "avatar_url": "https://avatars.githubusercontent.com/u/20659309?v=4", "profile": "https://github.com/peterschutt", "contributions": [ "maintenance", "code", "doc", "test", "ideas", "example", "bug" ] }, { "login": "ashwinvin", "name": "Ashwin Vinod", "avatar_url": "https://avatars.githubusercontent.com/u/38067089?v=4", "profile": "https://ashwinvin.github.io", "contributions": [ "code", "doc" ] }, { "login": "dkress59", "name": "Damian", "avatar_url": "https://avatars.githubusercontent.com/u/28515387?v=4", "profile": "http://www.damiankress.de", "contributions": [ "doc" ] }, { "login": "vincentsarago", "name": "Vincent Sarago", "avatar_url": "https://avatars.githubusercontent.com/u/10407788?v=4", "profile": "https://remotepixel.ca", "contributions": [ "code" ] }, { "login": "JonasKs", "name": "Jonas Krüger Svensson", "avatar_url": "https://avatars.githubusercontent.com/u/5310116?v=4", "profile": "https://hotfix.guru", "contributions": [ "platform" ] }, { "login": "sondrelg", "name": "Sondre Lillebø Gundersen", "avatar_url": "https://avatars.githubusercontent.com/u/25310870?v=4", "profile": "https://github.com/sondrelg", "contributions": [ "platform" ] }, { "login": "vrslev", "name": "Lev", "avatar_url": "https://avatars.githubusercontent.com/u/75225148?v=4", "profile": "https://github.com/vrslev", "contributions": [ "code", "ideas" ] }, { "login": "timwedde", "name": "Tim Wedde", "avatar_url": "https://avatars.githubusercontent.com/u/20231751?v=4", "profile": "https://github.com/timwedde", "contributions": [ "code" ] }, { "login": "tclasen", "name": "Tory Clasen", "avatar_url": "https://avatars.githubusercontent.com/u/11999013?v=4", "profile": "https://github.com/tclasen", "contributions": [ "code" ] }, { "login": "Bobronium", "name": "Arseny Boykov", "avatar_url": "https://avatars.githubusercontent.com/u/36469655?v=4", "profile": "http://t.me/Bobronium", "contributions": [ "code", "ideas" ] }, { "login": "yudjinn", "name": "Jacob Rodgers", "avatar_url": "https://avatars.githubusercontent.com/u/7493084?v=4", "profile": "https://github.com/yudjinn", "contributions": [ "example" ] }, { "login": "danesolberg", "name": "Dane Solberg", "avatar_url": "https://avatars.githubusercontent.com/u/25882507?v=4", "profile": "https://github.com/danesolberg", "contributions": [ "code" ] }, { "login": "madlad33", "name": "madlad33", "avatar_url": "https://avatars.githubusercontent.com/u/54079440?v=4", "profile": "https://github.com/madlad33", "contributions": [ "code" ] }, { "login": "Butch78", "name": "Matthew Aylward ", "avatar_url": "https://avatars.githubusercontent.com/u/19205392?v=4", "profile": "http://matthewtyleraylward.com", "contributions": [ "code" ] }, { "login": "Joko013", "name": "Jan Klima", "avatar_url": "https://avatars.githubusercontent.com/u/30841710?v=4", "profile": "https://github.com/Joko013", "contributions": [ "code" ] }, { "login": "i404788", "name": "C2D", "avatar_url": "https://avatars.githubusercontent.com/u/50617709?v=4", "profile": "https://github.com/i404788", "contributions": [ "test" ] }, { "login": "to-ph", "name": "to-ph", "avatar_url": "https://avatars.githubusercontent.com/u/84818322?v=4", "profile": "https://github.com/to-ph", "contributions": [ "code" ] }, { "login": "imbev", "name": "imbev", "avatar_url": "https://avatars.githubusercontent.com/u/105524473?v=4", "profile": "https://imbev.gitlab.io/site", "contributions": [ "doc" ] }, { "login": "185504a9", "name": "cătălin", "avatar_url": "https://avatars.githubusercontent.com/u/45485069?v=4", "profile": "https://git.roboces.dev/catalin", "contributions": [ "code" ] }, { "login": "Seon82", "name": "Seon82", "avatar_url": "https://avatars.githubusercontent.com/u/46298009?v=4", "profile": "https://github.com/Seon82", "contributions": [ "doc" ] }, { "login": "slavugan", "name": "Slava", "avatar_url": "https://avatars.githubusercontent.com/u/8457612?v=4", "profile": "https://github.com/slavugan", "contributions": [ "code" ] }, { "login": "Harry-Lees", "name": "Harry", "avatar_url": "https://avatars.githubusercontent.com/u/52263746?v=4", "profile": "https://github.com/Harry-Lees", "contributions": [ "code", "doc" ] }, { "login": "cofin", "name": "Cody Fincher", "avatar_url": "https://avatars.githubusercontent.com/u/204685?v=4", "profile": "https://github.com/cofin", "contributions": [ "maintenance", "code", "doc", "test", "ideas", "example", "bug" ] }, { "login": "cclauss", "name": "Christian Clauss", "avatar_url": "https://avatars.githubusercontent.com/u/3709715?v=4", "profile": "https://www.patreon.com/cclauss", "contributions": [ "doc" ] }, { "login": "josepdaniel", "name": "josepdaniel", "avatar_url": "https://avatars.githubusercontent.com/u/36941460?v=4", "profile": "https://github.com/josepdaniel", "contributions": [ "code" ] }, { "login": "devtud", "name": "devtud", "avatar_url": "https://avatars.githubusercontent.com/u/6808024?v=4", "profile": "https://github.com/devtud", "contributions": [ "bug" ] }, { "login": "nramos0", "name": "Nicholas Ramos", "avatar_url": "https://avatars.githubusercontent.com/u/35410160?v=4", "profile": "https://github.com/nramos0", "contributions": [ "code" ] }, { "login": "seladb", "name": "seladb", "avatar_url": "https://avatars.githubusercontent.com/u/9059541?v=4", "profile": "https://twitter.com/seladb", "contributions": [ "doc", "code" ] }, { "login": "aedify-swi", "name": "Simon Wienhöfer", "avatar_url": "https://avatars.githubusercontent.com/u/66629131?v=4", "profile": "https://github.com/aedify-swi", "contributions": [ "code" ] }, { "login": "mobiusxs", "name": "MobiusXS", "avatar_url": "https://avatars.githubusercontent.com/u/57055149?v=4", "profile": "https://github.com/mobiusxs", "contributions": [ "code" ] }, { "login": "Aidan-Simard", "name": "Aidan Simard", "avatar_url": "https://avatars.githubusercontent.com/u/73361895?v=4", "profile": "http://aidansimard.dev", "contributions": [ "doc" ] }, { "login": "waweber", "name": "wweber", "avatar_url": "https://avatars.githubusercontent.com/u/714224?v=4", "profile": "https://github.com/waweber", "contributions": [ "code" ] }, { "login": "samuelcolvin", "name": "Samuel Colvin", "avatar_url": "https://avatars.githubusercontent.com/u/4039449?v=4", "profile": "http://scolvin.com", "contributions": [ "code" ] }, { "login": "toudi", "name": "Mateusz Mikołajczyk", "avatar_url": "https://avatars.githubusercontent.com/u/81148?v=4", "profile": "https://github.com/toudi", "contributions": [ "code" ] }, { "login": "Alex-CodeLab", "name": "Alex ", "avatar_url": "https://avatars.githubusercontent.com/u/1678423?v=4", "profile": "https://github.com/Alex-CodeLab", "contributions": [ "code" ] }, { "login": "odiseo0", "name": "Odiseo", "avatar_url": "https://avatars.githubusercontent.com/u/87550035?v=4", "profile": "https://github.com/odiseo0", "contributions": [ "doc" ] }, { "login": "ingjavierpinilla", "name": "Javier Pinilla", "avatar_url": "https://avatars.githubusercontent.com/u/36714646?v=4", "profile": "https://github.com/ingjavierpinilla", "contributions": [ "code" ] }, { "login": "Chaoyingz", "name": "Chaoying", "avatar_url": "https://avatars.githubusercontent.com/u/32626585?v=4", "profile": "https://github.com/Chaoyingz", "contributions": [ "doc" ] }, { "login": "infohash", "name": "infohash", "avatar_url": "https://avatars.githubusercontent.com/u/46137868?v=4", "profile": "https://github.com/infohash", "contributions": [ "code" ] }, { "login": "john-ingles", "name": "John Ingles", "avatar_url": "https://avatars.githubusercontent.com/u/35442886?v=4", "profile": "https://www.linkedin.com/in/john-ingles/", "contributions": [ "code" ] }, { "login": "h0rn3t", "name": "Eugene", "avatar_url": "https://avatars.githubusercontent.com/u/1213719?v=4", "profile": "https://github.com/h0rn3t", "contributions": [ "test", "code" ] }, { "login": "jonadaly", "name": "Jon Daly", "avatar_url": "https://avatars.githubusercontent.com/u/26462826?v=4", "profile": "https://github.com/jonadaly", "contributions": [ "doc", "code" ] }, { "login": "Harshal6927", "name": "Harshal Laheri", "avatar_url": "https://avatars.githubusercontent.com/u/73422191?v=4", "profile": "https://harshallaheri.me/", "contributions": [ "code", "doc" ] }, { "login": "sorasful", "name": "Téva KRIEF", "avatar_url": "https://avatars.githubusercontent.com/u/32820423?v=4", "profile": "https://github.com/sorasful", "contributions": [ "code" ] }, { "login": "jtraub", "name": "Konstantin Mikhailov", "avatar_url": "https://avatars.githubusercontent.com/u/153191?v=4", "profile": "https://github.com/jtraub", "contributions": [ "maintenance", "code", "doc", "test", "ideas", "example", "bug" ] }, { "login": "devmitch", "name": "Mitchell Henry", "avatar_url": "https://avatars.githubusercontent.com/u/17354727?v=4", "profile": "http://linkedin.com/in/mitchell-henry334/", "contributions": [ "doc" ] }, { "login": "chbndrhnns", "name": "chbndrhnns", "avatar_url": "https://avatars.githubusercontent.com/u/7534547?v=4", "profile": "https://github.com/chbndrhnns", "contributions": [ "doc" ] }, { "login": "nielsvanhooy", "name": "nielsvanhooy", "avatar_url": "https://avatars.githubusercontent.com/u/40770348?v=4", "profile": "https://github.com/nielsvanhooy", "contributions": [ "code", "bug", "test" ] }, { "login": "provinzkraut", "name": "provinzkraut", "avatar_url": "https://avatars.githubusercontent.com/u/25355197?v=4", "profile": "https://github.com/provinzkraut", "contributions": [ "maintenance", "code", "doc", "test", "ideas", "example", "bug", "design" ] }, { "login": "jab", "name": "Joshua Bronson", "avatar_url": "https://avatars.githubusercontent.com/u/64992?v=4", "profile": "https://github.com/jab", "contributions": [ "doc" ] }, { "login": "ReznikovRoman", "name": "Roman Reznikov", "avatar_url": "https://avatars.githubusercontent.com/u/44291988?v=4", "profile": "http://linkedin.com/in/roman-reznikov", "contributions": [ "doc" ] }, { "login": "mookrs", "name": "mookrs", "avatar_url": "https://avatars.githubusercontent.com/u/985439?v=4", "profile": "http://mookrs.com", "contributions": [ "doc" ] }, { "login": "mivade", "name": "Mike DePalatis", "avatar_url": "https://avatars.githubusercontent.com/u/2805515?v=4", "profile": "http://mike.depalatis.net", "contributions": [ "doc" ] }, { "login": "pemocarlo", "name": "Carlos Alberto Pérez-Molano", "avatar_url": "https://avatars.githubusercontent.com/u/7297323?v=4", "profile": "https://github.com/pemocarlo", "contributions": [ "doc" ] }, { "login": "ThinksFast", "name": "ThinksFast", "avatar_url": "https://avatars.githubusercontent.com/u/114229148?v=4", "profile": "https://www.bestcryptocodes.com", "contributions": [ "test", "doc" ] }, { "login": "ottermata", "name": "Christopher Krause", "avatar_url": "https://avatars.githubusercontent.com/u/9451844?v=4", "profile": "https://github.com/ottermata", "contributions": [ "code" ] }, { "login": "smithk86", "name": "Kyle Smith", "avatar_url": "https://avatars.githubusercontent.com/u/1161424?v=4", "profile": "http://www.kylesmith.me", "contributions": [ "code", "doc", "bug" ] }, { "login": "scott2b", "name": "Scott Bradley", "avatar_url": "https://avatars.githubusercontent.com/u/307713?v=4", "profile": "https://github.com/scott2b", "contributions": [ "bug" ] }, { "login": "srikanthccv", "name": "Srikanth Chekuri", "avatar_url": "https://avatars.githubusercontent.com/u/22846633?v=4", "profile": "https://www.linkedin.com/in/srikanthccv/", "contributions": [ "test", "doc" ] }, { "login": "LonelyVikingMichael", "name": "Michael Bosch", "avatar_url": "https://avatars.githubusercontent.com/u/78952809?v=4", "profile": "https://lonelyviking.com", "contributions": [ "doc" ] }, { "login": "sssssss340", "name": "sssssss340", "avatar_url": "https://avatars.githubusercontent.com/u/8406195?v=4", "profile": "https://github.com/sssssss340", "contributions": [ "bug" ] }, { "login": "ste-pool", "name": "ste-pool", "avatar_url": "https://avatars.githubusercontent.com/u/17198460?v=4", "profile": "https://github.com/ste-pool", "contributions": [ "code", "infra" ] }, { "login": "Alc-Alc", "name": "Alc-Alc", "avatar_url": "https://avatars.githubusercontent.com/u/45509143?v=4", "profile": "https://github.com/Alc-Alc", "contributions": [ "doc", "code", "test", "infra" ] }, { "login": "asomethings", "name": "asomethings", "avatar_url": "https://avatars.githubusercontent.com/u/16171942?v=4", "profile": "http://asomethings.com", "contributions": [ "code" ] }, { "login": "garburator", "name": "Garry Bullock", "avatar_url": "https://avatars.githubusercontent.com/u/14207857?v=4", "profile": "https://github.com/garburator", "contributions": [ "doc" ] }, { "login": "NiclasHaderer", "name": "Niclas Haderer", "avatar_url": "https://avatars.githubusercontent.com/u/109728711?v=4", "profile": "https://github.com/NiclasHaderer", "contributions": [ "code" ] }, { "login": "dialvarezs", "name": "Diego Alvarez", "avatar_url": "https://avatars.githubusercontent.com/u/13831919?v=4", "profile": "https://github.com/dialvarezs", "contributions": [ "doc", "code", "test" ] }, { "login": "rgajason", "name": "Jason Nance", "avatar_url": "https://avatars.githubusercontent.com/u/51208317?v=4", "profile": "https://www.rgare.com", "contributions": [ "doc" ] }, { "login": "spikenn", "name": "Igor Kapadze", "avatar_url": "https://avatars.githubusercontent.com/u/32995595?v=4", "profile": "https://github.com/spikenn", "contributions": [ "doc" ] }, { "login": "Jarmos-san", "name": "Somraj Saha", "avatar_url": "https://avatars.githubusercontent.com/u/31373860?v=4", "profile": "https://jarmos.vercel.app", "contributions": [ "doc" ] }, { "login": "maggias", "name": "Magnús Ágúst Skúlason", "avatar_url": "https://avatars.githubusercontent.com/u/11139514?v=4", "profile": "http://skulason.me", "contributions": [ "code", "doc" ] }, { "login": "pomma89", "name": "Alessio Parma", "avatar_url": "https://avatars.githubusercontent.com/u/4697032?v=4", "profile": "https://alessioparma.xyz/", "contributions": [ "doc" ] }, { "login": "Lugoues", "name": "Peter Brunner", "avatar_url": "https://avatars.githubusercontent.com/u/372610?v=4", "profile": "https://github.com/Lugoues", "contributions": [ "code" ] }, { "login": "JacobCoffee", "name": "Jacob Coffee", "avatar_url": "https://avatars.githubusercontent.com/u/45884264?v=4", "profile": "https://scriptr.dev/", "contributions": [ "doc", "code", "test", "infra", "ideas", "maintenance", "business", "design" ] }, { "login": "Gamazic", "name": "Gamazic", "avatar_url": "https://avatars.githubusercontent.com/u/33692402?v=4", "profile": "https://github.com/Gamazic", "contributions": [ "code" ] }, { "login": "kareemmahlees", "name": "Kareem Mahlees", "avatar_url": "https://avatars.githubusercontent.com/u/89863279?v=4", "profile": "https://github.com/kareemmahlees", "contributions": [ "code" ] }, { "login": "abdulhaq-e", "name": "Abdulhaq Emhemmed", "avatar_url": "https://avatars.githubusercontent.com/u/2532125?v=4", "profile": "https://github.com/abdulhaq-e", "contributions": [ "code", "doc" ] }, { "login": "jenish2014", "name": "Jenish", "avatar_url": "https://avatars.githubusercontent.com/u/9599888?v=4", "profile": "https://github.com/jenish2014", "contributions": [ "code", "doc" ] }, { "login": "chris-telemetry", "name": "chris-telemetry", "avatar_url": "https://avatars.githubusercontent.com/u/78052999?v=4", "profile": "https://github.com/chris-telemetry", "contributions": [ "code" ] }, { "login": "WardPearce", "name": "Ward", "avatar_url": "https://avatars.githubusercontent.com/u/27844174?v=4", "profile": "http://wardpearce.com", "contributions": [ "bug" ] }, { "login": "knowsuchagency", "name": "Stephan Fitzpatrick", "avatar_url": "https://avatars.githubusercontent.com/u/11974795?v=4", "profile": "https://knowsuchagency.com", "contributions": [ "bug" ] }, { "login": "ekeric13", "name": "Eric Kennedy", "avatar_url": "https://avatars.githubusercontent.com/u/6489651?v=4", "profile": "https://codepen.io/ekeric13/", "contributions": [ "doc" ] }, { "login": "wassafshahzad", "name": "wassaf shahzad", "avatar_url": "https://avatars.githubusercontent.com/u/25094157?v=4", "profile": "https://github.com/wassafshahzad", "contributions": [ "code" ] }, { "login": "nilsso", "name": "Nils Olsson", "avatar_url": "https://avatars.githubusercontent.com/u/567181?v=4", "profile": "http://nilsso.github.io", "contributions": [ "code", "bug" ] }, { "login": "Nadock", "name": "Riley Chase", "avatar_url": "https://avatars.githubusercontent.com/u/1491530?v=4", "profile": "http://rileychase.net", "contributions": [ "code" ] }, { "login": "onerandomusername", "name": "arl", "avatar_url": "https://avatars.githubusercontent.com/u/71233171?v=4", "profile": "https://gh.arielle.codes", "contributions": [ "maintenance" ] }, { "login": "Galdanwing", "name": "Antoine van der Horst", "avatar_url": "https://avatars.githubusercontent.com/u/29492757?v=4", "profile": "https://github.com/Galdanwing", "contributions": [ "doc" ] }, { "login": "zoni", "name": "Nick Groenen", "avatar_url": "https://avatars.githubusercontent.com/u/145285?v=4", "profile": "https://nick.groenen.me", "contributions": [ "doc" ] }, { "login": "giorgiovilardo", "name": "Giorgio Vilardo", "avatar_url": "https://avatars.githubusercontent.com/u/56472600?v=4", "profile": "https://github.com/giorgiovilardo", "contributions": [ "doc" ] }, { "login": "bollwyvl", "name": "Nicholas Bollweg", "avatar_url": "https://avatars.githubusercontent.com/u/45380?v=4", "profile": "https://github.com/bollwyvl", "contributions": [ "code" ] }, { "login": "tompin82", "name": "Tomas Jonsson", "avatar_url": "https://avatars.githubusercontent.com/u/47041409?v=4", "profile": "https://github.com/tompin82", "contributions": [ "test", "code" ] }, { "login": "khiemdoan", "name": "Khiem Doan", "avatar_url": "https://avatars.githubusercontent.com/u/15646249?v=4", "profile": "https://www.linkedin.com/in/khiem-doan/", "contributions": [ "doc" ] }, { "login": "kedod", "name": "kedod", "avatar_url": "https://avatars.githubusercontent.com/u/35638715?v=4", "profile": "https://github.com/kedod", "contributions": [ "doc", "code", "test" ] }, { "login": "sonpro1296", "name": "sonpro1296", "avatar_url": "https://avatars.githubusercontent.com/u/17319142?v=4", "profile": "https://github.com/sonpro1296", "contributions": [ "code", "test", "infra", "doc" ] }, { "login": "patrickarmengol", "name": "Patrick Armengol", "avatar_url": "https://avatars.githubusercontent.com/u/42473149?v=4", "profile": "https://patrickarmengol.com", "contributions": [ "doc" ] }, { "login": "SanderWegter", "name": "Sander", "avatar_url": "https://avatars.githubusercontent.com/u/7465799?v=4", "profile": "https://sanderwegter.nl", "contributions": [ "doc" ] }, { "login": "erhuabushuo", "name": "疯人院主任", "avatar_url": "https://avatars.githubusercontent.com/u/1642364?v=4", "profile": "https://github.com/erhuabushuo", "contributions": [ "doc" ] }, { "login": "aviral-nayya", "name": "aviral-nayya", "avatar_url": "https://avatars.githubusercontent.com/u/121891493?v=4", "profile": "https://github.com/aviral-nayya", "contributions": [ "code" ] }, { "login": "whiskeyriver", "name": "whiskeyriver", "avatar_url": "https://avatars.githubusercontent.com/u/162092?v=4", "profile": "https://github.com/whiskeyriver", "contributions": [ "code" ] }, { "login": "v3ss0n", "name": "Phyo Arkar Lwin", "avatar_url": "https://avatars.githubusercontent.com/u/419606?v=4", "profile": "https://hexcode.tech", "contributions": [ "code" ] }, { "login": "MatthewNewland", "name": "MatthewNewland", "avatar_url": "https://avatars.githubusercontent.com/u/9618670?v=4", "profile": "https://github.com/MatthewNewland", "contributions": [ "bug", "code", "test" ] }, { "login": "vtarchon", "name": "Tom Kuo", "avatar_url": "https://avatars.githubusercontent.com/u/1598170?v=4", "profile": "https://github.com/vtarchon", "contributions": [ "bug" ] }, { "login": "LeckerenSirupwaffeln", "name": "LeckerenSirupwaffeln", "avatar_url": "https://avatars.githubusercontent.com/u/83568015?v=4", "profile": "https://github.com/LeckerenSirupwaffeln", "contributions": [ "bug" ] }, { "login": "eldano1995", "name": "Daniel González Fernández", "avatar_url": "https://avatars.githubusercontent.com/u/24553679?v=4", "profile": "https://github.com/eldano1995", "contributions": [ "doc" ] }, { "login": "01EK98", "name": "01EK98", "avatar_url": "https://avatars.githubusercontent.com/u/101988390?v=4", "profile": "https://github.com/01EK98", "contributions": [ "doc" ] }, { "login": "sarbor", "name": "Sarbo Roy", "avatar_url": "https://avatars.githubusercontent.com/u/15257226?v=4", "profile": "https://github.com/sarbor", "contributions": [ "code" ] }, { "login": "rseeley", "name": "Ryan Seeley", "avatar_url": "https://avatars.githubusercontent.com/u/5397221?v=4", "profile": "https://github.com/rseeley", "contributions": [ "code" ] }, { "login": "ctrl-Felix", "name": "Felix", "avatar_url": "https://avatars.githubusercontent.com/u/62290842?v=4", "profile": "https://github.com/ctrl-Felix", "contributions": [ "doc", "bug" ] }, { "login": "gsakkis", "name": "George Sakkis", "avatar_url": "https://avatars.githubusercontent.com/u/291289?v=4", "profile": "https://www.linkedin.com/in/gsakkis", "contributions": [ "code" ] }, { "login": "floxay", "name": "Huba Tuba", "avatar_url": "https://avatars.githubusercontent.com/u/57007485?v=4", "profile": "https://github.com/floxay", "contributions": [ "doc", "code", "test" ] }, { "login": "sfermigier", "name": "Stefane Fermigier", "avatar_url": "https://avatars.githubusercontent.com/u/271079?v=4", "profile": "http://fermigier.com/", "contributions": [ "doc" ] }, { "login": "r4gesingh47", "name": "r4ge", "avatar_url": "https://avatars.githubusercontent.com/u/71139938?v=4", "profile": "https://github.com/r4gesingh47", "contributions": [ "code", "doc" ] }, { "login": "jaykv", "name": "Jay", "avatar_url": "https://avatars.githubusercontent.com/u/18240054?v=4", "profile": "https://github.com/jaykv", "contributions": [ "code" ] }, { "login": "sinisaos", "name": "sinisaos", "avatar_url": "https://avatars.githubusercontent.com/u/30960668?v=4", "profile": "https://github.com/sinisaos", "contributions": [ "doc" ] }, { "login": "Tsdevendra1", "name": "Tharuka Devendra", "avatar_url": "https://avatars.githubusercontent.com/u/38055748?v=4", "profile": "https://github.com/Tsdevendra1", "contributions": [ "code" ] }, { "login": "euri10", "name": "euri10", "avatar_url": "https://avatars.githubusercontent.com/u/1104190?v=4", "profile": "https://github.com/euri10", "contributions": [ "code", "doc", "bug" ] }, { "login": "su-shubham", "name": "Shubham", "avatar_url": "https://avatars.githubusercontent.com/u/75021117?v=4", "profile": "https://github.com/su-shubham", "contributions": [ "doc" ] }, { "login": "erik-hasse", "name": "Erik Hasse", "avatar_url": "https://avatars.githubusercontent.com/u/37126755?v=4", "profile": "https://www.linkedin.com/in/erik-hasse", "contributions": [ "bug", "code" ] }, { "login": "sobolevn", "name": "Nikita Sobolev", "avatar_url": "https://avatars.githubusercontent.com/u/4660275?v=4", "profile": "https://sobolevn.me", "contributions": [ "infra", "code" ] }, { "login": "lazyc97", "name": "Nguyễn Hoàng Đức", "avatar_url": "https://avatars.githubusercontent.com/u/8538104?v=4", "profile": "https://github.com/lazyc97", "contributions": [ "bug" ] }, { "login": "RavanaBhrama", "name": "RavanaBhrama", "avatar_url": "https://avatars.githubusercontent.com/u/131459969?v=4", "profile": "https://github.com/RavanaBhrama", "contributions": [ "doc" ] }, { "login": "mj0nez", "name": "Marcel Johannesmann", "avatar_url": "https://avatars.githubusercontent.com/u/20128340?v=4", "profile": "https://github.com/mj0nez", "contributions": [ "doc" ] }, { "login": "therealzanfar", "name": "Matthew", "avatar_url": "https://avatars.githubusercontent.com/u/10294685?v=4", "profile": "http://zanfar.com/", "contributions": [ "doc" ] }, { "login": "Mattwmaster58", "name": "Mattwmaster58", "avatar_url": "https://avatars.githubusercontent.com/u/26337069?v=4", "profile": "https://github.com/Mattwmaster58", "contributions": [ "bug", "code", "test" ] }, { "login": "aorith", "name": "Manuel Sanchez Pinar", "avatar_url": "https://avatars.githubusercontent.com/u/5411704?v=4", "profile": "https://es.linkedin.com/in/manusp", "contributions": [ "doc" ] }, { "login": "juan-riveros", "name": "Juan Riveros", "avatar_url": "https://avatars.githubusercontent.com/u/1297567?v=4", "profile": "https://github.com/juan-riveros", "contributions": [ "doc" ] }, { "login": "davidbrochart", "name": "David Brochart", "avatar_url": "https://avatars.githubusercontent.com/u/4711805?v=4", "profile": "https://github.com/davidbrochart", "contributions": [ "doc" ] }, { "login": "sean-donoghue", "name": "Sean Donoghue", "avatar_url": "https://avatars.githubusercontent.com/u/64597271?v=4", "profile": "https://github.com/sean-donoghue", "contributions": [ "doc" ] }, { "login": "sykloid", "name": "P.C. Shyamshankar", "avatar_url": "https://avatars.githubusercontent.com/u/22753?v=4", "profile": "https://sykloid.org/", "contributions": [ "bug", "code", "test" ] }, { "login": "wevonosky", "name": "William Evonosky", "avatar_url": "https://avatars.githubusercontent.com/u/19598171?v=4", "profile": "https://github.com/wevonosky", "contributions": [ "code" ] }, { "login": "geeshta", "name": "geeshta", "avatar_url": "https://avatars.githubusercontent.com/u/61031243?v=4", "profile": "https://github.com/geeshta", "contributions": [ "doc", "code", "bug" ] }, { "login": "RobertRosca", "name": "Robert Rosca", "avatar_url": "https://avatars.githubusercontent.com/u/32569096?v=4", "profile": "https://fosstodon.org/@robertrosca", "contributions": [ "doc" ] }, { "login": "syshenyu", "name": "DICE_Lab", "avatar_url": "https://avatars.githubusercontent.com/u/92897003?v=4", "profile": "https://github.com/syshenyu", "contributions": [ "code" ] }, { "login": "lsanpablo", "name": "Luis San Pablo", "avatar_url": "https://avatars.githubusercontent.com/u/7145688?v=4", "profile": "https://github.com/lsanpablo", "contributions": [ "code", "test", "doc" ] }, { "login": "Lancetnik", "name": "Pastukhov Nikita", "avatar_url": "https://avatars.githubusercontent.com/u/44573917?v=4", "profile": "https://github.com/Lancetnik", "contributions": [ "doc" ] }, { "login": "ddxv", "name": "James O'Claire", "avatar_url": "https://avatars.githubusercontent.com/u/7601451?v=4", "profile": "http://jamesoclaire.com", "contributions": [ "doc" ] }, { "login": "pbaletkeman", "name": "Pete", "avatar_url": "https://avatars.githubusercontent.com/u/22402240?v=4", "profile": "https://github.com/pbaletkeman", "contributions": [ "doc" ] }, { "login": "heralight", "name": "Alexandre Richonnier", "avatar_url": "https://avatars.githubusercontent.com/u/534840?v=4", "profile": "http://www.hera.cc", "contributions": [ "code", "doc" ] }, { "login": "betaboon", "name": "betaboon", "avatar_url": "https://avatars.githubusercontent.com/u/7346933?v=4", "profile": "https://github.com/betaboon", "contributions": [ "code" ] }, { "login": "brakhane", "name": "Dennis Brakhane", "avatar_url": "https://avatars.githubusercontent.com/u/541637?v=4", "profile": "https://github.com/brakhane", "contributions": [ "code", "bug" ] }, { "login": "AgarwalPragy", "name": "Pragy Agarwal", "avatar_url": "https://avatars.githubusercontent.com/u/7423639?v=4", "profile": "https://mind.wiki", "contributions": [ "doc" ] }, { "login": "dybi", "name": "Piotr Dybowski", "avatar_url": "https://avatars.githubusercontent.com/u/36961162?v=4", "profile": "https://github.com/dybi", "contributions": [ "doc" ] }, { "login": "myslak71", "name": "Konrad Szczurek", "avatar_url": "https://avatars.githubusercontent.com/u/43068450?v=4", "profile": "https://github.com/myslak71", "contributions": [ "doc", "test" ] }, { "login": "orgarten", "name": "Orell Garten", "avatar_url": "https://avatars.githubusercontent.com/u/10799869?v=4", "profile": "https://github.com/orgarten", "contributions": [ "code", "doc", "test" ] }, { "login": "Kumzy", "name": "Julien", "avatar_url": "https://avatars.githubusercontent.com/u/5995441?v=4", "profile": "https://github.com/Kumzy", "contributions": [ "doc" ] }, { "login": "leejayhsu", "name": "Leejay Hsu", "avatar_url": "https://avatars.githubusercontent.com/u/37034741?v=4", "profile": "https://github.com/leejayhsu", "contributions": [ "maintenance", "infra", "doc" ] }, { "login": "mbeijen", "name": "Michiel W. Beijen", "avatar_url": "https://avatars.githubusercontent.com/u/659504?v=4", "profile": "https://x14.nl", "contributions": [ "doc" ] }, { "login": "baoliay2008", "name": "L. Bao", "avatar_url": "https://avatars.githubusercontent.com/u/13620348?v=4", "profile": "https://github.com/baoliay2008", "contributions": [ "doc" ] }, { "login": "jdglaser", "name": "Jarred Glaser", "avatar_url": "https://avatars.githubusercontent.com/u/32422167?v=4", "profile": "http://jarredglaser.com", "contributions": [ "doc" ] }, { "login": "hunterjsb", "name": "Hunter Boyd", "avatar_url": "https://avatars.githubusercontent.com/u/69213737?v=4", "profile": "https://github.com/hunterjsb", "contributions": [ "doc" ] }, { "login": "cesarmg1980", "name": "Cesar Giulietti", "avatar_url": "https://avatars.githubusercontent.com/u/38872121?v=4", "profile": "https://github.com/cesarmg1980", "contributions": [ "doc" ] }, { "login": "marcuslimdw", "name": "Marcus Lim", "avatar_url": "https://avatars.githubusercontent.com/u/42759889?v=4", "profile": "https://gitlab.com/marcuslimdw/", "contributions": [ "doc" ] }, { "login": "hzhou0", "name": "Henry Zhou", "avatar_url": "https://avatars.githubusercontent.com/u/43188301?v=4", "profile": "https://github.com/hzhou0", "contributions": [ "bug", "code" ] }, { "login": "WilliamStam", "name": "William Stam", "avatar_url": "https://avatars.githubusercontent.com/u/182800?v=4", "profile": "https://github.com/WilliamStam", "contributions": [ "doc" ] }, { "login": "andrewdoh", "name": "andrew do", "avatar_url": "https://avatars.githubusercontent.com/u/7662358?v=4", "profile": "https://github.com/andrewdoh", "contributions": [ "code", "test", "doc" ] }, { "login": "cbscsm", "name": "Boseong Choi", "avatar_url": "https://avatars.githubusercontent.com/u/31615733?v=4", "profile": "https://github.com/cbscsm", "contributions": [ "code", "test" ] }, { "login": "wer153", "name": "Kim Minki", "avatar_url": "https://avatars.githubusercontent.com/u/23370765?v=4", "profile": "https://github.com/wer153", "contributions": [ "code", "doc" ] }, { "login": "jseop-lim", "name": "Jeongseop Lim", "avatar_url": "https://avatars.githubusercontent.com/u/86508420?v=4", "profile": "https://velog.io/@azzurri21", "contributions": [ "doc" ] }, { "login": "FergusMok", "name": "FergusMok", "avatar_url": "https://avatars.githubusercontent.com/u/10182564?v=4", "profile": "https://github.com/FergusMok", "contributions": [ "doc", "code", "test" ] }, { "login": "manusinghal19", "name": "Manu Singhal", "avatar_url": "https://avatars.githubusercontent.com/u/8455587?v=4", "profile": "https://github.com/manusinghal19", "contributions": [ "doc" ] }, { "login": "jrycw", "name": "Jerry Wu", "avatar_url": "https://avatars.githubusercontent.com/u/67060418?v=4", "profile": "https://cv.ycwu.space", "contributions": [ "doc" ] }, { "login": "horo-fox", "name": "horo", "avatar_url": "https://avatars.githubusercontent.com/u/143025439?v=4", "profile": "https://github.com/horo-fox", "contributions": [ "bug" ] }, { "login": "rosstitmarsh", "name": "Ross Titmarsh", "avatar_url": "https://avatars.githubusercontent.com/u/23349806?v=4", "profile": "https://github.com/rosstitmarsh", "contributions": [ "code" ] }, { "login": "korneevm", "name": "Mike Korneev", "avatar_url": "https://avatars.githubusercontent.com/u/743250?v=4", "profile": "https://github.com/korneevm", "contributions": [ "doc" ] }, { "login": "patrickneise", "name": "Patrick Neise", "avatar_url": "https://avatars.githubusercontent.com/u/6312074?v=4", "profile": "https://github.com/patrickneise", "contributions": [ "code" ] }, { "login": "JeanArhancet", "name": "Jean Arhancet", "avatar_url": "https://avatars.githubusercontent.com/u/10811879?v=4", "profile": "https://github.com/JeanArhancet", "contributions": [ "bug" ] }, { "login": "betaprior", "name": "Leo Alekseyev", "avatar_url": "https://avatars.githubusercontent.com/u/338250?v=4", "profile": "http://dnquark.com", "contributions": [ "code" ] }, { "login": "aranvir", "name": "aranvir", "avatar_url": "https://avatars.githubusercontent.com/u/75439739?v=4", "profile": "https://github.com/aranvir", "contributions": [ "doc", "code", "test" ] }, { "login": "bunny-therapist", "name": "bunny-therapist", "avatar_url": "https://avatars.githubusercontent.com/u/87039365?v=4", "profile": "https://github.com/bunny-therapist", "contributions": [ "code" ] }, { "login": "benluo", "name": "Ben Luo", "avatar_url": "https://avatars.githubusercontent.com/u/70398?v=4", "profile": "http://www.benluo.cc", "contributions": [ "doc" ] }, { "login": "hugovk", "name": "Hugo van Kemenade", "avatar_url": "https://avatars.githubusercontent.com/u/1324225?v=4", "profile": "https://github.com/hugovk", "contributions": [ "doc" ] }, { "login": "error418", "name": "Michael Gerbig", "avatar_url": "https://avatars.githubusercontent.com/u/7716544?v=4", "profile": "https://error418.github.io", "contributions": [ "doc" ] }, { "login": "crisog", "name": "CrisOG", "avatar_url": "https://avatars.githubusercontent.com/u/40803711?v=4", "profile": "https://github.com/crisog", "contributions": [ "bug", "code", "test" ] }, { "login": "haryle", "name": "harryle", "avatar_url": "https://avatars.githubusercontent.com/u/64817481?v=4", "profile": "https://github.com/haryle", "contributions": [ "code", "test" ] }, { "login": "ubernostrum", "name": "James Bennett", "avatar_url": "https://avatars.githubusercontent.com/u/12384?v=4", "profile": "http://www.b-list.org/", "contributions": [ "bug" ] }, { "login": "sherbang", "name": "sherbang", "avatar_url": "https://avatars.githubusercontent.com/u/275015?v=4", "profile": "https://github.com/sherbang", "contributions": [ "doc" ] }, { "login": "carlsmedstad", "name": "Carl Smedstad", "avatar_url": "https://avatars.githubusercontent.com/u/6952324?v=4", "profile": "https://github.com/carlsmedstad", "contributions": [ "test" ] }, { "login": "maintain0404", "name": "Taein Min", "avatar_url": "https://avatars.githubusercontent.com/u/50428534?v=4", "profile": "https://github.com/maintain0404", "contributions": [ "doc" ] }, { "login": "wallseat", "name": "Stanislav Lyu.", "avatar_url": "https://avatars.githubusercontent.com/u/26143672?v=4", "profile": "https://github.com/wallseat", "contributions": [ "bug" ] }, { "login": "tibor-reiss", "name": "Tibor Reiss", "avatar_url": "https://avatars.githubusercontent.com/u/75096465?v=4", "profile": "https://github.com/tibor-reiss", "contributions": [ "test", "doc", "code" ] }, { "login": "0xE111", "name": "Alex", "avatar_url": "https://avatars.githubusercontent.com/u/11032969?v=4", "profile": "https://pogrom.dev", "contributions": [ "bug", "code" ] }, { "login": "JorenSix", "name": "Joren Six", "avatar_url": "https://avatars.githubusercontent.com/u/60453?v=4", "profile": "http://0110.be", "contributions": [ "doc" ] }, { "login": "jderrien", "name": "jderrien", "avatar_url": "https://avatars.githubusercontent.com/u/145396?v=4", "profile": "https://github.com/jderrien", "contributions": [ "doc" ] }, { "login": "PossiblePanda", "name": "PossiblePanda", "avatar_url": "https://avatars.githubusercontent.com/u/85448494?v=4", "profile": "https://possiblepanda.me", "contributions": [ "doc" ] }, { "login": "evstratbg", "name": "evstrat", "avatar_url": "https://avatars.githubusercontent.com/u/10176401?v=4", "profile": "https://github.com/evstratbg", "contributions": [ "infra" ] }, { "login": "eltociear", "name": "Ikko Eltociear Ashimine", "avatar_url": "https://avatars.githubusercontent.com/u/22633385?v=4", "profile": "https://speakerdeck.com/eltociear", "contributions": [ "doc" ] }, { "login": "taihim", "name": "Taimur Ibrahim", "avatar_url": "https://avatars.githubusercontent.com/u/13764071?v=4", "profile": "https://github.com/taihim", "contributions": [ "doc" ] }, { "login": "l-armstrong", "name": "l-armstrong", "avatar_url": "https://avatars.githubusercontent.com/u/43922258?v=4", "profile": "https://github.com/l-armstrong", "contributions": [ "doc" ] }, { "login": "Anu-cool-007", "name": "Anuranjan Srivastava", "avatar_url": "https://avatars.githubusercontent.com/u/16525919?v=4", "profile": "https://github.com/Anu-cool-007", "contributions": [ "code" ] }, { "login": "Zimzozaur", "name": "Simon Joseph", "avatar_url": "https://avatars.githubusercontent.com/u/106471045?v=4", "profile": "https://github.com/Zimzozaur", "contributions": [ "doc" ] }, { "login": "abelkm99", "name": "Abel Kidanemariam", "avatar_url": "https://avatars.githubusercontent.com/u/41730180?v=4", "profile": "https://github.com/abelkm99", "contributions": [ "code", "test", "doc" ] }, { "login": "trim21", "name": "Trim21", "avatar_url": "https://avatars.githubusercontent.com/u/13553903?v=4", "profile": "https://blog.trim21.me/", "contributions": [ "code", "test" ] }, { "login": "aarcex3", "name": "Agustin Arce", "avatar_url": "https://avatars.githubusercontent.com/u/59893355?v=4", "profile": "http://aarcex3.github.io", "contributions": [ "doc" ] }, { "login": "FarhanAliRaza", "name": "Farhan Ali Raza", "avatar_url": "https://avatars.githubusercontent.com/u/62690310?v=4", "profile": "https://github.com/FarhanAliRaza", "contributions": [ "doc" ] }, { "login": "pogopaule", "name": "Fabian", "avatar_url": "https://avatars.githubusercontent.com/u/576949?v=4", "profile": "https://github.com/pogopaule", "contributions": [ "code" ] }, { "login": "mohammedbabelly20", "name": "Mohammed Babelly", "avatar_url": "https://avatars.githubusercontent.com/u/104768048?v=4", "profile": "https://github.com/mohammedbabelly20", "contributions": [ "code" ] }, { "login": "charles-dyfis-net", "name": "Charles Duffy", "avatar_url": "https://avatars.githubusercontent.com/u/22370?v=4", "profile": "https://keybase.io/charlesdyfisnet", "contributions": [ "code" ] }, { "login": "RenameMe1", "name": "Evgeny Demchenko", "avatar_url": "https://avatars.githubusercontent.com/u/165988121?v=4", "profile": "https://github.com/RenameMe1", "contributions": [ "doc", "test" ] }, { "login": "olzhasar", "name": "Olzhas Arystanov", "avatar_url": "https://avatars.githubusercontent.com/u/12471703?v=4", "profile": "https://olzhasar.com", "contributions": [ "bug", "doc" ] }, { "login": "vikigenius", "name": "Vikash", "avatar_url": "https://avatars.githubusercontent.com/u/12724810?v=4", "profile": "https://github.com/vikigenius", "contributions": [ "code" ] }, { "login": "ftsartek", "name": "Jordan Russell", "avatar_url": "https://avatars.githubusercontent.com/u/20253317?v=4", "profile": "https://github.com/ftsartek", "contributions": [ "doc", "test", "code" ] }, { "login": "sloria", "name": "Steven Loria", "avatar_url": "https://avatars.githubusercontent.com/u/2379650?v=4", "profile": "https://stevenloria.com", "contributions": [ "doc" ] }, { "login": "oek1ng", "name": "oek1ng", "avatar_url": "https://avatars.githubusercontent.com/u/193062679?v=4", "profile": "https://github.com/oek1ng", "contributions": [ "code" ] }, { "login": "Ada-lave", "name": "Vladislav", "avatar_url": "https://avatars.githubusercontent.com/u/113159483?v=4", "profile": "https://github.com/Ada-lave", "contributions": [ "doc" ] }, { "login": "eandersons", "name": "Edgars", "avatar_url": "https://avatars.githubusercontent.com/u/9976861?v=4", "profile": "https://gaitenis.id.lv", "contributions": [ "doc" ] }, { "login": "Jannchie", "name": "Jianqi Pan", "avatar_url": "https://avatars.githubusercontent.com/u/29743310?v=4", "profile": "https://jannchie.com", "contributions": [ "code" ] } ], "contributorsPerLine": 7, "projectName": "litestar", "projectOwner": "litestar-org", "repoType": "github", "repoHost": "https://github.com", "skipCi": true, "commitConvention": "angular", "commitType": "docs" } litestar-2.16.0/.devcontainer/000077500000000000000000000000001500564371300161725ustar00rootroot00000000000000litestar-2.16.0/.devcontainer/Dockerfile000066400000000000000000000012611500564371300201640ustar00rootroot00000000000000# [Choice] Python version (use -bookworm or -bullseye variants on local arm64/Apple Silicon): 3, 3.13, 3.12, 3.11, 3.10, 3.9, 3.8, 3-bookworm, 3.13-bookworm, 3.12-bookworm, 3.11-bookworm, 3.10-bookworm, 3.9-bookworm, 3.8-bookworm, 3-bullseye, 3.11-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3-buster, 3.11-buster, 3.10-buster, 3.9-buster, 3.8-buster ARG VERSION=3.12 ARG VARIANT=-bookworm FROM python:${VERSION}${VARIANT} ARG VERSION ENV UV_LOCKED=1 UV_PYTHON=${VERSION} COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get purge -y fish RUN python3 -m pip install --upgrade setuptools cython pip litestar-2.16.0/.devcontainer/devcontainer.json000066400000000000000000000034061500564371300215510ustar00rootroot00000000000000{ "name": "litestar-org/litestar", "build": { "dockerfile": "./Dockerfile", "context": "." }, "features": { "ghcr.io/devcontainers/features/common-utils:2": { "installZsh": "true", "username": "vscode", "userUid": "1000", "userGid": "1000", "upgradePackages": "true" }, "ghcr.io/devcontainers/features/github-cli:1": {}, "ghcr.io/devcontainers-contrib/features/pre-commit:2": {}, "ghcr.io/devcontainers/features/python:1": "none", "ghcr.io/devcontainers/features/git:1": { "version": "latest", "ppa": "false" } }, "customizations": { "codespaces": { "openFiles": ["CONTRIBUTING.rst"] }, "vscode": { "extensions": [ "mhutchie.git-graph", "eamodio.gitlens", "github.vscode-github-actions", "ms-python.black-formatter", "ms-python.mypy-type-checker", "charliermarsh.ruff" ], "settings": { "python.editor.defaultFormatter": "charliermarsh.ruff", "python.defaultInterpreterPath": "${workspaceFolder}/.venv", "python.terminal.activateEnvInCurrentTerminal": true, "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.testing.pytestArgs": ["."], "python.terminal.launchArgs": ["-X", "dev"], "terminal.integrated.shell.linux": "/bin/bash", "terminal.integrated.profiles.linux": { "bash": { "path": "bash", "icon": "terminal-bash" }, "zsh": { "path": "zsh" }, "fish": { "path": "fish" } } } } }, "forwardPorts": [8000], "postCreateCommand": [ "uv", "sync" ], "remoteUser": "vscode" } litestar-2.16.0/.github/000077500000000000000000000000001500564371300147735ustar00rootroot00000000000000litestar-2.16.0/.github/CODEOWNERS000066400000000000000000000010761500564371300163720ustar00rootroot00000000000000# Code owner settings for `litestar` # @maintainers should be assigned to all reviews. # Most specific assignment takes precedence though, so if you add a more specific thing than the `*` glob, you must also add @maintainers # For more info about code owners see https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-example # Global Assignment * @litestar-org/maintainers @litestar-org/members # Documentation docs/* @litestar-org/maintainers @JacobCoffee @provinzkraut litestar-2.16.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001500564371300171565ustar00rootroot00000000000000litestar-2.16.0/.github/ISSUE_TEMPLATE/BUG.yml000066400000000000000000000051221500564371300203160ustar00rootroot00000000000000name: "Bug Report" description: Create an issue for a bug. title: "Bug: " labels: - "Bug :bug:" - "Triage Required" projects: - "litestar-org/16" body: - type: textarea id: description attributes: label: "Description" description: Please enter an description of the bug you are encountering placeholder: validations: required: true - type: input id: reprod-url attributes: label: "URL to code causing the issue" description: Please enter the URL to provide a reproduction of the issue, if applicable placeholder: ex. https://github.com/USERNAME/REPO-NAME validations: required: false - type: textarea id: mcve attributes: label: "MCVE" description: >- Please provide a minimal, complete, and verifiable example of the issue. This will be automatically formatted into code, so no need for backticks. placeholder: | from litestar import Litestar, get @get("/") def hello_world() -> str: return "hello world" app = Litestar(route_handlers=[hello_world]) render: python validations: required: false - type: textarea id: reprod attributes: label: "Steps to reproduce" description: Please enter the exact steps to reproduce the issue value: | 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error validations: required: false - type: textarea id: screenshot attributes: label: "Screenshots" description: If applicable, add screenshots to help explain your problem. placeholder: Drag-and-drop images up add them directly or use Markdown to embed external images. validations: required: false - type: textarea id: logs attributes: label: "Logs" description: >- Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. render: text validations: required: false - type: textarea id: version attributes: label: "Litestar Version" description: What version of Litestar are you using when encountering this issue? validations: required: true - type: checkboxes id: platform attributes: label: "Platform" description: What platform are you encountering the issue on? options: - label: "Linux" - label: "Mac" - label: "Windows" - label: "Other (Please specify in the description above)" validations: required: false ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/ISSUE_TEMPLATE/DOCS.yml�����������������������������������������������������0000664�0000000�0000000�00000000650�15005643713�0020432�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: "Documentation Update" description: Create an issue for documentation changes title: "Docs: <title>" labels: - "Documentation :books:" projects: - "litestar-org/16" body: - type: textarea id: summary attributes: label: "Summary" description: Provide a brief summary of your feature request placeholder: Describe in a few lines your feature request validations: required: true ����������������������������������������������������������������������������������������litestar-2.16.0/.github/ISSUE_TEMPLATE/REQUEST.yml��������������������������������������������������0000664�0000000�0000000�00000002343�15005643713�0021033�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: "Feature Request" description: Create an issue for a new feature request title: "Enhancement: <title>" labels: - "Enhancement" projects: - "litestar-org/16" body: - type: textarea id: summary attributes: label: "Summary" description: Provide a brief summary of your feature request placeholder: Describe in a few lines your feature request validations: required: true - type: textarea id: basic_example attributes: label: "Basic Example" description: Indicate here some basic examples of your feature. placeholder: Provide some basic example of your feature request validations: required: false - type: textarea id: drawbacks attributes: label: "Drawbacks and Impact" description: What are the drawbacks or impacts of your feature request? placeholder: Describe any the drawbacks or impacts of your feature request validations: required: false - type: textarea id: unresolved_question attributes: label: "Unresolved questions" description: What, if any, unresolved questions do you have about your feature request? placeholder: Identify any unresolved issues. validations: required: false ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/ISSUE_TEMPLATE/config.yml���������������������������������������������������0000664�0000000�0000000�00000000727�15005643713�0021154�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������blank_issues_enabled: false contact_links: - name: Litestar Documentation url: https://docs.litestar.dev/ about: Official Litestar documentation - please check here before opening an issue. - name: Litestar Website url: https://litestar.dev/ about: Main Litestar website - for details about Litestar's projects. - name: Discord url: https://discord.gg/litestar about: Join our Discord community to chat or get in touch with the maintainers. �����������������������������������������litestar-2.16.0/.github/PULL_REQUEST_TEMPLATE.md����������������������������������������������������0000664�0000000�0000000�00000001320�15005643713�0020570�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- By submitting this pull request, you agree to: - follow [Litestar's Code of Conduct](https://github.com/litestar-org/.github/blob/main/CODE_OF_CONDUCT.md) - follow [Litestar's contribution guidelines](https://github.com/litestar-org/.github/blob/main/CONTRIBUTING.md) - follow the [PSFs's Code of Conduct](https://www.python.org/psf/conduct/) --> ## Description - <!-- Please add in issue numbers this pull request will close, if applicable Examples: Fixes #4321 or Closes #1234 Ensure you are using a supported keyword to properly link an issue: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword --> ## Closes ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/dependabot.yml��������������������������������������������������������������0000664�0000000�0000000�00000000165�15005643713�0017625�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/labeler.yml�����������������������������������������������������������������0000664�0000000�0000000�00000013624�15005643713�0017132�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������version: v1 labels: # -- types ------------------------------------------------------------------- - label: 'type/feat' sync: true matcher: title: '^feat(\([^)]+\))?!?:' - label: 'type/bug' sync: true matcher: title: '^fix(\([^)]+\))?!?:' - label: 'type/docs' sync: true matcher: title: '^docs(\([^)]+\))?:' - label: 'Breaking 🔨' sync: true matcher: title: '^(feat|fix)(\([^)]+\))?!:' # -- distinct areas ---------------------------------------------------------- - label: '3.x' sync: true matcher: baseBranch: '^v3$' - label: 'area/docs' sync: true matcher: files: any: ['docs/*', 'docs/**/*', '**/*.rst', '**/*.md'] - label: 'area/unit-tests' sync: true matcher: files: any: ['test/unit/*', 'test/unit/**/*'] - label: 'area/end-to-end-tests' sync: true matcher: files: any: ['test/e2e/*', 'test/e2e/**/*'] - label: 'area/test-apps' sync: true matcher: files: any: ['test/test_apps/*', 'test/test_apps/**/*'] - label: 'area/docs' sync: true matcher: files: any: ['docs/*', 'docs/**/*', '**/*.rst', '**/*.md'] - label: 'area/unit-tests' sync: true matcher: files: any: ['test/unit/*', 'test/unit/**/*'] - label: 'area/end-to-end-tests' sync: true matcher: files: any: ['test/e2e/*', 'test/e2e/**/*'] - label: 'area/test-apps' sync: true matcher: files: any: ['test/test_apps/*', 'test/test_apps/**/*'] - label: 'area/ci' sync: true matcher: files: any: ['.github/**/*', 'codecov.yml', 'pre-commit-config.yaml', 'sonar-project.properties'] - label: 'area/dependencies' sync: true matcher: files: any: ['pyproject.toml', '*.lock'] - label: 'area/enums' sync: true matcher: files: ['litestar/enums.py'] - label: 'area/background-tasks' sync: true matcher: files: ['litestar/background_tasks.py'] - label: 'area/constants' sync: true matcher: files: ['litestar/constants.py'] - label: 'area/concurrency' sync: true matcher: files: ['litestar/concurrency.py'] - label: 'area/parsers' sync: true matcher: files: ['litestar/_parsers.py'] - label: 'area/layers' sync: true matcher: files: ['litestar/_layers/*'] - label: 'area/multipart' sync: true matcher: files: ['litestar/_multipart.py'] - label: 'area/di' sync: true matcher: files: ['litestar/di.py'] - label: 'area/file-system' sync: true matcher: files: ['litestar/file_system.py'] - label: 'area/controller' sync: true matcher: files: ['litestar/controller.py'] - label: 'area/serialization' sync: true matcher: files: ['litestar/serialization/*'] - label: 'area/params' sync: true matcher: files: ['litestar/params.py'] - label: 'area/template' sync: true matcher: files: ['litestar/template/*'] - label: 'area/events' sync: true matcher: files: ['litestar/events/*'] - label: 'area/router' sync: true matcher: files: ['litestar/router.py'] - label: 'area/exceptions' sync: true matcher: files: ['litestar/exceptions/*'] - label: 'area/static-files' sync: true matcher: files: ['litestar/static_files/*'] - label: 'area/signature' sync: true matcher: files: ['litestar/_signature/*'] - label: 'area/plugins' sync: true matcher: files: ['litestar/plugins/*'] - label: 'area/stores' sync: true matcher: files: ['litestar/stores/*'] - label: 'area/logging' sync: true matcher: files: ['litestar/logging/*'] - label: 'area/connection' sync: true matcher: files: ['litestar/connection/*'] - label: 'area/asgi' sync: true matcher: files: ['litestar/_asgi/*'] - label: 'area/types' sync: true matcher: files: ['litestar/types/*'] - label: 'area/kwargs' sync: true matcher: files: ['litestar/_kwargs/*'] - label: 'area/datastructures' sync: true matcher: files: ['litestar/datastructures/*'] - label: 'area/channels' sync: true matcher: files: ['litestar/channels/*'] - label: 'area/response' sync: true matcher: files: ['litestar/response/*'] - label: 'area/repository' sync: true matcher: files: ['litestar/repository/*'] - label: 'area/security' sync: true matcher: files: ['litestar/security/*'] - label: 'area/dto' sync: true matcher: files: ['litestar/dto/*'] - label: 'area/testing' sync: true matcher: files: ['litestar/testing/*'] - label: 'area/openapi' sync: true matcher: files: ['litestar/_openapi/*'] - label: 'area/middleware' sync: true matcher: files: ['litestar/middleware/*'] - label: 'area/handlers' sync: true matcher: files: ['litestar/handlers/*'] - label: 'area/contrib' sync: true matcher: files: ['litestar/contrib/*'] - label: 'area/private-api' sync: true matcher: files: any: ['litestar/_*.py', 'litestar/*/_*.py', 'litestar/_*/**/*.py'] # -- Size Based Labels ------------------------------------------------------- - label: 'size: small' sync: true matcher: files: count: gte: 1 lte: 10 - label: 'size: medium' sync: true matcher: files: count: gte: 10 lte: 25 - label: 'size: large' sync: true matcher: files: count: gte: 26 # -- Merge Checks -------------------------------------------------------------- checks: - context: 'No Merge check' description: "Disable merging when 'do not merge' label is set" labels: none: ['do not merge'] ������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/workflows/������������������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0017030�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/workflows/ci.yml������������������������������������������������������������0000664�0000000�0000000�00000022326�15005643713�0020153�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: Tests And Linting on: pull_request: merge_group: push: branches: - main - v1.51 env: UV_LOCKED: 1 jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.8" - name: Install Pre-Commit run: python -m pip install pre-commit - name: Load cached Pre-Commit Dependencies id: cached-pre-commit-dependencies uses: actions/cache@v4 with: path: ~/.cache/pre-commit/ key: pre-commit-4|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} - name: Execute Pre-Commit run: pre-commit run --show-diff-on-failure --color=always --all-files mypy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.8" allow-prereleases: true - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.5.4" enable-cache: true - name: Run mypy run: uv run mypy pyright: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.8" allow-prereleases: true - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.5.4" enable-cache: true - name: Run pyright run: uv run pyright slotscheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.8" allow-prereleases: false - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.5.4" enable-cache: true - name: Run slotscheck run: uv run slotscheck litestar test: name: "test (${{ matrix.python-version }})" strategy: fail-fast: true matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] uses: ./.github/workflows/test.yml with: coverage: ${{ (matrix.python-version == '3.12' || matrix.python-version == '3.8') }} python-version: ${{ matrix.python-version }} # add an aggregate step here to check if any of the steps of the matrix 'test' job # failed. this allows us to have dynamic or diverging steps in the matrix, while still # being able to mark the 'test' step as a required check for a PR to be considered # mergeable, without having to specify each individual matrix item. test_success: needs: test # ensure this step always runs if: always() runs-on: ubuntu-latest steps: - name: Report success or fail run: exit ${{ needs.test.result == 'success' && '0' || '1' }} test_typing_extensions: runs-on: ubuntu-latest strategy: matrix: typing-extensions: ["4.12.1", "4.13.1", "latest"] steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up python uses: actions/setup-python@v5 with: python-version: 3.13 - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.6.12" enable-cache: true - name: Install dependencies run: uv sync - name: Install typing-extensions if: ${{ matrix.typing-extensions != 'latest' }} run: uv pip install typing-extensions=="${{ matrix.typing-extensions }}" - name: Install typing-extensions if: ${{ matrix.typing-extensions == 'latest' }} run: uv pip install typing-extensions --upgrade --prerelease=allow - name: Test run: uv run --no-sync -- python -m pytest tests/unit/test_typing.py test_integration: name: Test server integration runs-on: ubuntu-latest strategy: matrix: uvicorn-version: ["uvicorn<0.27.0", "uvicorn>=0.27.0"] steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up python 3.11 uses: actions/setup-python@v5 with: python-version: 3.11 - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.5.4" enable-cache: true - name: Install Build Dependencies run: sudo apt-get install build-essential libpq-dev python3-dev -y - name: Install dependencies run: | uv sync uv pip install -U "${{ matrix.uvicorn-version }}" - name: Set PYTHONPATH run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV - name: Test run: uv run --no-sync pytest tests -m server_integration test-platform-compat: if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'test platform compat') strategy: fail-fast: false matrix: os: ["macos-latest", "windows-latest"] uses: ./.github/workflows/test.yml with: python-version: "3.13" os: ${{ matrix.os }} timeout: 30 codeql: needs: - test - validate runs-on: ubuntu-latest permissions: security-events: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Initialize CodeQL Without Dependencies uses: github/codeql-action/init@v3 with: setup-python-dependencies: false - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 build-docs: needs: - validate if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - name: Check out repository uses: actions/checkout@v4 - name: Install Build Dependencies run: sudo apt-get install build-essential libpq-dev python3-dev -y - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" allow-prereleases: true - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.5.4" enable-cache: true - name: Build docs run: uv run make docs - name: Check docs links env: LITESTAR_DOCS_IGNORE_MISSING_EXAMPLE_OUTPUT: 1 run: uv run make docs-linkcheck - name: Save PR number run: | echo "${{ github.event.number }}" > .pr_number - name: Upload artifact uses: actions/upload-artifact@v4 with: name: docs-preview path: | docs/_build/html .pr_number include-hidden-files: true test_minimal_app: name: Test Minimal Application with Base Dependencies runs-on: ubuntu-latest env: python_version: "3.12" steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.5.4" enable-cache: true - name: Set pythonpath run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV - name: Test run: | mv tests/examples/test_hello_world.py test_hello_world.py uv run pytest test_hello_world.py test_pydantic_1_app: name: Test Minimal Pydantic 1 application runs-on: ubuntu-latest env: python_version: "3.12" steps: - name: Check out repository uses: actions/checkout@v4 - name: Install Build Dependencies run: sudo apt-get install build-essential libpq-dev python3-dev -y - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.5.4" enable-cache: true - name: Install dependencies run: | uv sync uv pip install "pydantic==1.*" - name: Set pythonpath run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV - name: Test run: uv run --no-sync coverage run --branch -m unittest test_apps/pydantic_1_app.py - name: Rename coverage file run: mv .coverage* .coverage.pydantic_v1 - uses: actions/upload-artifact@v4 with: name: coverage-data-pydantic_v1-${{ inputs.python-version }} path: .coverage.pydantic_v1 include-hidden-files: true upload-test-coverage: runs-on: ubuntu-latest needs: - test - test_pydantic_1_app steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Download Artifacts uses: actions/download-artifact@v4 with: pattern: coverage-data* merge-multiple: true - name: Combine coverage files run: | python -Im pip install coverage covdefaults python -Im coverage combine python -Im coverage xml -i - name: Fix coverage file name run: sed -i "s/home\/runner\/work\/litestar\/litestar/github\/workspace/g" coverage.xml - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v5 with: files: coverage.xml token: ${{ secrets.CODECOV_TOKEN }} ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/workflows/codeql.yml��������������������������������������������������������0000664�0000000�0000000�00000000674�15005643713�0021031�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: CodeQL scheduled on: schedule: - cron: "0 4 * * *" jobs: codeql: runs-on: ubuntu-latest permissions: security-events: write steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: "main" - name: Initialize CodeQL With Dependencies uses: github/codeql-action/init@v3 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 ��������������������������������������������������������������������litestar-2.16.0/.github/workflows/docs-preview.yml��������������������������������������������������0000664�0000000�0000000�00000004732�15005643713�0022170�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: Deploy documentation preview on: workflow_run: workflows: [Tests And Linting] types: [completed] jobs: deploy: if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - name: Check out repository uses: actions/checkout@v4 - name: Download artifact uses: dawidd6/action-download-artifact@v9 with: workflow_conclusion: success run_id: ${{ github.event.workflow_run.id }} path: docs-preview name: docs-preview - name: Validate and set PR number run: | PR_NUMBER=$(cat docs-preview/.pr_number) if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then echo "Invalid PR number: $PR_NUMBER" exit 1 fi echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV - name: Deploy docs preview uses: JamesIves/github-pages-deploy-action@v4 with: folder: docs-preview/docs/_build/html token: ${{ secrets.DOCS_PREVIEW_DEPLOY_TOKEN }} repository-name: litestar-org/litestar-docs-preview clean: false target-folder: ${{ env.PR_NUMBER }} branch: gh-pages - uses: actions/github-script@v7 env: PR_NUMBER: ${{ env.PR_NUMBER }} with: script: | const issue_number = process.env.PR_NUMBER const body = "Documentation preview will be available shortly at https://litestar-org.github.io/litestar-docs-preview/" + issue_number const opts = github.rest.issues.listComments.endpoint.merge({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue_number, }); const comments = await github.paginate(opts) for (const comment of comments) { if (comment.user.id === 41898282 && comment.body === body) { await github.rest.issues.deleteComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: comment.id }) } } await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue_number, body: body, }) ��������������������������������������litestar-2.16.0/.github/workflows/docs.yml����������������������������������������������������������0000664�0000000�0000000�00000002340�15005643713�0020502�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: Documentation Building on: release: types: [published] push: branches: - main - v3.0 env: UV_LOCKED: 1 jobs: docs: permissions: contents: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.5.4" enable-cache: true - name: Install dependencies run: uv sync - name: Fetch gh pages run: git fetch origin gh-pages --depth=1 - name: Build release docs run: uv run python tools/build_docs.py docs-build if: github.event_name == 'release' - name: Build docs (main branch) run: uv run python tools/build_docs.py docs-build --version main if: github.event_name == 'push' && github.ref == 'refs/heads/main' - name: Build docs (v3.0 branch) run: uv run python tools/build_docs.py docs-build --version 3-dev if: github.event_name == 'push' && github.ref == 'refs/heads/v3.0' - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 with: folder: docs-build ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/workflows/notify-released-issues.yml����������������������������������������0000664�0000000�0000000�00000001570�15005643713�0024161�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: Notify released issues on: workflow_call: inputs: release_tag: type: string required: true jobs: notify: runs-on: ubuntu-latest steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Get released issues id: get-released-issues run: echo "issues=$(python ./.github/workflows/notify_released_issues/get_closed_issues.py ${{ inputs.release_tag }})" >> "$GITHUB_OUTPUT" - uses: actions/github-script@v7 env: CLOSED_ISSUES: ${{ steps.get-released-issues.outputs.issues }} with: script: | const script = require('./.github/workflows/notify_released_issues/notify.js') await script({github, context, core}) ����������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/workflows/notify_released_issues/�������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0023577�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/workflows/notify_released_issues/get_closed_issues.py�����������������������0000664�0000000�0000000�00000001650�15005643713�0027656�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations import itertools import json import pathlib import re import sys __all__ = ( "find_resolved_issues", "main", ) def find_resolved_issues(source: str, tag: str) -> list[str]: version = tag.split("v", maxsplit=1)[-1] changelog_line = f".. changelog:: {version}" stop_line = ".. changelog::" return list( { issue for line in itertools.takewhile( lambda l: stop_line not in l, # noqa: E741 source.split(changelog_line, maxsplit=1)[1].splitlines(), ) if re.match(r"\s+:issue: [\d ,]+", line) for issue in re.findall(r"\d+", line) } ) def main(tag: str) -> str: source = pathlib.Path("docs/release-notes/changelog.rst").read_text() return json.dumps(find_resolved_issues(source, tag)) if __name__ == "__main__": print(main(sys.argv[1])) # noqa: T201 ����������������������������������������������������������������������������������������litestar-2.16.0/.github/workflows/notify_released_issues/notify.js����������������������������������0000664�0000000�0000000�00000002273�15005643713�0025451�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������module.exports = async ({github, context, core}) => { const issues = JSON.parse(process.env.CLOSED_ISSUES) const releaseURL = context.payload.release.html_url const releaseName = context.payload.release.name const baseBody = "A fix for this issue has been released in" const body = baseBody + ` [${releaseName}](${releaseURL})` for (const issueNumber of issues) { const opts = github.rest.issues.listComments.endpoint.merge({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, }); const comments = await github.paginate(opts) for (const comment of comments) { if (comment.user.id === 41898282 && comment.body.startsWith(baseBody)) { await github.rest.issues.deleteComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: comment.id }) } } await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, body: body, }) } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/workflows/pr-labeler.yml����������������������������������������������������0000664�0000000�0000000�00000002622�15005643713�0021602�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: "Pull Request Labeler" on: pull_request_target: jobs: apply-labels: permissions: contents: read pull-requests: write checks: write statuses: write runs-on: ubuntu-latest steps: - uses: fuxingloh/multi-labeler@v4 with: github-token: "${{ secrets.GITHUB_TOKEN }}" distinguish-pr-origin: needs: apply-labels if: ${{ always() }} permissions: pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/github-script@v7 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | const maintainers = [ 'JacobCoffee', 'provinzkraut', 'cofin', 'peterschutt', 'Alc-Alc', 'guacs', 'dependabot[bot]', 'all-contributors[bot]' ] if (maintainers.includes(context.payload.sender.login)) { github.rest.issues.addLabels({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, labels: ['pr/internal'] }) } else { github.rest.issues.addLabels({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, labels: ['pr/external', 'Triage Required :hospital:'] }) } ��������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/workflows/pr-merged.yml�����������������������������������������������������0000664�0000000�0000000�00000003654�15005643713�0021445�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: "PR merged" on: pull_request: types: - closed branches: - main - v3.0 jobs: close_and_notify: name: Close issues and notify if: github.event.pull_request.merged == true runs-on: ubuntu-latest steps: - uses: actions/github-script@v7 with: script: | const prNumber = context.payload.number const branch = context.baseRef // TODO: use semantic commits to specify the exact version, when it will be released const commentBody = `<!--closing-comment-->\nThis issue has been closed in #${prNumber}. The change will be included in upcoming releases.` const query = `query($number: Int!, $owner: String!, $name: String!) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { id closingIssuesReferences (first: 10) { edges { node { number } } } } } }` const res = await github.graphql(query, {number: prNumber, owner: context.repo.owner, name: context.repo.repo}) const linkedIssues = res.repository.pullRequest.closingIssuesReferences.edges.map( edge => edge.node.number ) for (const issueNumber of linkedIssues) { const res = await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, state: "closed", state_reason: "completed" }) if (res.status === 200) { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, body: commentBody, }) } } ������������������������������������������������������������������������������������litestar-2.16.0/.github/workflows/pr-title.yml������������������������������������������������������0000664�0000000�0000000�00000000532�15005643713�0021313�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: "Lint PR Title" on: pull_request_target: types: - opened - edited - synchronize permissions: pull-requests: read jobs: main: name: Validate PR title runs-on: ubuntu-latest steps: - uses: amannn/action-semantic-pull-request@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ����������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/workflows/publish.yml�������������������������������������������������������0000664�0000000�0000000�00000001557�15005643713�0021231�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: Latest Release on: release: types: [published] workflow_dispatch: jobs: publish-release: name: upload release to PyPI runs-on: ubuntu-latest permissions: id-token: write environment: release steps: - name: Check out repository uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.5.4" enable-cache: true - name: Build package run: uv build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 notify-issues: needs: publish-release name: Notify issues uses: ./.github/workflows/notify-released-issues.yml with: release_tag: ${{ github.event.release.tag_name }} �������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.github/workflows/test.yml����������������������������������������������������������0000664�0000000�0000000�00000004134�15005643713�0020534�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: Test on: workflow_call: inputs: python-version: required: true type: string coverage: required: false type: boolean default: false os: required: false type: string default: "ubuntu-latest" timeout: required: false type: number default: 10 env: UV_LOCKED: 1 jobs: test: runs-on: ${{ inputs.os }} timeout-minutes: ${{ inputs.timeout }} defaults: run: shell: bash steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up python ${{ inputs.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} # Linux Source - name: Install Build Dependencies run: sudo apt-get install build-essential libpq-dev python3-dev -y if: startsWith(inputs.os, 'ubuntu') # MacOS Source - name: Install Build Dependencies run: brew install libpq && brew link --force libpq if: startsWith(inputs.os, 'macos') # Windows Source - name: Install Build Dependencies uses: ikalnytskyi/action-setup-postgres@v7 if: startsWith(inputs.os, 'windows') - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.6.12" enable-cache: true - name: Install dependencies run: uv sync - name: Set PYTHONPATH run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV - name: Test if: ${{ !inputs.coverage }} run: uv run pytest docs/examples tests -n auto - name: Test with coverage if: inputs.coverage run: uv run pytest docs/examples tests -n auto --cov - name: Rename coverage file if: inputs.coverage run: mv .coverage .coverage.${{ inputs.python-version }} - uses: actions/upload-artifact@v4 if: inputs.coverage with: name: coverage-data-${{ inputs.python-version }} path: .coverage.${{ inputs.python-version }} include-hidden-files: true ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.gitignore��������������������������������������������������������������������������0000664�0000000�0000000�00000001004�15005643713�0015416�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# folders *.egg-info/ .auto_pytabs_cache/ .hypothesis/ .idea/ .mypy_cache/ .pytest_cache/ .scannerwork/ .unasyncd_cache/ .venv/ .venv* .vscode/ __pycache__/ assets/ build/ dist/ html/ node_modules/ results/ site/ target/ # files **/*.so **/*.sqlite **/*.sqlite* *.iml .DS_Store .coverage .ruff_cache /docs/_build/ coverage.* setup.py # pdm .pdm.toml .pdm-python .pdm-build/ # pdm - PEP 582 __pypackages__/ # pyenv / rtx / asdf .tool-versions .python-version /.dmypy.json # test certificates certs/ pdm.toml .zed ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/.pre-commit-config.yaml�������������������������������������������������������������0000664�0000000�0000000�00000002756�15005643713�0017726�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������default_language_version: python: "3.12" repos: - repo: https://github.com/compilerla/conventional-pre-commit rev: v3.6.0 hooks: - id: conventional-pre-commit stages: [commit-msg] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-ast - id: check-case-conflict - id: check-toml - id: debug-statements exclude: ^(litestar/config/app\.py|litestar/app\.py|test_apps/debugging/main\.py)$ - id: end-of-file-fixer - id: mixed-line-ending - id: trailing-whitespace exclude: "tests/unit/test_openapi/test_typescript_converter/test_converter.py" - repo: https://github.com/provinzkraut/unasyncd rev: "v0.8.1" hooks: - id: unasyncd additional_dependencies: ["ruff"] - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.8.1" hooks: - id: ruff args: ["--fix"] - id: ruff-format - repo: https://github.com/crate-ci/typos rev: v1.30.3 hooks: - id: typos - repo: https://github.com/python-formate/flake8-dunder-all rev: v0.4.1 hooks: - id: ensure-dunder-all exclude: "test*|examples*|tools" args: ["--use-tuple"] - repo: https://github.com/sphinx-contrib/sphinx-lint rev: "v1.0.0" hooks: - id: sphinx-lint - repo: local hooks: - id: pypi-readme name: pypi-readme language: python entry: python tools/pypi_readme.py types: [markdown] ������������������litestar-2.16.0/CITATION.cff������������������������������������������������������������������������0000664�0000000�0000000�00000003053�15005643713�0015326�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������cff-version: 1.2.0 title: Litestar message: 'If you use this software, please cite it as below.' type: software authors: - given-names: Janek Nouvertné - given-names: Peter Schutt - given-names: Cody Fincher - given-names: Visakh Unnikrishnan - given-names: Jacob Coffee - given-names: Na'aman Hirschfeld repository-code: 'https://github.com/litestar-org/litestar' url: 'https://docs.litestar.dev/latest/' abstract: >- Litestar is a powerful, flexible, and highly performant Python web framework for building modern APIs and applications. With an emphasis on developer experience and performance, Litestar provides a rich set of features out of the box, including automatic API documentation, data validation and serialization, ORM integration, dependency injection, caching, websockets, and more. Litestar's layered architecture and open ecosystem enable seamless integration with popular libraries like Pydantic, SQLAlchemy, and msgspec. It offers both asynchronous and synchronous execution models without performance penalties. With Litestar, you can effortlessly build and deploy production-ready APIs and web applications, leveraging features like interactive API documentation, middlewares for common tasks, session and JWT-based authentication, and strict runtime validation for enhanced safety. Experience the perfect blend of ease of use, flexibility and performance with Litestar. keywords: - python - web - framework - typing - dependency injection - api license: MIT version: v2.8.0 date-released: '2024-04-05' �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/CONTRIBUTING.rst��������������������������������������������������������������������0000664�0000000�0000000�00000034024�15005643713�0016077�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������Contribution Guide ================== .. _pipx: https://pypa.github.io/pipx/ .. |pipx| replace:: `pipx <https://pypa.github.io/pipx/>`__ .. _homebrew: https://brew.sh/ .. |homebrew| replace:: `Homebrew <https://brew.sh/>`__ Getting Started --------------- Supported Python Versions +++++++++++++++++++++++++ The lowest currently supported version is Python 3.8. At a minimum you will need Python 3.8 for code changes and 3.12 if you plan on doing documentation building / changes. You can use various tools to manage multiple Python versions on your system including: * `pyenv <https://github.com/pyenv/pyenv>`_ and `pyenv-win for Windows <https://github.com/pyenv-win/pyenv-win>`_ * `rtx / mise <https://mise.jdx.dev/>`_ * `asdf <https://asdf-vm.com/>`_ * `Building each version manually from source <https://www.build-python-from-source.com/>`_ * Utilizing `GitHub Codespaces <https://codespaces.new/litestar-org/litestar?quickstart=1>`_ We use the lowest supported version in our type-checking CI, this ensures that the changes you made are backward compatible. Setting up the environment ++++++++++++++++++++++++++ .. tip:: We maintain a Makefile with several commands to help with common tasks. You can run ``make help`` to see a list of available commands. If you are utilizing `GitHub Codespaces <https://codespaces.new/litestar-org/litestar?quickstart=1>`_, the environment will bootstrap itself automatically. The steps below are for local development. #. Install `uv <https://docs.astral.sh/uv/getting-started/installation/>`_: #. Run ``make install`` to create a `virtual environment <https://docs.python.org/3/tutorial/venv.html>`_ and install the required development dependencies or run the ``uv sync`` command manually: .. code-block:: shell :caption: Installing the development dependencies uv sync .. tip:: Many modern IDEs like PyCharm or VS Code will enable the uv-managed virtualenv that is created in step 2 for you automatically. If your IDE / editor does not offer this functionality, then you will need to manually activate the virtualenv yourself. Otherwise you may encounter errors or unexpected behaviour when trying to run the commands referenced within this document. To activate the virtualenv manually, please consult uv's documentation on `working with virtual environments <https://docs.astral.sh/uv/pip/environments/>`_. The rest of this document will assume this environment is active wherever commands are referenced. Code contributions ------------------ Workflow ++++++++ #. `Fork <https://github.com/litestar-org/litestar/fork>`_ the `Litestar repository <https://github.com/litestar-org/litestar>`_ #. Clone your fork locally with git #. `Set up the environment <#setting-up-the-environment>`_ #. Make your changes #. (Optional) Run ``pre-commit run --all-files`` to run linters and formatters. This step is optional and will be executed automatically by git before you make a commit, but you may want to run it manually in order to apply fixes #. Commit your changes to git. We follow `conventional commits <https://www.conventionalcommits.org/>`_ which are enforced using a ``pre-commit`` hook. #. Push the changes to your fork #. Open a `pull request <https://docs.github.com/en/pull-requests>`_. Give the pull request a descriptive title indicating what it changes. The style of the PR title should also follow `conventional commits <https://www.conventionalcommits.org/>`_, and this is enforced using a GitHub action. #. Add yourself as a contributor using the `all-contributors bot <https://allcontributors.org/docs/en/bot/usage>`_ Guidelines for writing code ---------------------------- - Code should be `Pythonic and zen <https://peps.python.org/pep-0020/>`_ - All code should be fully `typed <https://peps.python.org/pep-0484/>`_. This is enforced via `mypy <https://mypy.readthedocs.io/en/stable/>`_ and `Pyright <https://github.com/microsoft/pyright/>`_ * When requiring complex types, use a `type alias <https://docs.python.org/3/library/typing.html#type-aliases>`_. Check :doc:`reference/types` if a type alias for your use case already exists * If something cannot be typed correctly due to a limitation of the type checkers, you may use :func:`typing.cast` to rectify the situation. However, you should only use this as a last resort if you've exhausted all other options of `type narrowing <https://mypy.readthedocs.io/en/stable/type_narrowing.html>`_, such as :func:`isinstance` checks and `type guards <https://docs.python.org/3/library/typing.html#typing.TypeGuard>`_. * You may use a properly scoped ``type: ignore`` if you ensured that a line is correct, but mypy / pyright has issues with it. Properly scoped meaning do not use blank ``type: ignore``, instead supply the specific error code, e.g., ``type: ignore[attr-defined]`` - If you are adding or modifying existing code, ensure that it's fully tested. 100% test coverage is mandatory, and will be checked on the PR using `SonarCloud <https://www.sonarsource.com/products/sonarcloud/>`_ and `Codecov <https://codecov.io/>`_ - All functions, methods, classes, and attributes should be documented with a docstring. We use the `Google docstring style <https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html>`_. If you come across a function or method that doesn't conform to this standard, please update it as you go - When adding a new public interface, it has to be included in the reference documentation located in ``docs/reference``. If applicable, add or modify examples in the docs related to the new functionality implemented, following the guidelines established in `Adding examples`_. Writing and running tests +++++++++++++++++++++++++ Tests are contained within the ``tests`` directory, and follow the same directory structure as the ``litestar`` module. If you are adding a test case, it should be located within the correct submodule of ``tests``. E.g., tests for ``litestar/utils/sync.py`` reside in ``tests/utils/test_sync.py``. The ``Makefile`` includes several commands for running tests: - ``make test`` to run tests located in ``tests`` - ``make test-examples`` to run tests located in ``docs/examples/tests`` - ``make test-all`` to run all tests - ``make coverage`` to run tests with coverage and generate an html report The tests make use of `pytest-xdist <https://pytest-xdist.readthedocs.io>`_ to speed up test runs. These are enabled by default when running ``make test``, ``make test-all`` or ``make coverage``. Due to the nature of pytest-xdist, attaching a debugger isn't as straightforward. For debugging, it's recommended to run the tests individually with ``pytest <test name>`` or via an IDE, which will skip ``pytest-xdist``. Running type checkers +++++++++++++++++++++ We use `mypy <https://mypy.readthedocs.io/en/stable/>`_ and `pyright <https://github.com/microsoft/pyright/>`_ to enforce type safety. You can run them with: - ``make mypy`` - ``make pyright`` - ``make type-check`` to run both - ``make lint`` to run pre-commit hooks and type checkers. Our type checkers are run on Python 3.8 in CI, so you should make sure to run them on the same version locally as well. Project documentation --------------------- The documentation is located in the ``/docs`` directory and is written in `reStructuredText <https://docutils.sourceforge.io/rst.html>`_ with the `Sphinx <https://www.sphinx-doc.org/en/master/>`_. library. If you're unfamiliar with any of those, `reStructuredText primer <https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_ and `Sphinx quickstart <https://www.sphinx-doc.org/en/master/usage/quickstart.html>`_ are recommended reads. Docs theme and appearance +++++++++++++++++++++++++ We welcome contributions that enhance / improve the appearance and usability of the docs. We use the excellent `PyData Sphinx Theme <https://pydata-sphinx-theme.readthedocs.io/>`_ theme, which comes with a lot of options out of the box. If you wish to contribute to the docs style / setup, or static site generation, you should consult the theme docs as a first step. Running the docs locally ++++++++++++++++++++++++ You can serve the documentation locally with .. code-block:: shell :caption: Serving the documentation locally make docs-serve or build it with .. code-block:: shell :caption: Serving the documentation locally make docs Writing and editing docs ++++++++++++++++++++++++ We welcome contributions that enhance / improve the content of the docs. Feel free to add examples, clarify text, restructure the docs, etc., but make sure to follow these guidelines: - Write text in idiomatic English, using simple language - Do not use contractions for ease of reading for non-native English speakers - Opt for `Oxford commas <https://en.wikipedia.org/wiki/Serial_comma>`_ when listing a series of terms - Keep examples simple and self contained (see `Adding examples`_). This is to ensure they are tested alongside the rest of the test suite and properly type checked and linted. - Provide links where applicable. - Use `intersphinx <https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html>`_ wherever possible when referencing external libraries - Provide diagrams using `Mermaid <https://mermaid.js.org/>`_ where applicable and possible Adding examples ~~~~~~~~~~~~~~~ The examples from the docs are located in their own modules inside the ``/docs/examples`` folder. This makes it easier to test them alongside the rest of the test suite, ensuring they do not become stale as Litestar evolves. Please follow the next guidelines when adding a new example: - Add the example in the corresponding module directory in ``/docs/examples`` or create a new one if necessary - Create a suite for the module in ``/tests/examples`` that tests the aspects of the example that it demonstrates - Reference the example in the rst file with an external reference code block, e.g. .. code-block:: rst :caption: An example of how to use literal includes of external files .. literalinclude:: /examples/test_thing.py :language: python :caption: All includes should have a descriptive caption Automatically execute examples ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Our docs include a Sphinx extension that can automatically run requests against example apps and include their result in the documentation page when its being built. This only requires 2 steps: 1. Create an example file with an ``app`` object in it, which is an instance of ``Litestar`` 2. Add a comment in the form of ``# run: /hello`` to the example file When building the docs (or serving them locally), a process serving the ``app`` instance will be launched, and the requests specified in the comments will be run against it. The comments will be stripped from the result, and the output of the ``curl`` invocation inserted after the example code-block. The ``# run:`` syntax is nothing special; everything after the colon will be passed to the ``curl`` command that's being invoked. The URL is built automatically, so the specified path can just be a path relative to the app. In practice, this looks like the following: .. code-block:: python :caption: An example of how to use the automatic example runner :no-upgrade: from typing import Dict from litestar import Litestar, get @get("/") def hello_world() -> Dict[str, str]: """Handler function that returns a greeting dictionary.""" return {"hello": "world"} app = Litestar(route_handlers=[hello_world]) # run: / This is equivalent to: .. code-block:: python :caption: An example of how to use the automatic example runner from typing import Dict from litestar import Litestar, get @get("/") def hello_world() -> Dict[str, str]: """Handler function that returns a greeting dictionary.""" return {"hello": "world"} app = Litestar(route_handlers=[hello_world]) .. admonition:: Run it .. code-block:: bash > curl http://127.0.0.1:8000/ {"hello": "world"} Creating a New Release ---------------------- #. Checkout the ``main`` branch: .. code-block:: shell :caption: Checking out the main branch of the ``litestar`` repository git checkout main #. Run the release preparation script: .. code-block:: shell :caption: Preparing a new release python tools/prepare_release.py <new version number> --update-version --create-draft-release Replace ``<new version number>`` with the desired version number following the `versioning scheme <https://litestar.dev/about/litestar-releases#version-numbering>`_. This script will: - Update the version in ``pyproject.toml`` - Generate a changelog entry in :doc:`/release-notes/changelog` - Create a draft release on GitHub #. Review the generated changelog entry in :doc:`/release-notes/changelog` to ensure it looks correct. #. Commit the changes to ``main``: .. code-block:: shell :caption: Committing the changes to the main branch git commit -am "chore(release): prepare release vX.Y.Z" Replace ``vX.Y.Z`` with the actual version number. #. Create a new branch for the release: .. code-block:: shell :caption: Creating a new branch for the release git checkout -b vX.Y.Z #. Push the changes to a ``vX.Y.Z`` branch: .. code-block:: shell :caption: Pushing the changes to the ``vX.Y.Z`` branch git push origin vX.Y.Z #. Open a pull request from the ``vX.Y.Z`` branch to ``main``. #. Once the pull request is approved, go to the draft release on GitHub (the release preparation script will provide a link). #. Review the release notes in the draft release to ensure they look correct. #. If everything looks good, click "Publish release" to make the release official. #. Go to the `Release Action <https://github.com/litestar-org/litestar/actions/workflows/publish.yml>`_ and approve the release workflow if necessary. #. Check that the release workflow runs successfully. .. note:: The version number should follow `semantic versioning <https://semver.org/>`_ and `PEP 440 <https://peps.python.org/pep-0440/>`_. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/LICENSE�����������������������������������������������������������������������������0000664�0000000�0000000�00000002120�15005643713�0014433�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������The MIT License (MIT) Copyright (c) 2021, 2022, 2023, 2024, 2025 Litestar Org. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/Makefile����������������������������������������������������������������������������0000664�0000000�0000000�00000012633�15005643713�0015100�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������SHELL := /bin/bash # ============================================================================= # Variables # ============================================================================= .DEFAULT_GOAL:=help .ONESHELL: ENV_PREFIX = .venv/bin/ VENV_EXISTS = $(shell python3 -c "if __import__('pathlib').Path('.venv/bin/activate').exists(): print('yes')") .EXPORT_ALL_VARIABLES: .PHONY: help help: ## Display this help text for Makefile @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) .PHONY: upgrade upgrade: ## Upgrade all dependencies to the latest stable versions @echo "=> Updating all dependencies" @uv lock --upgrade @echo "=> Dependencies Updated" @uv run pre-commit autoupdate @echo "=> Updated Pre-commit" # ============================================================================= # Developer Utils # ============================================================================= .PHONY: install install: ## Install dependencies @uv sync .PHONY: clean clean: ## Cleanup temporary build artifacts @echo "=> Cleaning working directory" @rm -rf .pytest_cache .ruff_cache .hypothesis build/ -rf dist/ .eggs/ @find . -name '*.egg-info' -exec rm -rf {} + @find . -type f -name '*.egg' -exec rm -f {} + @find . -name '*.pyc' -exec rm -f {} + @find . -name '*.pyo' -exec rm -f {} + @find . -name '*~' -exec rm -f {} + @find . -name '__pycache__' -exec rm -rf {} + @find . -name '.ipynb_checkpoints' -exec rm -rf {} + @rm -rf .coverage coverage.xml coverage.json htmlcov/ .pytest_cache tests/.pytest_cache tests/**/.pytest_cache .mypy_cache $(MAKE) docs-clean .PHONY: destroy destroy: ## Destroy the virtual environment @rm -rf .venv .PHONY: lock lock: ## Rebuild lockfiles from scratch, updating all dependencies @uv lock # ============================================================================= # Tests, Linting, Coverage # ============================================================================= .PHONY: mypy mypy: ## Run mypy @echo "=> Running mypy" @uv run dmypy run @echo "=> mypy complete" .PHONY: mypy-nocache mypy-nocache: ## Run Mypy without cache @echo "=> Running mypy without a cache" @uv run dmypy run -- --cache-dir=/dev/null @echo "=> mypy complete" .PHONY: pyright pyright: ## Run pyright @echo "=> Running pyright" @uv run pyright @echo "=> pyright complete" .PHONY: type-check type-check: mypy pyright ## Run all type checking .PHONY: pre-commit pre-commit: ## Runs pre-commit hooks; includes ruff formatting and linting, codespell @echo "=> Running pre-commit process" @uv run pre-commit run --all-files @echo "=> Pre-commit complete" .PHONY: slots-check slots-check: ## Check for slots usage in classes @echo "=> Checking for slots usage in classes" @uv run slotscheck litestar @echo "=> Slots check complete" .PHONY: lint lint: pre-commit type-check slots-check ## Run all linting .PHONY: coverage coverage: ## Run the tests and generate coverage report @echo "=> Running tests with coverage" @uv run pytest tests --cov -n auto @uv run coverage html @uv run coverage xml @echo "=> Coverage report generated" .PHONY: test test: ## Run the tests @echo "=> Running test cases" @uv run pytest tests @echo "=> Tests complete" .PHONY: test-examples test-examples: ## Run the examples tests @uv run pytest docs/examples .PHONY: test-all test-all: test test-examples ## Run all tests .PHONY: check-all check-all: lint test-all coverage ## Run all linting, tests, and coverage checks # ============================================================================= # Docs # ============================================================================= # XXX: docs commands are pinned to Python 3.12 due to picologging not being compatible with 3.13 .PHONY: docs-install docs-install: ## Install docs dependencies @echo "=> Installing documentation dependencies" @uv sync --python 3.12 --group docs @echo "=> Installed documentation dependencies" docs-clean: ## Dump the existing built docs @echo "=> Cleaning documentation build assets" @rm -rf docs/_build @echo "=> Removed existing documentation build assets" docs-serve: docs-clean ## Serve the docs locally @echo "=> Serving documentation" uv run --python 3.12 sphinx-autobuild docs docs/_build/ -j auto --watch litestar --watch docs --watch tests --watch CONTRIBUTING.rst --open-browser --port=0 docs: docs-clean ## Dump the existing built docs and rebuild them @echo "=> Building documentation" @uv run --python 3.12 sphinx-build -M html docs docs/_build/ -E -a -j auto -W --keep-going .PHONY: docs-linkcheck docs-linkcheck: ## Run the link check on the docs @uv run --python 3.12 sphinx-build -b linkcheck ./docs ./docs/_build -D linkcheck_ignore='http://.*','https://.*' .PHONY: docs-linkcheck-full docs-linkcheck-full: ## Run the full link check on the docs @uv run --python 3.12 sphinx-build -b linkcheck ./docs ./docs/_build -D linkcheck_anchors=0 �����������������������������������������������������������������������������������������������������litestar-2.16.0/README.md���������������������������������������������������������������������������0000664�0000000�0000000�00000313627�15005643713�0014726�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- markdownlint-disable --> <p align="center"> <!-- github-banner-start --> <img src="https://raw.githubusercontent.com/litestar-org/branding/main/assets/Branding%20-%20SVG%20-%20Transparent/Logo%20-%20Banner%20-%20Inline%20-%20Light.svg#gh-light-mode-only" alt="Litestar Logo - Light" width="100%" height="auto" /> <img src="https://raw.githubusercontent.com/litestar-org/branding/main/assets/Branding%20-%20SVG%20-%20Transparent/Logo%20-%20Banner%20-%20Inline%20-%20Dark.svg#gh-dark-mode-only" alt="Litestar Logo - Dark" width="100%" height="auto" /> <!-- github-banner-end --> </p> <!-- markdownlint-restore --> <div align="center"> <!-- prettier-ignore-start --> | Project | | Status | |-----------|:----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | CI/CD | | [![Latest Release](https://github.com/litestar-org/litestar/actions/workflows/publish.yml/badge.svg)](https://github.com/litestar-org/litestar/actions/workflows/publish.yml) [![ci](https://github.com/litestar-org/litestar/actions/workflows/ci.yml/badge.svg)](https://github.com/litestar-org/litestar/actions/workflows/ci.yml) [![Documentation Building](https://github.com/litestar-org/litestar/actions/workflows/docs.yml/badge.svg?branch=main)](https://github.com/litestar-org/litestar/actions/workflows/docs.yml) | | Quality | | [![Coverage](https://codecov.io/github/litestar-org/litestar/graph/badge.svg?token=vKez4Pycrc)](https://codecov.io/github/litestar-org/litestar) | | Package | | [![PyPI - Version](https://img.shields.io/pypi/v/litestar?labelColor=202235&color=edb641&logo=python&logoColor=edb641)](https://badge.fury.io/py/litestar) ![PyPI - Support Python Versions](https://img.shields.io/pypi/pyversions/litestar?labelColor=202235&color=edb641&logo=python&logoColor=edb641) ![Starlite PyPI - Downloads](https://img.shields.io/pypi/dm/starlite?logo=python&label=starlite%20downloads&labelColor=202235&color=edb641&logoColor=edb641) ![Litestar PyPI - Downloads](https://img.shields.io/pypi/dm/litestar?logo=python&label=litestar%20downloads&labelColor=202235&color=edb641&logoColor=edb641) | | Community | | [![Reddit](https://img.shields.io/reddit/subreddit-subscribers/litestarapi?label=r%2FLitestar&logo=reddit&labelColor=202235&color=edb641&logoColor=edb641)](https://reddit.com/r/litestarapi) [![Discord](https://img.shields.io/discord/919193495116337154?labelColor=202235&color=edb641&label=chat%20on%20discord&logo=discord&logoColor=edb641)](https://discord.gg/litestar) [![Matrix](https://img.shields.io/badge/chat%20on%20Matrix-bridged-202235?labelColor=202235&color=edb641&logo=matrix&logoColor=edb641)](https://matrix.to/#/#litestar:matrix.org) [![Medium](https://img.shields.io/badge/Medium-202235?labelColor=202235&color=edb641&logo=medium&logoColor=edb641)](https://blog.litestar.dev) [![Twitter](https://img.shields.io/twitter/follow/LitestarAPI?labelColor=202235&color=edb641&logo=twitter&logoColor=edb641&style=flat)](https://twitter.com/LitestarAPI) [![Blog](https://img.shields.io/badge/Blog-litestar.dev-202235?logo=blogger&labelColor=202235&color=edb641&logoColor=edb641)](https://blog.litestar.dev) | | Meta | | [![Litestar Project](https://img.shields.io/badge/Litestar%20Org-%E2%AD%90%20Litestar-202235.svg?logo=python&labelColor=202235&color=edb641&logoColor=edb641)](https://github.com/litestar-org/litestar) [![types - Mypy](https://img.shields.io/badge/types-Mypy-202235.svg?logo=python&labelColor=202235&color=edb641&logoColor=edb641)](https://github.com/python/mypy) [![License - MIT](https://img.shields.io/badge/license-MIT-202235.svg?logo=python&labelColor=202235&color=edb641&logoColor=edb641)](https://spdx.org/licenses/) [![Litestar Sponsors](https://img.shields.io/badge/Sponsor-%E2%9D%A4-%23edb641.svg?&logo=github&logoColor=edb641&labelColor=202235)](https://github.com/sponsors/litestar-org) [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json&labelColor=202235)](https://github.com/astral-sh/ruff) [![code style - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/format.json&labelColor=202235)](https://github.com/psf/black) [![All Contributors](https://img.shields.io/github/all-contributors/litestar-org/litestar?labelColor=202235&color=edb641&logoColor=edb641)](#contributors-) | <!-- prettier-ignore-end --> </div> <hr> Litestar is a powerful, flexible yet opinionated ASGI framework, focused on building APIs. It offers high-performance data validation, dependency injection, first-class ORM integration, authorization primitives, a rich plugin API, middleware, and much more that's needed to get applications up and running. Check out the [documentation 📚](https://docs.litestar.dev/) for a detailed overview of its features! Additionally, the [Litestar fullstack repository](https://github.com/litestar-org/litestar-fullstack) can give you a good impression how a fully fledged Litestar application may look. <details> <summary>Table of Contents</summary> - [Installation](#installation) - [Quick Start](#quick-start) - [Core Features](#core-features) - [Example Applications](#example-applications) - [Features](#features) - [Class-based Controllers](#class-based-controllers) - [Data Parsing, Type Hints, and Msgspec](#data-parsing-type-hints-and-msgspec) - [Plugin System, ORM support, and DTOs](#plugin-system-orm-support-and-dtos) - [OpenAPI](#openapi) - [Dependency Injection](#dependency-injection) - [Middleware](#middleware) - [Route Guards](#route-guards) - [Request Life Cycle Hooks](#request-life-cycle-hooks) - [Performance](#performance) - [Contributing](#contributing) </details> ## Installation ```shell pip install litestar ``` or to include the CLI and a server (uvicorn) for running your application: ```shell pip install litestar[standard] ``` ## Quick Start ```python title="app.py" from litestar import Litestar, get @get("/") async def hello_world() -> dict[str, str]: """Keeping the tradition alive with hello world.""" return {"hello": "world"} app = Litestar(route_handlers=[hello_world]) ``` And run it with ```bash litestar run ``` ## Core Features - [Class based controllers](#class-based-controllers) - [Dependency Injection](#dependency-injection) - [Layered Middleware](#middleware) - [Plugin System](#plugin-system-orm-support-and-dtos) - [OpenAPI 3.1 schema generation](#openapi) - [Life Cycle Hooks](#request-life-cycle-hooks) - [Route Guards based Authorization](#route-guards) - Support for `dataclasses`, `TypedDict`, [`msgspec`](https://jcristharif.com/msgspec/), [pydantic version 1 and version 2 (even within the same application)](https://docs.pydantic.dev/latest/) and [(c)attrs](https://catt.rs/en/stable/) [msgspec](https://github.com/jcrist/msgspec) and [attrs](https://www.attrs.org/en/stable/) - Layered parameter declaration - Support for [RFC 9457](https://datatracker.ietf.org/doc/html/rfc9457) standardized "Problem Detail" error responses - [Automatic API documentation with](#redoc-swagger-ui-and-stoplight-elements-api-documentation): - [Scalar](https://github.com/scalar/scalar/) - [RapiDoc](https://github.com/rapi-doc/RapiDoc) - [Redoc](https://github.com/Redocly/redoc) - [Stoplight Elements](https://github.com/stoplightio/elements) - [Swagger-UI](https://swagger.io/tools/swagger-ui/) - [Trio](https://trio.readthedocs.io/en/stable/) support (built-in, via [AnyIO](https://anyio.readthedocs.io/)) - Ultra-fast validation, serialization and deserialization using [msgspec](https://github.com/jcrist/msgspec) - [SQLAlchemy integration](https://docs.advanced-alchemy.litestar.dev/latest/) ## Example Applications <details> <summary>Pre-built Example Apps</summary> - [litestar-hello-world](https://github.com/litestar-org/litestar-hello-world): A bare-minimum application setup. Great for testing and POC work. - [litestar-fullstack](https://github.com/litestar-org/litestar-fullstack): A reference application that contains most of the boilerplate required for a web application. It features a Litestar app configured with best practices, SQLAlchemy 2.0 and SAQ, a frontend integrated with Vitejs and Jinja2 templates, Docker, and more. Like all Litestar projects, this application is open to contributions, big and small. </details> ## Sponsors Litestar is an open-source project, and we enjoy the support of our sponsors to help fund the exciting work we do. A **huge** thanks to our sponsors: [//]: # "Note to maintainers: Highest sponsors first; no more than 3 per row - create new div if needed" <a href="https://github.com/scalar/scalar/?utm_source=litestar&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar.com - Document, Discover and Test APIs with Scalar."><img src="https://raw.githubusercontent.com/litestar-org/branding/main/assets/sponsors/scalar.svg" width="180" alt="Scalar.com"></a> <a href="https://telemetrysports.com/" title="Telemetry Sports - Changing the way data influences the sports experience"><img src="https://raw.githubusercontent.com/litestar-org/branding/main/assets/sponsors/telemetry-sports/unofficial-telemetry-whitebg.svg" width="150" alt="Telemetry Sports"></a> <a href="https://docs.litestar.dev/dev/#sponsors" class="external-link" target="_blank">Check out our sponsors in the docs</a> If you would like to support the work that we do please consider [becoming a sponsor][sponsor-polar] via [Polar.sh][sponsor-polar] (preferred), [GitHub][sponsor-github] or [Open Collective][sponsor-oc]. Also, exclusively with [Polar][sponsor-polar], you can engage in pledge-based sponsorships. [sponsor-github]: https://github.com/sponsors/litestar-org [sponsor-oc]: https://opencollective.com/litestar [sponsor-polar]: https://polar.sh/litestar-org ## Features ### Class-based Controllers While supporting function-based route handlers, Litestar also supports and promotes python OOP using class based controllers: <details> <summary>Example for class-based controllers</summary> ```python title="my_app/controllers/user.py" from typing import List, Optional from datetime import datetime from litestar import Controller, get, post, put, patch, delete from litestar.dto import DTOData from pydantic import UUID4 from my_app.models import User, PartialUserDTO class UserController(Controller): path = "/users" @post() async def create_user(self, data: User) -> User: ... @get() async def list_users(self) -> List[User]: ... @get(path="/{date:int}") async def list_new_users(self, date: datetime) -> List[User]: ... @patch(path="/{user_id:uuid}", dto=PartialUserDTO) async def partial_update_user( self, user_id: UUID4, data: DTOData[PartialUserDTO] ) -> User: ... @put(path="/{user_id:uuid}") async def update_user(self, user_id: UUID4, data: User) -> User: ... @get(path="/{user_name:str}") async def get_user_by_name(self, user_name: str) -> Optional[User]: ... @get(path="/{user_id:uuid}") async def get_user(self, user_id: UUID4) -> User: ... @delete(path="/{user_id:uuid}") async def delete_user(self, user_id: UUID4) -> None: ... ``` </details> ### Data Parsing, Type Hints, and Msgspec Litestar is rigorously typed, and it enforces typing. For example, if you forget to type a return value for a route handler, an exception will be raised. The reason for this is that Litestar uses typing data to generate OpenAPI specs, as well as to validate and parse data. Thus, typing is essential to the framework. Furthermore, Litestar allows extending its support using plugins. ### Plugin System, ORM support, and DTOs Litestar has a plugin system that allows the user to extend serialization/deserialization, OpenAPI generation, and other features. It ships with a builtin plugin for SQL Alchemy, which allows the user to use SQLAlchemy declarative classes "natively" i.e., as type parameters that will be serialized/deserialized and to return them as values from route handlers. Litestar also supports the programmatic creation of DTOs with a `DTOFactory` class, which also supports the use of plugins. ### OpenAPI Litestar has custom logic to generate OpenAPI 3.1.0 schema, include optional generation of examples using the [`polyfactory`](https://pypi.org/project/polyfactory/) library. #### ReDoc, Swagger-UI and Stoplight Elements API Documentation Litestar serves the documentation from the generated OpenAPI schema with: - [ReDoc](https://redoc.ly/) - [Swagger-UI](https://swagger.io/tools/swagger-ui/) - [Stoplight Elements](https://github.com/stoplightio/elements) - [RapiDoc](https://rapidocweb.com/) All these are available and enabled by default. ### Dependency Injection Litestar has a simple but powerful DI system inspired by pytest. You can define named dependencies - sync or async - at different levels of the application, and then selective use or overwrite them. <details> <summary>Example for DI</summary> ```python from litestar import Litestar, get from litestar.di import Provide async def my_dependency() -> str: ... @get("/") async def index(injected: str) -> str: return injected app = Litestar([index], dependencies={"injected": Provide(my_dependency)}) ``` </details> ### Middleware Litestar supports typical ASGI middleware and ships with middlewares to handle things such as - CORS - CSRF - Rate limiting - GZip and Brotli compression - Client- and server-side sessions ### Route Guards Litestar has an authorization mechanism called `guards`, which allows the user to define guard functions at different level of the application (app, router, controller etc.) and validate the request before hitting the route handler function. <details> <summary>Example for route guards</summary> ```python from litestar import Litestar, get from litestar.connection import ASGIConnection from litestar.handlers.base import BaseRouteHandler from litestar.exceptions import NotAuthorizedException async def is_authorized(connection: ASGIConnection, handler: BaseRouteHandler) -> None: # validate authorization # if not authorized, raise NotAuthorizedException raise NotAuthorizedException() @get("/", guards=[is_authorized]) async def index() -> None: ... app = Litestar([index]) ``` </details> ### Request Life Cycle Hooks Litestar supports request life cycle hooks, similarly to Flask - i.e. `before_request` and `after_request` ## Performance Litestar is fast. It is on par with, or significantly faster than comparable ASGI frameworks. You can see and run the benchmarks [here](https://github.com/litestar-org/api-performance-tests), or read more about it [here](https://docs.litestar.dev/latest/benchmarks) in our documentation. ## Contributing Litestar is open to contributions big and small. You can always [join our discord](https://discord.gg/litestar) server or [join our Matrix](https://matrix.to/#/#litestar:matrix.org) space to discuss contributions and project maintenance. For guidelines on how to contribute, please see [the contribution guide](CONTRIBUTING.rst). <!-- contributors-start --> ## Contributors ✨ <details> <summary>Thanks goes to these wonderful people:</summary> <a href="https://allcontributors.org/docs/en/emoji-key">Emoji Key </a> <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> <!-- prettier-ignore-start --> <!-- markdownlint-disable --> <table> <tbody> <tr> <td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/nhirschfeld/"><img src="https://avatars.githubusercontent.com/u/30733348?v=4?s=100" width="100px;" alt="Na'aman Hirschfeld"/><br /><sub><b>Na'aman Hirschfeld</b></sub></a><br /><a href="#maintenance-Goldziher" title="Maintenance">🚧</a> <a href="https://github.com/litestar-org/litestar/commits?author=Goldziher" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=Goldziher" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=Goldziher" title="Tests">⚠️</a> <a href="#ideas-Goldziher" title="Ideas, Planning, & Feedback">🤔</a> <a href="#example-Goldziher" title="Examples">💡</a> <a href="https://github.com/litestar-org/litestar/issues?q=author%3AGoldziher" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/peterschutt"><img src="https://avatars.githubusercontent.com/u/20659309?v=4?s=100" width="100px;" alt="Peter Schutt"/><br /><sub><b>Peter Schutt</b></sub></a><br /><a href="#maintenance-peterschutt" title="Maintenance">🚧</a> <a href="https://github.com/litestar-org/litestar/commits?author=peterschutt" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=peterschutt" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=peterschutt" title="Tests">⚠️</a> <a href="#ideas-peterschutt" title="Ideas, Planning, & Feedback">🤔</a> <a href="#example-peterschutt" title="Examples">💡</a> <a href="https://github.com/litestar-org/litestar/issues?q=author%3Apeterschutt" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://ashwinvin.github.io"><img src="https://avatars.githubusercontent.com/u/38067089?v=4?s=100" width="100px;" alt="Ashwin Vinod"/><br /><sub><b>Ashwin Vinod</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=ashwinvin" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=ashwinvin" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://www.damiankress.de"><img src="https://avatars.githubusercontent.com/u/28515387?v=4?s=100" width="100px;" alt="Damian"/><br /><sub><b>Damian</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=dkress59" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://remotepixel.ca"><img src="https://avatars.githubusercontent.com/u/10407788?v=4?s=100" width="100px;" alt="Vincent Sarago"/><br /><sub><b>Vincent Sarago</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=vincentsarago" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://hotfix.guru"><img src="https://avatars.githubusercontent.com/u/5310116?v=4?s=100" width="100px;" alt="Jonas Krüger Svensson"/><br /><sub><b>Jonas Krüger Svensson</b></sub></a><br /><a href="#platform-JonasKs" title="Packaging/porting to new platform">📦</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/sondrelg"><img src="https://avatars.githubusercontent.com/u/25310870?v=4?s=100" width="100px;" alt="Sondre Lillebø Gundersen"/><br /><sub><b>Sondre Lillebø Gundersen</b></sub></a><br /><a href="#platform-sondrelg" title="Packaging/porting to new platform">📦</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/vrslev"><img src="https://avatars.githubusercontent.com/u/75225148?v=4?s=100" width="100px;" alt="Lev"/><br /><sub><b>Lev</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=vrslev" title="Code">💻</a> <a href="#ideas-vrslev" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/timwedde"><img src="https://avatars.githubusercontent.com/u/20231751?v=4?s=100" width="100px;" alt="Tim Wedde"/><br /><sub><b>Tim Wedde</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=timwedde" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/tclasen"><img src="https://avatars.githubusercontent.com/u/11999013?v=4?s=100" width="100px;" alt="Tory Clasen"/><br /><sub><b>Tory Clasen</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=tclasen" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="http://t.me/Bobronium"><img src="https://avatars.githubusercontent.com/u/36469655?v=4?s=100" width="100px;" alt="Arseny Boykov"/><br /><sub><b>Arseny Boykov</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Bobronium" title="Code">💻</a> <a href="#ideas-Bobronium" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/yudjinn"><img src="https://avatars.githubusercontent.com/u/7493084?v=4?s=100" width="100px;" alt="Jacob Rodgers"/><br /><sub><b>Jacob Rodgers</b></sub></a><br /><a href="#example-yudjinn" title="Examples">💡</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/danesolberg"><img src="https://avatars.githubusercontent.com/u/25882507?v=4?s=100" width="100px;" alt="Dane Solberg"/><br /><sub><b>Dane Solberg</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=danesolberg" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/madlad33"><img src="https://avatars.githubusercontent.com/u/54079440?v=4?s=100" width="100px;" alt="madlad33"/><br /><sub><b>madlad33</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=madlad33" title="Code">💻</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="http://matthewtyleraylward.com"><img src="https://avatars.githubusercontent.com/u/19205392?v=4?s=100" width="100px;" alt="Matthew Aylward "/><br /><sub><b>Matthew Aylward </b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Butch78" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Joko013"><img src="https://avatars.githubusercontent.com/u/30841710?v=4?s=100" width="100px;" alt="Jan Klima"/><br /><sub><b>Jan Klima</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Joko013" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/i404788"><img src="https://avatars.githubusercontent.com/u/50617709?v=4?s=100" width="100px;" alt="C2D"/><br /><sub><b>C2D</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=i404788" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/to-ph"><img src="https://avatars.githubusercontent.com/u/84818322?v=4?s=100" width="100px;" alt="to-ph"/><br /><sub><b>to-ph</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=to-ph" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://imbev.gitlab.io/site"><img src="https://avatars.githubusercontent.com/u/105524473?v=4?s=100" width="100px;" alt="imbev"/><br /><sub><b>imbev</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=imbev" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://git.roboces.dev/catalin"><img src="https://avatars.githubusercontent.com/u/45485069?v=4?s=100" width="100px;" alt="cătălin"/><br /><sub><b>cătălin</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=185504a9" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Seon82"><img src="https://avatars.githubusercontent.com/u/46298009?v=4?s=100" width="100px;" alt="Seon82"/><br /><sub><b>Seon82</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Seon82" title="Documentation">📖</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/slavugan"><img src="https://avatars.githubusercontent.com/u/8457612?v=4?s=100" width="100px;" alt="Slava"/><br /><sub><b>Slava</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=slavugan" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Harry-Lees"><img src="https://avatars.githubusercontent.com/u/52263746?v=4?s=100" width="100px;" alt="Harry"/><br /><sub><b>Harry</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Harry-Lees" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=Harry-Lees" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/cofin"><img src="https://avatars.githubusercontent.com/u/204685?v=4?s=100" width="100px;" alt="Cody Fincher"/><br /><sub><b>Cody Fincher</b></sub></a><br /><a href="#maintenance-cofin" title="Maintenance">🚧</a> <a href="https://github.com/litestar-org/litestar/commits?author=cofin" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=cofin" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=cofin" title="Tests">⚠️</a> <a href="#ideas-cofin" title="Ideas, Planning, & Feedback">🤔</a> <a href="#example-cofin" title="Examples">💡</a> <a href="https://github.com/litestar-org/litestar/issues?q=author%3Acofin" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://www.patreon.com/cclauss"><img src="https://avatars.githubusercontent.com/u/3709715?v=4?s=100" width="100px;" alt="Christian Clauss"/><br /><sub><b>Christian Clauss</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=cclauss" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/josepdaniel"><img src="https://avatars.githubusercontent.com/u/36941460?v=4?s=100" width="100px;" alt="josepdaniel"/><br /><sub><b>josepdaniel</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=josepdaniel" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/devtud"><img src="https://avatars.githubusercontent.com/u/6808024?v=4?s=100" width="100px;" alt="devtud"/><br /><sub><b>devtud</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Adevtud" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/nramos0"><img src="https://avatars.githubusercontent.com/u/35410160?v=4?s=100" width="100px;" alt="Nicholas Ramos"/><br /><sub><b>Nicholas Ramos</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=nramos0" title="Code">💻</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://twitter.com/seladb"><img src="https://avatars.githubusercontent.com/u/9059541?v=4?s=100" width="100px;" alt="seladb"/><br /><sub><b>seladb</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=seladb" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=seladb" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/aedify-swi"><img src="https://avatars.githubusercontent.com/u/66629131?v=4?s=100" width="100px;" alt="Simon Wienhöfer"/><br /><sub><b>Simon Wienhöfer</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=aedify-swi" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/mobiusxs"><img src="https://avatars.githubusercontent.com/u/57055149?v=4?s=100" width="100px;" alt="MobiusXS"/><br /><sub><b>MobiusXS</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=mobiusxs" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="http://aidansimard.dev"><img src="https://avatars.githubusercontent.com/u/73361895?v=4?s=100" width="100px;" alt="Aidan Simard"/><br /><sub><b>Aidan Simard</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Aidan-Simard" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/waweber"><img src="https://avatars.githubusercontent.com/u/714224?v=4?s=100" width="100px;" alt="wweber"/><br /><sub><b>wweber</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=waweber" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="http://scolvin.com"><img src="https://avatars.githubusercontent.com/u/4039449?v=4?s=100" width="100px;" alt="Samuel Colvin"/><br /><sub><b>Samuel Colvin</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=samuelcolvin" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/toudi"><img src="https://avatars.githubusercontent.com/u/81148?v=4?s=100" width="100px;" alt="Mateusz Mikołajczyk"/><br /><sub><b>Mateusz Mikołajczyk</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=toudi" title="Code">💻</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Alex-CodeLab"><img src="https://avatars.githubusercontent.com/u/1678423?v=4?s=100" width="100px;" alt="Alex "/><br /><sub><b>Alex </b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Alex-CodeLab" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/odiseo0"><img src="https://avatars.githubusercontent.com/u/87550035?v=4?s=100" width="100px;" alt="Odiseo"/><br /><sub><b>Odiseo</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=odiseo0" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/ingjavierpinilla"><img src="https://avatars.githubusercontent.com/u/36714646?v=4?s=100" width="100px;" alt="Javier Pinilla"/><br /><sub><b>Javier Pinilla</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=ingjavierpinilla" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Chaoyingz"><img src="https://avatars.githubusercontent.com/u/32626585?v=4?s=100" width="100px;" alt="Chaoying"/><br /><sub><b>Chaoying</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Chaoyingz" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/infohash"><img src="https://avatars.githubusercontent.com/u/46137868?v=4?s=100" width="100px;" alt="infohash"/><br /><sub><b>infohash</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=infohash" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/john-ingles/"><img src="https://avatars.githubusercontent.com/u/35442886?v=4?s=100" width="100px;" alt="John Ingles"/><br /><sub><b>John Ingles</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=john-ingles" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/h0rn3t"><img src="https://avatars.githubusercontent.com/u/1213719?v=4?s=100" width="100px;" alt="Eugene"/><br /><sub><b>Eugene</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=h0rn3t" title="Tests">⚠️</a> <a href="https://github.com/litestar-org/litestar/commits?author=h0rn3t" title="Code">💻</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/jonadaly"><img src="https://avatars.githubusercontent.com/u/26462826?v=4?s=100" width="100px;" alt="Jon Daly"/><br /><sub><b>Jon Daly</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=jonadaly" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=jonadaly" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://harshallaheri.me/"><img src="https://avatars.githubusercontent.com/u/73422191?v=4?s=100" width="100px;" alt="Harshal Laheri"/><br /><sub><b>Harshal Laheri</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Harshal6927" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=Harshal6927" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/sorasful"><img src="https://avatars.githubusercontent.com/u/32820423?v=4?s=100" width="100px;" alt="Téva KRIEF"/><br /><sub><b>Téva KRIEF</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=sorasful" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/jtraub"><img src="https://avatars.githubusercontent.com/u/153191?v=4?s=100" width="100px;" alt="Konstantin Mikhailov"/><br /><sub><b>Konstantin Mikhailov</b></sub></a><br /><a href="#maintenance-jtraub" title="Maintenance">🚧</a> <a href="https://github.com/litestar-org/litestar/commits?author=jtraub" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=jtraub" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=jtraub" title="Tests">⚠️</a> <a href="#ideas-jtraub" title="Ideas, Planning, & Feedback">🤔</a> <a href="#example-jtraub" title="Examples">💡</a> <a href="https://github.com/litestar-org/litestar/issues?q=author%3Ajtraub" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="http://linkedin.com/in/mitchell-henry334/"><img src="https://avatars.githubusercontent.com/u/17354727?v=4?s=100" width="100px;" alt="Mitchell Henry"/><br /><sub><b>Mitchell Henry</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=devmitch" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/chbndrhnns"><img src="https://avatars.githubusercontent.com/u/7534547?v=4?s=100" width="100px;" alt="chbndrhnns"/><br /><sub><b>chbndrhnns</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=chbndrhnns" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/nielsvanhooy"><img src="https://avatars.githubusercontent.com/u/40770348?v=4?s=100" width="100px;" alt="nielsvanhooy"/><br /><sub><b>nielsvanhooy</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=nielsvanhooy" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/issues?q=author%3Anielsvanhooy" title="Bug reports">🐛</a> <a href="https://github.com/litestar-org/litestar/commits?author=nielsvanhooy" title="Tests">⚠️</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/provinzkraut"><img src="https://avatars.githubusercontent.com/u/25355197?v=4?s=100" width="100px;" alt="provinzkraut"/><br /><sub><b>provinzkraut</b></sub></a><br /><a href="#maintenance-provinzkraut" title="Maintenance">🚧</a> <a href="https://github.com/litestar-org/litestar/commits?author=provinzkraut" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=provinzkraut" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=provinzkraut" title="Tests">⚠️</a> <a href="#ideas-provinzkraut" title="Ideas, Planning, & Feedback">🤔</a> <a href="#example-provinzkraut" title="Examples">💡</a> <a href="https://github.com/litestar-org/litestar/issues?q=author%3Aprovinzkraut" title="Bug reports">🐛</a> <a href="#design-provinzkraut" title="Design">🎨</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/jab"><img src="https://avatars.githubusercontent.com/u/64992?v=4?s=100" width="100px;" alt="Joshua Bronson"/><br /><sub><b>Joshua Bronson</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=jab" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://linkedin.com/in/roman-reznikov"><img src="https://avatars.githubusercontent.com/u/44291988?v=4?s=100" width="100px;" alt="Roman Reznikov"/><br /><sub><b>Roman Reznikov</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=ReznikovRoman" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://mookrs.com"><img src="https://avatars.githubusercontent.com/u/985439?v=4?s=100" width="100px;" alt="mookrs"/><br /><sub><b>mookrs</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=mookrs" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://mike.depalatis.net"><img src="https://avatars.githubusercontent.com/u/2805515?v=4?s=100" width="100px;" alt="Mike DePalatis"/><br /><sub><b>Mike DePalatis</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=mivade" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/pemocarlo"><img src="https://avatars.githubusercontent.com/u/7297323?v=4?s=100" width="100px;" alt="Carlos Alberto Pérez-Molano"/><br /><sub><b>Carlos Alberto Pérez-Molano</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=pemocarlo" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://www.bestcryptocodes.com"><img src="https://avatars.githubusercontent.com/u/114229148?v=4?s=100" width="100px;" alt="ThinksFast"/><br /><sub><b>ThinksFast</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=ThinksFast" title="Tests">⚠️</a> <a href="https://github.com/litestar-org/litestar/commits?author=ThinksFast" title="Documentation">📖</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/ottermata"><img src="https://avatars.githubusercontent.com/u/9451844?v=4?s=100" width="100px;" alt="Christopher Krause"/><br /><sub><b>Christopher Krause</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=ottermata" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="http://www.kylesmith.me"><img src="https://avatars.githubusercontent.com/u/1161424?v=4?s=100" width="100px;" alt="Kyle Smith"/><br /><sub><b>Kyle Smith</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=smithk86" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=smithk86" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/issues?q=author%3Asmithk86" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/scott2b"><img src="https://avatars.githubusercontent.com/u/307713?v=4?s=100" width="100px;" alt="Scott Bradley"/><br /><sub><b>Scott Bradley</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Ascott2b" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/srikanthccv/"><img src="https://avatars.githubusercontent.com/u/22846633?v=4?s=100" width="100px;" alt="Srikanth Chekuri"/><br /><sub><b>Srikanth Chekuri</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=srikanthccv" title="Tests">⚠️</a> <a href="https://github.com/litestar-org/litestar/commits?author=srikanthccv" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://lonelyviking.com"><img src="https://avatars.githubusercontent.com/u/78952809?v=4?s=100" width="100px;" alt="Michael Bosch"/><br /><sub><b>Michael Bosch</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=LonelyVikingMichael" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/sssssss340"><img src="https://avatars.githubusercontent.com/u/8406195?v=4?s=100" width="100px;" alt="sssssss340"/><br /><sub><b>sssssss340</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Asssssss340" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/ste-pool"><img src="https://avatars.githubusercontent.com/u/17198460?v=4?s=100" width="100px;" alt="ste-pool"/><br /><sub><b>ste-pool</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=ste-pool" title="Code">💻</a> <a href="#infra-ste-pool" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Alc-Alc"><img src="https://avatars.githubusercontent.com/u/45509143?v=4?s=100" width="100px;" alt="Alc-Alc"/><br /><sub><b>Alc-Alc</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Alc-Alc" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=Alc-Alc" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=Alc-Alc" title="Tests">⚠️</a> <a href="#infra-Alc-Alc" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> <td align="center" valign="top" width="14.28%"><a href="http://asomethings.com"><img src="https://avatars.githubusercontent.com/u/16171942?v=4?s=100" width="100px;" alt="asomethings"/><br /><sub><b>asomethings</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=asomethings" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/garburator"><img src="https://avatars.githubusercontent.com/u/14207857?v=4?s=100" width="100px;" alt="Garry Bullock"/><br /><sub><b>Garry Bullock</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=garburator" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/NiclasHaderer"><img src="https://avatars.githubusercontent.com/u/109728711?v=4?s=100" width="100px;" alt="Niclas Haderer"/><br /><sub><b>Niclas Haderer</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=NiclasHaderer" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/dialvarezs"><img src="https://avatars.githubusercontent.com/u/13831919?v=4?s=100" width="100px;" alt="Diego Alvarez"/><br /><sub><b>Diego Alvarez</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=dialvarezs" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=dialvarezs" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=dialvarezs" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://www.rgare.com"><img src="https://avatars.githubusercontent.com/u/51208317?v=4?s=100" width="100px;" alt="Jason Nance"/><br /><sub><b>Jason Nance</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=rgajason" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/spikenn"><img src="https://avatars.githubusercontent.com/u/32995595?v=4?s=100" width="100px;" alt="Igor Kapadze"/><br /><sub><b>Igor Kapadze</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=spikenn" title="Documentation">📖</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://jarmos.vercel.app"><img src="https://avatars.githubusercontent.com/u/31373860?v=4?s=100" width="100px;" alt="Somraj Saha"/><br /><sub><b>Somraj Saha</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Jarmos-san" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://skulason.me"><img src="https://avatars.githubusercontent.com/u/11139514?v=4?s=100" width="100px;" alt="Magnús Ágúst Skúlason"/><br /><sub><b>Magnús Ágúst Skúlason</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=maggias" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=maggias" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://alessioparma.xyz/"><img src="https://avatars.githubusercontent.com/u/4697032?v=4?s=100" width="100px;" alt="Alessio Parma"/><br /><sub><b>Alessio Parma</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=pomma89" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Lugoues"><img src="https://avatars.githubusercontent.com/u/372610?v=4?s=100" width="100px;" alt="Peter Brunner"/><br /><sub><b>Peter Brunner</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Lugoues" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://scriptr.dev/"><img src="https://avatars.githubusercontent.com/u/45884264?v=4?s=100" width="100px;" alt="Jacob Coffee"/><br /><sub><b>Jacob Coffee</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=JacobCoffee" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=JacobCoffee" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=JacobCoffee" title="Tests">⚠️</a> <a href="#infra-JacobCoffee" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-JacobCoffee" title="Ideas, Planning, & Feedback">🤔</a> <a href="#maintenance-JacobCoffee" title="Maintenance">🚧</a> <a href="#business-JacobCoffee" title="Business development">💼</a> <a href="#design-JacobCoffee" title="Design">🎨</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Gamazic"><img src="https://avatars.githubusercontent.com/u/33692402?v=4?s=100" width="100px;" alt="Gamazic"/><br /><sub><b>Gamazic</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Gamazic" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/kareemmahlees"><img src="https://avatars.githubusercontent.com/u/89863279?v=4?s=100" width="100px;" alt="Kareem Mahlees"/><br /><sub><b>Kareem Mahlees</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=kareemmahlees" title="Code">💻</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/abdulhaq-e"><img src="https://avatars.githubusercontent.com/u/2532125?v=4?s=100" width="100px;" alt="Abdulhaq Emhemmed"/><br /><sub><b>Abdulhaq Emhemmed</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=abdulhaq-e" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=abdulhaq-e" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/jenish2014"><img src="https://avatars.githubusercontent.com/u/9599888?v=4?s=100" width="100px;" alt="Jenish"/><br /><sub><b>Jenish</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=jenish2014" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=jenish2014" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/chris-telemetry"><img src="https://avatars.githubusercontent.com/u/78052999?v=4?s=100" width="100px;" alt="chris-telemetry"/><br /><sub><b>chris-telemetry</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=chris-telemetry" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="http://wardpearce.com"><img src="https://avatars.githubusercontent.com/u/27844174?v=4?s=100" width="100px;" alt="Ward"/><br /><sub><b>Ward</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3AWardPearce" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://knowsuchagency.com"><img src="https://avatars.githubusercontent.com/u/11974795?v=4?s=100" width="100px;" alt="Stephan Fitzpatrick"/><br /><sub><b>Stephan Fitzpatrick</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Aknowsuchagency" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://codepen.io/ekeric13/"><img src="https://avatars.githubusercontent.com/u/6489651?v=4?s=100" width="100px;" alt="Eric Kennedy"/><br /><sub><b>Eric Kennedy</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=ekeric13" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/wassafshahzad"><img src="https://avatars.githubusercontent.com/u/25094157?v=4?s=100" width="100px;" alt="wassaf shahzad"/><br /><sub><b>wassaf shahzad</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=wassafshahzad" title="Code">💻</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="http://nilsso.github.io"><img src="https://avatars.githubusercontent.com/u/567181?v=4?s=100" width="100px;" alt="Nils Olsson"/><br /><sub><b>Nils Olsson</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=nilsso" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/issues?q=author%3Anilsso" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="http://rileychase.net"><img src="https://avatars.githubusercontent.com/u/1491530?v=4?s=100" width="100px;" alt="Riley Chase"/><br /><sub><b>Riley Chase</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Nadock" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://gh.arielle.codes"><img src="https://avatars.githubusercontent.com/u/71233171?v=4?s=100" width="100px;" alt="arl"/><br /><sub><b>arl</b></sub></a><br /><a href="#maintenance-onerandomusername" title="Maintenance">🚧</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Galdanwing"><img src="https://avatars.githubusercontent.com/u/29492757?v=4?s=100" width="100px;" alt="Antoine van der Horst"/><br /><sub><b>Antoine van der Horst</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Galdanwing" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://nick.groenen.me"><img src="https://avatars.githubusercontent.com/u/145285?v=4?s=100" width="100px;" alt="Nick Groenen"/><br /><sub><b>Nick Groenen</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=zoni" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/giorgiovilardo"><img src="https://avatars.githubusercontent.com/u/56472600?v=4?s=100" width="100px;" alt="Giorgio Vilardo"/><br /><sub><b>Giorgio Vilardo</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=giorgiovilardo" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/bollwyvl"><img src="https://avatars.githubusercontent.com/u/45380?v=4?s=100" width="100px;" alt="Nicholas Bollweg"/><br /><sub><b>Nicholas Bollweg</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=bollwyvl" title="Code">💻</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/tompin82"><img src="https://avatars.githubusercontent.com/u/47041409?v=4?s=100" width="100px;" alt="Tomas Jonsson"/><br /><sub><b>Tomas Jonsson</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=tompin82" title="Tests">⚠️</a> <a href="https://github.com/litestar-org/litestar/commits?author=tompin82" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/khiem-doan/"><img src="https://avatars.githubusercontent.com/u/15646249?v=4?s=100" width="100px;" alt="Khiem Doan"/><br /><sub><b>Khiem Doan</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=khiemdoan" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/kedod"><img src="https://avatars.githubusercontent.com/u/35638715?v=4?s=100" width="100px;" alt="kedod"/><br /><sub><b>kedod</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=kedod" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=kedod" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=kedod" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/sonpro1296"><img src="https://avatars.githubusercontent.com/u/17319142?v=4?s=100" width="100px;" alt="sonpro1296"/><br /><sub><b>sonpro1296</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=sonpro1296" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=sonpro1296" title="Tests">⚠️</a> <a href="#infra-sonpro1296" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/litestar-org/litestar/commits?author=sonpro1296" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://patrickarmengol.com"><img src="https://avatars.githubusercontent.com/u/42473149?v=4?s=100" width="100px;" alt="Patrick Armengol"/><br /><sub><b>Patrick Armengol</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=patrickarmengol" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://sanderwegter.nl"><img src="https://avatars.githubusercontent.com/u/7465799?v=4?s=100" width="100px;" alt="Sander"/><br /><sub><b>Sander</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=SanderWegter" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/erhuabushuo"><img src="https://avatars.githubusercontent.com/u/1642364?v=4?s=100" width="100px;" alt="疯人院主任"/><br /><sub><b>疯人院主任</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=erhuabushuo" title="Documentation">📖</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/aviral-nayya"><img src="https://avatars.githubusercontent.com/u/121891493?v=4?s=100" width="100px;" alt="aviral-nayya"/><br /><sub><b>aviral-nayya</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=aviral-nayya" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/whiskeyriver"><img src="https://avatars.githubusercontent.com/u/162092?v=4?s=100" width="100px;" alt="whiskeyriver"/><br /><sub><b>whiskeyriver</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=whiskeyriver" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://hexcode.tech"><img src="https://avatars.githubusercontent.com/u/419606?v=4?s=100" width="100px;" alt="Phyo Arkar Lwin"/><br /><sub><b>Phyo Arkar Lwin</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=v3ss0n" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/MatthewNewland"><img src="https://avatars.githubusercontent.com/u/9618670?v=4?s=100" width="100px;" alt="MatthewNewland"/><br /><sub><b>MatthewNewland</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3AMatthewNewland" title="Bug reports">🐛</a> <a href="https://github.com/litestar-org/litestar/commits?author=MatthewNewland" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=MatthewNewland" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/vtarchon"><img src="https://avatars.githubusercontent.com/u/1598170?v=4?s=100" width="100px;" alt="Tom Kuo"/><br /><sub><b>Tom Kuo</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Avtarchon" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/LeckerenSirupwaffeln"><img src="https://avatars.githubusercontent.com/u/83568015?v=4?s=100" width="100px;" alt="LeckerenSirupwaffeln"/><br /><sub><b>LeckerenSirupwaffeln</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3ALeckerenSirupwaffeln" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/eldano1995"><img src="https://avatars.githubusercontent.com/u/24553679?v=4?s=100" width="100px;" alt="Daniel González Fernández"/><br /><sub><b>Daniel González Fernández</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=eldano1995" title="Documentation">📖</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/01EK98"><img src="https://avatars.githubusercontent.com/u/101988390?v=4?s=100" width="100px;" alt="01EK98"/><br /><sub><b>01EK98</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=01EK98" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/sarbor"><img src="https://avatars.githubusercontent.com/u/15257226?v=4?s=100" width="100px;" alt="Sarbo Roy"/><br /><sub><b>Sarbo Roy</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=sarbor" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/rseeley"><img src="https://avatars.githubusercontent.com/u/5397221?v=4?s=100" width="100px;" alt="Ryan Seeley"/><br /><sub><b>Ryan Seeley</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=rseeley" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/ctrl-Felix"><img src="https://avatars.githubusercontent.com/u/62290842?v=4?s=100" width="100px;" alt="Felix"/><br /><sub><b>Felix</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=ctrl-Felix" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/issues?q=author%3Actrl-Felix" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/gsakkis"><img src="https://avatars.githubusercontent.com/u/291289?v=4?s=100" width="100px;" alt="George Sakkis"/><br /><sub><b>George Sakkis</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=gsakkis" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/floxay"><img src="https://avatars.githubusercontent.com/u/57007485?v=4?s=100" width="100px;" alt="Huba Tuba"/><br /><sub><b>Huba Tuba</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=floxay" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=floxay" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=floxay" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="http://fermigier.com/"><img src="https://avatars.githubusercontent.com/u/271079?v=4?s=100" width="100px;" alt="Stefane Fermigier"/><br /><sub><b>Stefane Fermigier</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=sfermigier" title="Documentation">📖</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/r4gesingh47"><img src="https://avatars.githubusercontent.com/u/71139938?v=4?s=100" width="100px;" alt="r4ge"/><br /><sub><b>r4ge</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=r4gesingh47" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=r4gesingh47" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/jaykv"><img src="https://avatars.githubusercontent.com/u/18240054?v=4?s=100" width="100px;" alt="Jay"/><br /><sub><b>Jay</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=jaykv" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/sinisaos"><img src="https://avatars.githubusercontent.com/u/30960668?v=4?s=100" width="100px;" alt="sinisaos"/><br /><sub><b>sinisaos</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=sinisaos" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Tsdevendra1"><img src="https://avatars.githubusercontent.com/u/38055748?v=4?s=100" width="100px;" alt="Tharuka Devendra"/><br /><sub><b>Tharuka Devendra</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Tsdevendra1" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/euri10"><img src="https://avatars.githubusercontent.com/u/1104190?v=4?s=100" width="100px;" alt="euri10"/><br /><sub><b>euri10</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=euri10" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=euri10" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/issues?q=author%3Aeuri10" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/su-shubham"><img src="https://avatars.githubusercontent.com/u/75021117?v=4?s=100" width="100px;" alt="Shubham"/><br /><sub><b>Shubham</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=su-shubham" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/erik-hasse"><img src="https://avatars.githubusercontent.com/u/37126755?v=4?s=100" width="100px;" alt="Erik Hasse"/><br /><sub><b>Erik Hasse</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Aerik-hasse" title="Bug reports">🐛</a> <a href="https://github.com/litestar-org/litestar/commits?author=erik-hasse" title="Code">💻</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://sobolevn.me"><img src="https://avatars.githubusercontent.com/u/4660275?v=4?s=100" width="100px;" alt="Nikita Sobolev"/><br /><sub><b>Nikita Sobolev</b></sub></a><br /><a href="#infra-sobolevn" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/litestar-org/litestar/commits?author=sobolevn" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/lazyc97"><img src="https://avatars.githubusercontent.com/u/8538104?v=4?s=100" width="100px;" alt="Nguyễn Hoàng Đức"/><br /><sub><b>Nguyễn Hoàng Đức</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Alazyc97" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/RavanaBhrama"><img src="https://avatars.githubusercontent.com/u/131459969?v=4?s=100" width="100px;" alt="RavanaBhrama"/><br /><sub><b>RavanaBhrama</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=RavanaBhrama" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/mj0nez"><img src="https://avatars.githubusercontent.com/u/20128340?v=4?s=100" width="100px;" alt="Marcel Johannesmann"/><br /><sub><b>Marcel Johannesmann</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=mj0nez" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://zanfar.com/"><img src="https://avatars.githubusercontent.com/u/10294685?v=4?s=100" width="100px;" alt="Matthew"/><br /><sub><b>Matthew</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=therealzanfar" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Mattwmaster58"><img src="https://avatars.githubusercontent.com/u/26337069?v=4?s=100" width="100px;" alt="Mattwmaster58"/><br /><sub><b>Mattwmaster58</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3AMattwmaster58" title="Bug reports">🐛</a> <a href="https://github.com/litestar-org/litestar/commits?author=Mattwmaster58" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=Mattwmaster58" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://es.linkedin.com/in/manusp"><img src="https://avatars.githubusercontent.com/u/5411704?v=4?s=100" width="100px;" alt="Manuel Sanchez Pinar"/><br /><sub><b>Manuel Sanchez Pinar</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=aorith" title="Documentation">📖</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/juan-riveros"><img src="https://avatars.githubusercontent.com/u/1297567?v=4?s=100" width="100px;" alt="Juan Riveros"/><br /><sub><b>Juan Riveros</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=juan-riveros" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/davidbrochart"><img src="https://avatars.githubusercontent.com/u/4711805?v=4?s=100" width="100px;" alt="David Brochart"/><br /><sub><b>David Brochart</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=davidbrochart" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/sean-donoghue"><img src="https://avatars.githubusercontent.com/u/64597271?v=4?s=100" width="100px;" alt="Sean Donoghue"/><br /><sub><b>Sean Donoghue</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=sean-donoghue" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://sykloid.org/"><img src="https://avatars.githubusercontent.com/u/22753?v=4?s=100" width="100px;" alt="P.C. Shyamshankar"/><br /><sub><b>P.C. Shyamshankar</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Asykloid" title="Bug reports">🐛</a> <a href="https://github.com/litestar-org/litestar/commits?author=sykloid" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=sykloid" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/wevonosky"><img src="https://avatars.githubusercontent.com/u/19598171?v=4?s=100" width="100px;" alt="William Evonosky"/><br /><sub><b>William Evonosky</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=wevonosky" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/geeshta"><img src="https://avatars.githubusercontent.com/u/61031243?v=4?s=100" width="100px;" alt="geeshta"/><br /><sub><b>geeshta</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=geeshta" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=geeshta" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/issues?q=author%3Ageeshta" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://fosstodon.org/@robertrosca"><img src="https://avatars.githubusercontent.com/u/32569096?v=4?s=100" width="100px;" alt="Robert Rosca"/><br /><sub><b>Robert Rosca</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=RobertRosca" title="Documentation">📖</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/syshenyu"><img src="https://avatars.githubusercontent.com/u/92897003?v=4?s=100" width="100px;" alt="DICE_Lab"/><br /><sub><b>DICE_Lab</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=syshenyu" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/lsanpablo"><img src="https://avatars.githubusercontent.com/u/7145688?v=4?s=100" width="100px;" alt="Luis San Pablo"/><br /><sub><b>Luis San Pablo</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=lsanpablo" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=lsanpablo" title="Tests">⚠️</a> <a href="https://github.com/litestar-org/litestar/commits?author=lsanpablo" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Lancetnik"><img src="https://avatars.githubusercontent.com/u/44573917?v=4?s=100" width="100px;" alt="Pastukhov Nikita"/><br /><sub><b>Pastukhov Nikita</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Lancetnik" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://jamesoclaire.com"><img src="https://avatars.githubusercontent.com/u/7601451?v=4?s=100" width="100px;" alt="James O'Claire"/><br /><sub><b>James O'Claire</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=ddxv" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/pbaletkeman"><img src="https://avatars.githubusercontent.com/u/22402240?v=4?s=100" width="100px;" alt="Pete"/><br /><sub><b>Pete</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=pbaletkeman" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://www.hera.cc"><img src="https://avatars.githubusercontent.com/u/534840?v=4?s=100" width="100px;" alt="Alexandre Richonnier"/><br /><sub><b>Alexandre Richonnier</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=heralight" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=heralight" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/betaboon"><img src="https://avatars.githubusercontent.com/u/7346933?v=4?s=100" width="100px;" alt="betaboon"/><br /><sub><b>betaboon</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=betaboon" title="Code">💻</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/brakhane"><img src="https://avatars.githubusercontent.com/u/541637?v=4?s=100" width="100px;" alt="Dennis Brakhane"/><br /><sub><b>Dennis Brakhane</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=brakhane" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/issues?q=author%3Abrakhane" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://mind.wiki"><img src="https://avatars.githubusercontent.com/u/7423639?v=4?s=100" width="100px;" alt="Pragy Agarwal"/><br /><sub><b>Pragy Agarwal</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=AgarwalPragy" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/dybi"><img src="https://avatars.githubusercontent.com/u/36961162?v=4?s=100" width="100px;" alt="Piotr Dybowski"/><br /><sub><b>Piotr Dybowski</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=dybi" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/myslak71"><img src="https://avatars.githubusercontent.com/u/43068450?v=4?s=100" width="100px;" alt="Konrad Szczurek"/><br /><sub><b>Konrad Szczurek</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=myslak71" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=myslak71" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/orgarten"><img src="https://avatars.githubusercontent.com/u/10799869?v=4?s=100" width="100px;" alt="Orell Garten"/><br /><sub><b>Orell Garten</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=orgarten" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=orgarten" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=orgarten" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Kumzy"><img src="https://avatars.githubusercontent.com/u/5995441?v=4?s=100" width="100px;" alt="Julien"/><br /><sub><b>Julien</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Kumzy" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/leejayhsu"><img src="https://avatars.githubusercontent.com/u/37034741?v=4?s=100" width="100px;" alt="Leejay Hsu"/><br /><sub><b>Leejay Hsu</b></sub></a><br /><a href="#maintenance-leejayhsu" title="Maintenance">🚧</a> <a href="#infra-leejayhsu" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/litestar-org/litestar/commits?author=leejayhsu" title="Documentation">📖</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://x14.nl"><img src="https://avatars.githubusercontent.com/u/659504?v=4?s=100" width="100px;" alt="Michiel W. Beijen"/><br /><sub><b>Michiel W. Beijen</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=mbeijen" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/baoliay2008"><img src="https://avatars.githubusercontent.com/u/13620348?v=4?s=100" width="100px;" alt="L. Bao"/><br /><sub><b>L. Bao</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=baoliay2008" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://jarredglaser.com"><img src="https://avatars.githubusercontent.com/u/32422167?v=4?s=100" width="100px;" alt="Jarred Glaser"/><br /><sub><b>Jarred Glaser</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=jdglaser" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/hunterjsb"><img src="https://avatars.githubusercontent.com/u/69213737?v=4?s=100" width="100px;" alt="Hunter Boyd"/><br /><sub><b>Hunter Boyd</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=hunterjsb" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/cesarmg1980"><img src="https://avatars.githubusercontent.com/u/38872121?v=4?s=100" width="100px;" alt="Cesar Giulietti"/><br /><sub><b>Cesar Giulietti</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=cesarmg1980" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://gitlab.com/marcuslimdw/"><img src="https://avatars.githubusercontent.com/u/42759889?v=4?s=100" width="100px;" alt="Marcus Lim"/><br /><sub><b>Marcus Lim</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=marcuslimdw" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/hzhou0"><img src="https://avatars.githubusercontent.com/u/43188301?v=4?s=100" width="100px;" alt="Henry Zhou"/><br /><sub><b>Henry Zhou</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Ahzhou0" title="Bug reports">🐛</a> <a href="https://github.com/litestar-org/litestar/commits?author=hzhou0" title="Code">💻</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/WilliamStam"><img src="https://avatars.githubusercontent.com/u/182800?v=4?s=100" width="100px;" alt="William Stam"/><br /><sub><b>William Stam</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=WilliamStam" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/andrewdoh"><img src="https://avatars.githubusercontent.com/u/7662358?v=4?s=100" width="100px;" alt="andrew do"/><br /><sub><b>andrew do</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=andrewdoh" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=andrewdoh" title="Tests">⚠️</a> <a href="https://github.com/litestar-org/litestar/commits?author=andrewdoh" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/cbscsm"><img src="https://avatars.githubusercontent.com/u/31615733?v=4?s=100" width="100px;" alt="Boseong Choi"/><br /><sub><b>Boseong Choi</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=cbscsm" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=cbscsm" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/wer153"><img src="https://avatars.githubusercontent.com/u/23370765?v=4?s=100" width="100px;" alt="Kim Minki"/><br /><sub><b>Kim Minki</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=wer153" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=wer153" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://velog.io/@azzurri21"><img src="https://avatars.githubusercontent.com/u/86508420?v=4?s=100" width="100px;" alt="Jeongseop Lim"/><br /><sub><b>Jeongseop Lim</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=jseop-lim" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/FergusMok"><img src="https://avatars.githubusercontent.com/u/10182564?v=4?s=100" width="100px;" alt="FergusMok"/><br /><sub><b>FergusMok</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=FergusMok" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=FergusMok" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=FergusMok" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/manusinghal19"><img src="https://avatars.githubusercontent.com/u/8455587?v=4?s=100" width="100px;" alt="Manu Singhal"/><br /><sub><b>Manu Singhal</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=manusinghal19" title="Documentation">📖</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://cv.ycwu.space"><img src="https://avatars.githubusercontent.com/u/67060418?v=4?s=100" width="100px;" alt="Jerry Wu"/><br /><sub><b>Jerry Wu</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=jrycw" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/horo-fox"><img src="https://avatars.githubusercontent.com/u/143025439?v=4?s=100" width="100px;" alt="horo"/><br /><sub><b>horo</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Ahoro-fox" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/rosstitmarsh"><img src="https://avatars.githubusercontent.com/u/23349806?v=4?s=100" width="100px;" alt="Ross Titmarsh"/><br /><sub><b>Ross Titmarsh</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=rosstitmarsh" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/korneevm"><img src="https://avatars.githubusercontent.com/u/743250?v=4?s=100" width="100px;" alt="Mike Korneev"/><br /><sub><b>Mike Korneev</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=korneevm" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/patrickneise"><img src="https://avatars.githubusercontent.com/u/6312074?v=4?s=100" width="100px;" alt="Patrick Neise"/><br /><sub><b>Patrick Neise</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=patrickneise" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/JeanArhancet"><img src="https://avatars.githubusercontent.com/u/10811879?v=4?s=100" width="100px;" alt="Jean Arhancet"/><br /><sub><b>Jean Arhancet</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3AJeanArhancet" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="http://dnquark.com"><img src="https://avatars.githubusercontent.com/u/338250?v=4?s=100" width="100px;" alt="Leo Alekseyev"/><br /><sub><b>Leo Alekseyev</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=betaprior" title="Code">💻</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/aranvir"><img src="https://avatars.githubusercontent.com/u/75439739?v=4?s=100" width="100px;" alt="aranvir"/><br /><sub><b>aranvir</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=aranvir" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=aranvir" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=aranvir" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/bunny-therapist"><img src="https://avatars.githubusercontent.com/u/87039365?v=4?s=100" width="100px;" alt="bunny-therapist"/><br /><sub><b>bunny-therapist</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=bunny-therapist" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="http://www.benluo.cc"><img src="https://avatars.githubusercontent.com/u/70398?v=4?s=100" width="100px;" alt="Ben Luo"/><br /><sub><b>Ben Luo</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=benluo" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/hugovk"><img src="https://avatars.githubusercontent.com/u/1324225?v=4?s=100" width="100px;" alt="Hugo van Kemenade"/><br /><sub><b>Hugo van Kemenade</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=hugovk" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://error418.github.io"><img src="https://avatars.githubusercontent.com/u/7716544?v=4?s=100" width="100px;" alt="Michael Gerbig"/><br /><sub><b>Michael Gerbig</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=error418" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/crisog"><img src="https://avatars.githubusercontent.com/u/40803711?v=4?s=100" width="100px;" alt="CrisOG"/><br /><sub><b>CrisOG</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Acrisog" title="Bug reports">🐛</a> <a href="https://github.com/litestar-org/litestar/commits?author=crisog" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=crisog" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/haryle"><img src="https://avatars.githubusercontent.com/u/64817481?v=4?s=100" width="100px;" alt="harryle"/><br /><sub><b>harryle</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=haryle" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=haryle" title="Tests">⚠️</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="http://www.b-list.org/"><img src="https://avatars.githubusercontent.com/u/12384?v=4?s=100" width="100px;" alt="James Bennett"/><br /><sub><b>James Bennett</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Aubernostrum" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/sherbang"><img src="https://avatars.githubusercontent.com/u/275015?v=4?s=100" width="100px;" alt="sherbang"/><br /><sub><b>sherbang</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=sherbang" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/carlsmedstad"><img src="https://avatars.githubusercontent.com/u/6952324?v=4?s=100" width="100px;" alt="Carl Smedstad"/><br /><sub><b>Carl Smedstad</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=carlsmedstad" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/maintain0404"><img src="https://avatars.githubusercontent.com/u/50428534?v=4?s=100" width="100px;" alt="Taein Min"/><br /><sub><b>Taein Min</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=maintain0404" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/wallseat"><img src="https://avatars.githubusercontent.com/u/26143672?v=4?s=100" width="100px;" alt="Stanislav Lyu."/><br /><sub><b>Stanislav Lyu.</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Awallseat" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/tibor-reiss"><img src="https://avatars.githubusercontent.com/u/75096465?v=4?s=100" width="100px;" alt="Tibor Reiss"/><br /><sub><b>Tibor Reiss</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=tibor-reiss" title="Tests">⚠️</a> <a href="https://github.com/litestar-org/litestar/commits?author=tibor-reiss" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=tibor-reiss" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://pogrom.dev"><img src="https://avatars.githubusercontent.com/u/11032969?v=4?s=100" width="100px;" alt="Alex"/><br /><sub><b>Alex</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3A0xE111" title="Bug reports">🐛</a> <a href="https://github.com/litestar-org/litestar/commits?author=0xE111" title="Code">💻</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="http://0110.be"><img src="https://avatars.githubusercontent.com/u/60453?v=4?s=100" width="100px;" alt="Joren Six"/><br /><sub><b>Joren Six</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=JorenSix" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/jderrien"><img src="https://avatars.githubusercontent.com/u/145396?v=4?s=100" width="100px;" alt="jderrien"/><br /><sub><b>jderrien</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=jderrien" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://possiblepanda.me"><img src="https://avatars.githubusercontent.com/u/85448494?v=4?s=100" width="100px;" alt="PossiblePanda"/><br /><sub><b>PossiblePanda</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=PossiblePanda" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/evstratbg"><img src="https://avatars.githubusercontent.com/u/10176401?v=4?s=100" width="100px;" alt="evstrat"/><br /><sub><b>evstrat</b></sub></a><br /><a href="#infra-evstratbg" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> <td align="center" valign="top" width="14.28%"><a href="https://speakerdeck.com/eltociear"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Eltociear Ashimine"/><br /><sub><b>Ikko Eltociear Ashimine</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=eltociear" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/taihim"><img src="https://avatars.githubusercontent.com/u/13764071?v=4?s=100" width="100px;" alt="Taimur Ibrahim"/><br /><sub><b>Taimur Ibrahim</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=taihim" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/l-armstrong"><img src="https://avatars.githubusercontent.com/u/43922258?v=4?s=100" width="100px;" alt="l-armstrong"/><br /><sub><b>l-armstrong</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=l-armstrong" title="Documentation">📖</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Anu-cool-007"><img src="https://avatars.githubusercontent.com/u/16525919?v=4?s=100" width="100px;" alt="Anuranjan Srivastava"/><br /><sub><b>Anuranjan Srivastava</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Anu-cool-007" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Zimzozaur"><img src="https://avatars.githubusercontent.com/u/106471045?v=4?s=100" width="100px;" alt="Simon Joseph"/><br /><sub><b>Simon Joseph</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Zimzozaur" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/abelkm99"><img src="https://avatars.githubusercontent.com/u/41730180?v=4?s=100" width="100px;" alt="Abel Kidanemariam"/><br /><sub><b>Abel Kidanemariam</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=abelkm99" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=abelkm99" title="Tests">⚠️</a> <a href="https://github.com/litestar-org/litestar/commits?author=abelkm99" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://blog.trim21.me/"><img src="https://avatars.githubusercontent.com/u/13553903?v=4?s=100" width="100px;" alt="Trim21"/><br /><sub><b>Trim21</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=trim21" title="Code">💻</a> <a href="https://github.com/litestar-org/litestar/commits?author=trim21" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="http://aarcex3.github.io"><img src="https://avatars.githubusercontent.com/u/59893355?v=4?s=100" width="100px;" alt="Agustin Arce"/><br /><sub><b>Agustin Arce</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=aarcex3" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/FarhanAliRaza"><img src="https://avatars.githubusercontent.com/u/62690310?v=4?s=100" width="100px;" alt="Farhan Ali Raza"/><br /><sub><b>Farhan Ali Raza</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=FarhanAliRaza" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/pogopaule"><img src="https://avatars.githubusercontent.com/u/576949?v=4?s=100" width="100px;" alt="Fabian"/><br /><sub><b>Fabian</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=pogopaule" title="Code">💻</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/mohammedbabelly20"><img src="https://avatars.githubusercontent.com/u/104768048?v=4?s=100" width="100px;" alt="Mohammed Babelly"/><br /><sub><b>Mohammed Babelly</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=mohammedbabelly20" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://keybase.io/charlesdyfisnet"><img src="https://avatars.githubusercontent.com/u/22370?v=4?s=100" width="100px;" alt="Charles Duffy"/><br /><sub><b>Charles Duffy</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=charles-dyfis-net" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/RenameMe1"><img src="https://avatars.githubusercontent.com/u/165988121?v=4?s=100" width="100px;" alt="Evgeny Demchenko"/><br /><sub><b>Evgeny Demchenko</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=RenameMe1" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=RenameMe1" title="Tests">⚠️</a></td> <td align="center" valign="top" width="14.28%"><a href="https://olzhasar.com"><img src="https://avatars.githubusercontent.com/u/12471703?v=4?s=100" width="100px;" alt="Olzhas Arystanov"/><br /><sub><b>Olzhas Arystanov</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/issues?q=author%3Aolzhasar" title="Bug reports">🐛</a> <a href="https://github.com/litestar-org/litestar/commits?author=olzhasar" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/vikigenius"><img src="https://avatars.githubusercontent.com/u/12724810?v=4?s=100" width="100px;" alt="Vikash"/><br /><sub><b>Vikash</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=vikigenius" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/ftsartek"><img src="https://avatars.githubusercontent.com/u/20253317?v=4?s=100" width="100px;" alt="Jordan Russell"/><br /><sub><b>Jordan Russell</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=ftsartek" title="Documentation">📖</a> <a href="https://github.com/litestar-org/litestar/commits?author=ftsartek" title="Tests">⚠️</a> <a href="https://github.com/litestar-org/litestar/commits?author=ftsartek" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://stevenloria.com"><img src="https://avatars.githubusercontent.com/u/2379650?v=4?s=100" width="100px;" alt="Steven Loria"/><br /><sub><b>Steven Loria</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=sloria" title="Documentation">📖</a></td> </tr> <tr> <td align="center" valign="top" width="14.28%"><a href="https://github.com/oek1ng"><img src="https://avatars.githubusercontent.com/u/193062679?v=4?s=100" width="100px;" alt="oek1ng"/><br /><sub><b>oek1ng</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=oek1ng" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Ada-lave"><img src="https://avatars.githubusercontent.com/u/113159483?v=4?s=100" width="100px;" alt="Vladislav"/><br /><sub><b>Vladislav</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Ada-lave" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://gaitenis.id.lv"><img src="https://avatars.githubusercontent.com/u/9976861?v=4?s=100" width="100px;" alt="Edgars"/><br /><sub><b>Edgars</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=eandersons" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://jannchie.com"><img src="https://avatars.githubusercontent.com/u/29743310?v=4?s=100" width="100px;" alt="Jianqi Pan"/><br /><sub><b>Jianqi Pan</b></sub></a><br /><a href="https://github.com/litestar-org/litestar/commits?author=Jannchie" title="Code">💻</a></td> </tr> </tbody> </table> <!-- markdownlint-restore --> <!-- prettier-ignore-end --> <!-- ALL-CONTRIBUTORS-LIST:END --> This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! </details> <!-- contributors-end --> ���������������������������������������������������������������������������������������������������������litestar-2.16.0/codecov.yml�������������������������������������������������������������������������0000664�0000000�0000000�00000000255�15005643713�0015602�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������coverage: status: project: default: target: auto threshold: 0.1% patch: default: target: auto comment: require_changes: true ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/�������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0014363�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/PYPI_README.md�����������������������������������������������������������������0000664�0000000�0000000�00000046732�15005643713�0016517�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- markdownlint-disable --> <p align="center"> <img src="https://raw.githubusercontent.com/litestar-org/branding/473f54621e55cde9acbb6fcab7fc03036173eb3d/assets/Branding%20-%20PNG%20-%20Transparent/Logo%20-%20Banner%20-%20Inline%20-%20Light.png" alt="Litestar Logo - Light" width="100%" height="auto" /> </p> <!-- markdownlint-restore --> <div align="center"> <!-- prettier-ignore-start --> | Project | | Status | |-----------|:----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | CI/CD | | [![Latest Release](https://github.com/litestar-org/litestar/actions/workflows/publish.yml/badge.svg)](https://github.com/litestar-org/litestar/actions/workflows/publish.yml) [![ci](https://github.com/litestar-org/litestar/actions/workflows/ci.yml/badge.svg)](https://github.com/litestar-org/litestar/actions/workflows/ci.yml) [![Documentation Building](https://github.com/litestar-org/litestar/actions/workflows/docs.yml/badge.svg?branch=main)](https://github.com/litestar-org/litestar/actions/workflows/docs.yml) | | Quality | | [![Coverage](https://codecov.io/github/litestar-org/litestar/graph/badge.svg?token=vKez4Pycrc)](https://codecov.io/github/litestar-org/litestar) | | Package | | [![PyPI - Version](https://img.shields.io/pypi/v/litestar?labelColor=202235&color=edb641&logo=python&logoColor=edb641)](https://badge.fury.io/py/litestar) ![PyPI - Support Python Versions](https://img.shields.io/pypi/pyversions/litestar?labelColor=202235&color=edb641&logo=python&logoColor=edb641) ![Starlite PyPI - Downloads](https://img.shields.io/pypi/dm/starlite?logo=python&label=starlite%20downloads&labelColor=202235&color=edb641&logoColor=edb641) ![Litestar PyPI - Downloads](https://img.shields.io/pypi/dm/litestar?logo=python&label=litestar%20downloads&labelColor=202235&color=edb641&logoColor=edb641) | | Community | | [![Reddit](https://img.shields.io/reddit/subreddit-subscribers/litestarapi?label=r%2FLitestar&logo=reddit&labelColor=202235&color=edb641&logoColor=edb641)](https://reddit.com/r/litestarapi) [![Discord](https://img.shields.io/discord/919193495116337154?labelColor=202235&color=edb641&label=chat%20on%20discord&logo=discord&logoColor=edb641)](https://discord.gg/litestar) [![Matrix](https://img.shields.io/badge/chat%20on%20Matrix-bridged-202235?labelColor=202235&color=edb641&logo=matrix&logoColor=edb641)](https://matrix.to/#/#litestar:matrix.org) [![Medium](https://img.shields.io/badge/Medium-202235?labelColor=202235&color=edb641&logo=medium&logoColor=edb641)](https://blog.litestar.dev) [![Twitter](https://img.shields.io/twitter/follow/LitestarAPI?labelColor=202235&color=edb641&logo=twitter&logoColor=edb641&style=flat)](https://twitter.com/LitestarAPI) [![Blog](https://img.shields.io/badge/Blog-litestar.dev-202235?logo=blogger&labelColor=202235&color=edb641&logoColor=edb641)](https://blog.litestar.dev) | | Meta | | [![Litestar Project](https://img.shields.io/badge/Litestar%20Org-%E2%AD%90%20Litestar-202235.svg?logo=python&labelColor=202235&color=edb641&logoColor=edb641)](https://github.com/litestar-org/litestar) [![types - Mypy](https://img.shields.io/badge/types-Mypy-202235.svg?logo=python&labelColor=202235&color=edb641&logoColor=edb641)](https://github.com/python/mypy) [![License - MIT](https://img.shields.io/badge/license-MIT-202235.svg?logo=python&labelColor=202235&color=edb641&logoColor=edb641)](https://spdx.org/licenses/) [![Litestar Sponsors](https://img.shields.io/badge/Sponsor-%E2%9D%A4-%23edb641.svg?&logo=github&logoColor=edb641&labelColor=202235)](https://github.com/sponsors/litestar-org) [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json&labelColor=202235)](https://github.com/astral-sh/ruff) [![code style - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/format.json&labelColor=202235)](https://github.com/psf/black) [![All Contributors](https://img.shields.io/github/all-contributors/litestar-org/litestar?labelColor=202235&color=edb641&logoColor=edb641)](#contributors-) | <!-- prettier-ignore-end --> </div> <hr> Litestar is a powerful, flexible yet opinionated ASGI framework, focused on building APIs. It offers high-performance data validation, dependency injection, first-class ORM integration, authorization primitives, a rich plugin API, middleware, and much more that's needed to get applications up and running. Check out the [documentation 📚](https://docs.litestar.dev/) for a detailed overview of its features! Additionally, the [Litestar fullstack repository](https://github.com/litestar-org/litestar-fullstack) can give you a good impression how a fully fledged Litestar application may look. <details> <summary>Table of Contents</summary> - [Installation](#installation) - [Quick Start](#quick-start) - [Core Features](#core-features) - [Example Applications](#example-applications) - [Features](#features) - [Class-based Controllers](#class-based-controllers) - [Data Parsing, Type Hints, and Msgspec](#data-parsing-type-hints-and-msgspec) - [Plugin System, ORM support, and DTOs](#plugin-system-orm-support-and-dtos) - [OpenAPI](#openapi) - [Dependency Injection](#dependency-injection) - [Middleware](#middleware) - [Route Guards](#route-guards) - [Request Life Cycle Hooks](#request-life-cycle-hooks) - [Performance](#performance) - [Contributing](#contributing) </details> ## Installation ```shell pip install litestar ``` or to include the CLI and a server (uvicorn) for running your application: ```shell pip install litestar[standard] ``` ## Quick Start ```python title="app.py" from litestar import Litestar, get @get("/") async def hello_world() -> dict[str, str]: """Keeping the tradition alive with hello world.""" return {"hello": "world"} app = Litestar(route_handlers=[hello_world]) ``` And run it with ```bash litestar run ``` ## Core Features - [Class based controllers](#class-based-controllers) - [Dependency Injection](#dependency-injection) - [Layered Middleware](#middleware) - [Plugin System](#plugin-system-orm-support-and-dtos) - [OpenAPI 3.1 schema generation](#openapi) - [Life Cycle Hooks](#request-life-cycle-hooks) - [Route Guards based Authorization](#route-guards) - Support for `dataclasses`, `TypedDict`, [`msgspec`](https://jcristharif.com/msgspec/), [pydantic version 1 and version 2 (even within the same application)](https://docs.pydantic.dev/latest/) and [(c)attrs](https://catt.rs/en/stable/) [msgspec](https://github.com/jcrist/msgspec) and [attrs](https://www.attrs.org/en/stable/) - Layered parameter declaration - Support for [RFC 9457](https://datatracker.ietf.org/doc/html/rfc9457) standardized "Problem Detail" error responses - [Automatic API documentation with](#redoc-swagger-ui-and-stoplight-elements-api-documentation): - [Scalar](https://github.com/scalar/scalar/) - [RapiDoc](https://github.com/rapi-doc/RapiDoc) - [Redoc](https://github.com/Redocly/redoc) - [Stoplight Elements](https://github.com/stoplightio/elements) - [Swagger-UI](https://swagger.io/tools/swagger-ui/) - [Trio](https://trio.readthedocs.io/en/stable/) support (built-in, via [AnyIO](https://anyio.readthedocs.io/)) - Ultra-fast validation, serialization and deserialization using [msgspec](https://github.com/jcrist/msgspec) - [SQLAlchemy integration](https://docs.advanced-alchemy.litestar.dev/latest/) ## Example Applications <details> <summary>Pre-built Example Apps</summary> - [litestar-hello-world](https://github.com/litestar-org/litestar-hello-world): A bare-minimum application setup. Great for testing and POC work. - [litestar-fullstack](https://github.com/litestar-org/litestar-fullstack): A reference application that contains most of the boilerplate required for a web application. It features a Litestar app configured with best practices, SQLAlchemy 2.0 and SAQ, a frontend integrated with Vitejs and Jinja2 templates, Docker, and more. Like all Litestar projects, this application is open to contributions, big and small. </details> ## Sponsors Litestar is an open-source project, and we enjoy the support of our sponsors to help fund the exciting work we do. A **huge** thanks to our sponsors: [//]: # "Note to maintainers: Highest sponsors first; no more than 3 per row - create new div if needed" <a href="https://github.com/scalar/scalar/?utm_source=litestar&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar.com - Document, Discover and Test APIs with Scalar."><img src="https://raw.githubusercontent.com/litestar-org/branding/main/assets/sponsors/scalar.svg" width="180" alt="Scalar.com"></a> <a href="https://telemetrysports.com/" title="Telemetry Sports - Changing the way data influences the sports experience"><img src="https://raw.githubusercontent.com/litestar-org/branding/main/assets/sponsors/telemetry-sports/unofficial-telemetry-whitebg.svg" width="150" alt="Telemetry Sports"></a> <a href="https://docs.litestar.dev/dev/#sponsors" class="external-link" target="_blank">Check out our sponsors in the docs</a> If you would like to support the work that we do please consider [becoming a sponsor][sponsor-polar] via [Polar.sh][sponsor-polar] (preferred), [GitHub][sponsor-github] or [Open Collective][sponsor-oc]. Also, exclusively with [Polar][sponsor-polar], you can engage in pledge-based sponsorships. [sponsor-github]: https://github.com/sponsors/litestar-org [sponsor-oc]: https://opencollective.com/litestar [sponsor-polar]: https://polar.sh/litestar-org ## Features ### Class-based Controllers While supporting function-based route handlers, Litestar also supports and promotes python OOP using class based controllers: <details> <summary>Example for class-based controllers</summary> ```python title="my_app/controllers/user.py" from typing import List, Optional from datetime import datetime from litestar import Controller, get, post, put, patch, delete from litestar.dto import DTOData from pydantic import UUID4 from my_app.models import User, PartialUserDTO class UserController(Controller): path = "/users" @post() async def create_user(self, data: User) -> User: ... @get() async def list_users(self) -> List[User]: ... @get(path="/{date:int}") async def list_new_users(self, date: datetime) -> List[User]: ... @patch(path="/{user_id:uuid}", dto=PartialUserDTO) async def partial_update_user( self, user_id: UUID4, data: DTOData[PartialUserDTO] ) -> User: ... @put(path="/{user_id:uuid}") async def update_user(self, user_id: UUID4, data: User) -> User: ... @get(path="/{user_name:str}") async def get_user_by_name(self, user_name: str) -> Optional[User]: ... @get(path="/{user_id:uuid}") async def get_user(self, user_id: UUID4) -> User: ... @delete(path="/{user_id:uuid}") async def delete_user(self, user_id: UUID4) -> None: ... ``` </details> ### Data Parsing, Type Hints, and Msgspec Litestar is rigorously typed, and it enforces typing. For example, if you forget to type a return value for a route handler, an exception will be raised. The reason for this is that Litestar uses typing data to generate OpenAPI specs, as well as to validate and parse data. Thus, typing is essential to the framework. Furthermore, Litestar allows extending its support using plugins. ### Plugin System, ORM support, and DTOs Litestar has a plugin system that allows the user to extend serialization/deserialization, OpenAPI generation, and other features. It ships with a builtin plugin for SQL Alchemy, which allows the user to use SQLAlchemy declarative classes "natively" i.e., as type parameters that will be serialized/deserialized and to return them as values from route handlers. Litestar also supports the programmatic creation of DTOs with a `DTOFactory` class, which also supports the use of plugins. ### OpenAPI Litestar has custom logic to generate OpenAPI 3.1.0 schema, include optional generation of examples using the [`polyfactory`](https://pypi.org/project/polyfactory/) library. #### ReDoc, Swagger-UI and Stoplight Elements API Documentation Litestar serves the documentation from the generated OpenAPI schema with: - [ReDoc](https://redoc.ly/) - [Swagger-UI](https://swagger.io/tools/swagger-ui/) - [Stoplight Elements](https://github.com/stoplightio/elements) - [RapiDoc](https://rapidocweb.com/) All these are available and enabled by default. ### Dependency Injection Litestar has a simple but powerful DI system inspired by pytest. You can define named dependencies - sync or async - at different levels of the application, and then selective use or overwrite them. <details> <summary>Example for DI</summary> ```python from litestar import Litestar, get from litestar.di import Provide async def my_dependency() -> str: ... @get("/") async def index(injected: str) -> str: return injected app = Litestar([index], dependencies={"injected": Provide(my_dependency)}) ``` </details> ### Middleware Litestar supports typical ASGI middleware and ships with middlewares to handle things such as - CORS - CSRF - Rate limiting - GZip and Brotli compression - Client- and server-side sessions ### Route Guards Litestar has an authorization mechanism called `guards`, which allows the user to define guard functions at different level of the application (app, router, controller etc.) and validate the request before hitting the route handler function. <details> <summary>Example for route guards</summary> ```python from litestar import Litestar, get from litestar.connection import ASGIConnection from litestar.handlers.base import BaseRouteHandler from litestar.exceptions import NotAuthorizedException async def is_authorized(connection: ASGIConnection, handler: BaseRouteHandler) -> None: # validate authorization # if not authorized, raise NotAuthorizedException raise NotAuthorizedException() @get("/", guards=[is_authorized]) async def index() -> None: ... app = Litestar([index]) ``` </details> ### Request Life Cycle Hooks Litestar supports request life cycle hooks, similarly to Flask - i.e. `before_request` and `after_request` ## Performance Litestar is fast. It is on par with, or significantly faster than comparable ASGI frameworks. You can see and run the benchmarks [here](https://github.com/litestar-org/api-performance-tests), or read more about it [here](https://docs.litestar.dev/latest/benchmarks) in our documentation. ## Contributing Litestar is open to contributions big and small. You can always [join our discord](https://discord.gg/litestar) server or [join our Matrix](https://matrix.to/#/#litestar:matrix.org) space to discuss contributions and project maintenance. For guidelines on how to contribute, please see [the contribution guide](CONTRIBUTING.rst). ��������������������������������������litestar-2.16.0/docs/_static/�����������������������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0016011�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/_static/style.css��������������������������������������������������������������0000664�0000000�0000000�00000001234�15005643713�0017663�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#version-warning { top: 0; position: sticky; z-index: 60; width: 100%; height: 2.5rem; display: flex; column-gap: 0.5rem; justify-content: center; justify-items: center; align-items: center; background-color: #eee; border-bottom: 2px solid #ae2828; } @media (prefers-color-scheme: dark) { body:not([data-theme="light"]) #version-warning { background-color: black; } } .breaking-change { font-variant: all-small-caps; margin-left: 0.4rem; color: #f55353; } p { font-size: 1.1em; } html[data-theme="dark"] .mermaid svg { background-color: white; padding: 1em; } html[data-theme="dark"] .mermaid svg p { color: black; } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/_static/versioning.js����������������������������������������������������������0000664�0000000�0000000�00000005704�15005643713�0020540�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������const loadVersions = async () => { const res = await fetch( DOCUMENTATION_OPTIONS.URL_ROOT + "_static/versions.json", ); if (res.status !== 200) { return null; } return await res.json(); }; const addVersionWarning = (currentVersion, latestVersion) => { if (currentVersion === latestVersion) { return; } const header = document.querySelector(".bd-header__inner")?.parentElement; if (!header) { return; } const container = document.createElement("div"); container.id = "version-warning"; const warningText = document.createElement("span"); warningText.textContent = `You are viewing the documentation for ${ currentVersion === "dev" || parseInt(currentVersion) > parseInt(latestVersion) ? "a preview" : "an outdated" } version of Litestar.`; container.appendChild(warningText); const latestLink = document.createElement("a"); latestLink.textContent = "Click here to go to the latest version"; latestLink.href = DOCUMENTATION_OPTIONS.URL_ROOT + "../latest"; container.appendChild(latestLink); header.before(container); }; const formatVersionName = (version, isLatest) => version + (isLatest ? " (latest)" : ""); const addVersionSelect = (currentVersion, versionSpec) => { const navEnd = document.querySelector(".navbar-header-items__end"); if (!navEnd) { return; } const container = document.createElement("div"); container.classList.add("navbar-nav"); const dropdown = document.createElement("div"); dropdown.classList.add("dropdown"); container.appendChild(dropdown); const dropdownToggle = document.createElement("button"); dropdownToggle.classList.add("btn", "dropdown-toggle", "nav-item"); dropdownToggle.setAttribute("data-bs-toggle", "dropdown"); dropdownToggle.setAttribute("type", "button"); dropdownToggle.textContent = `Version: ${formatVersionName( currentVersion, currentVersion === versionSpec.latest, )}`; dropdown.appendChild(dropdownToggle); const dropdownContent = document.createElement("div"); dropdownContent.classList.add("dropdown-menu"); dropdown.appendChild(dropdownContent); for (const version of versionSpec.versions) { const navItem = document.createElement("li"); navItem.classList.add("nav-item"); const navLink = document.createElement("a"); navLink.classList.add("nav-link", "nav-internal"); navLink.href = DOCUMENTATION_OPTIONS.URL_ROOT + `../${version}`; navLink.textContent = formatVersionName( version, version === versionSpec.latest, ); navItem.appendChild(navLink); dropdownContent.appendChild(navItem); } navEnd.prepend(container); }; const setupVersioning = (versions) => { if (versions === null) { return; } const currentVersion = DOCUMENTATION_OPTIONS.VERSION; addVersionWarning(currentVersion, versions.latest); addVersionSelect(currentVersion, versions); }; window.addEventListener("DOMContentLoaded", () => { loadVersions().then(setupVersioning); }); ������������������������������������������������������������litestar-2.16.0/docs/_static/versions.json����������������������������������������������������������0000664�0000000�0000000�00000000073�15005643713�0020554�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "versions": ["1", "2", "main", "3-dev"], "latest": "2" } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/admonitions/�������������������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0016707�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/admonitions/sync-to-thread-info.rst��������������������������������������������0000664�0000000�0000000�00000001652�15005643713�0023237�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������.. admonition:: Synchronous and asynchronous callables :class: important Both synchronous and asynchronous callables are supported. One important aspect of this is that using a synchronous function which perform blocking operations, such as I/O or computationally intensive tasks, can potentially block the main thread running the event loop, and in turn block the whole application. To mitigate this, the ``sync_to_thread`` parameter can be set to ``True``, which will result in the function being run in a thread pool. If a synchronous function is non-blocking, setting ``sync_to_thread`` to ``False`` will tell Litestar that the user is sure about its behavior and the function can be treated as non-blocking. If a synchronous function is passed, without setting an explicit ``sync_to_thread`` value, a warning will be raised. .. seealso:: :doc:`/topics/sync-vs-async` ��������������������������������������������������������������������������������������litestar-2.16.0/docs/benchmarks.rst�����������������������������������������������������������������0000664�0000000�0000000�00000007741�15005643713�0017243�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������Benchmarks ========== Methodology ----------- - Benchmarking is done using the `bombardier <https://github.com/codesenberg/bombardier>`__ benchmarking tool. - Benchmarks are run on a dedicated machine, with a base Debian 11 installation. - Each framework is contained within its own docker container, running on a dedicated CPU core (using the ``cset shield`` command and the ``--cpuset-cpus`` option for docker) - Tests for the frameworks are written to make them as comparable as possible while completing the same tasks (you can see them `here <https://github.com/litestar-org/api-performance-tests/tree/main/frameworks>`__) - Each application is run using `uvicorn <https://www.uvicorn.org/>`__ with **one worker** and `uvloop <https://uvloop.readthedocs.io/>`__ - Test data has been randomly generated and is being imported from a shared module - All frameworks are used with their "stock" configuration, i.e. without applying any additional optimizations. All tests have been written according to the respective official documentation, applying the practices shown there Results ------- .. note:: If a result is missing for a specific framework that means either - The framework does not support this functionality (this will be mentioned in the test description) - More than 0.1% of responses were dropped JSON ~~~~ Serializing a dictionary into JSON .. figure:: /images/benchmarks/rps_json.svg :alt: RPS JSON RPS JSON .. note:: Because all frameworks are being used in their "stock" configuration, Litestar will run the data through `msgspec <https://jcristharif.com/msgspec/>`_ and FastAPI through `Pydantic <https://docs.pydantic.dev/latest/>`_ Serialization ~~~~~~~~~~~~~ Serializing Pydantic models and dataclasses into JSON .. figure:: /images/benchmarks/rps_serialization.svg :alt: RPS serializing Pydantic models and dataclasses into JSON RPS serializing Pydantic models and dataclasses into JSON Files ~~~~~ .. figure:: /images/benchmarks/rps_files.svg :alt: RPS files RPS files .. note:: Synchronous file responses are not / only partially supported for Sanic and Quart Path and query parameter handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *All responses return “No Content”* - No params: No path parameters - Path params: Single path parameter, coerced into an integer - Query params: Single query parameter, coerced into an integer - Mixed params: A path and a query parameters, coerced into integers .. figure:: /images/benchmarks/rps_params.svg :alt: RPS path and query parameters RPS path and query parameters Dependency injection ~~~~~~~~~~~~~~~~~~~~ - Resolving 3 nested synchronous dependencies - Resolving 3 nested asynchronous dependencies (only supported by ``Litestar`` and ``FastAPI``) - Resolving 3 nested synchronous, and 3 nested asynchronous dependencies (only supported by ``Litestar`` and ``FastAPI``) .. figure:: /images/benchmarks/rps_dependency-injection.svg :alt: RPS Dependency injection RPS Dependency injection .. note:: Dependency injection is not supported by Starlette. Plaintext ~~~~~~~~~ .. figure:: /images/benchmarks/rps_plaintext.svg :alt: RPS Plaintext RPS Plaintext Interpreting the results ------------------------ An interpretation of these results should be approached with caution, as is the case for nearly all benchmarks. A high score in a test does not necessarily translate to high performance of **your** application and **your** use case. For almost any test you can probably write an app that performs better or worse at a comparable task **in your scenario**. While trying to design the tests in a way that simulate somewhat realistic scenarios, they can never give an exact representation of how a real world application behaves and performs, where, aside from the workload, many other factors come into play. These tests were mainly written to be used internally for Litestar development, to help us locate and track performance regressions and improvements. �������������������������������litestar-2.16.0/docs/conf.py������������������������������������������������������������������������0000664�0000000�0000000�00000037116�15005643713�0015672�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations import importlib.metadata import os import re import warnings from functools import partial from typing import Any from sphinx.addnodes import document from sphinx.application import Sphinx from sqlalchemy.exc import SAWarning warnings.filterwarnings("ignore", category=SAWarning) __all__ = ["setup", "update_html_context"] PY_CLASS = "py:class" PY_RE = r"py:.*" PY_METH = "py:meth" PY_ATTR = "py:attr" PY_OBJ = "py:obj" PY_FUNC = "py:func" project = "Litestar" copyright = "2025, Litestar-Org" author = "Litestar-Org" release = os.getenv("_LITESTAR_DOCS_BUILD_VERSION", importlib.metadata.version("litestar").rsplit(".")[0]) extensions = [ "sphinx.ext.intersphinx", "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.autosectionlabel", "sphinx_design", "auto_pytabs.sphinx_ext", "tools.sphinx_ext", "sphinx_copybutton", "sphinxcontrib.mermaid", "sphinx_click", "sphinx_paramlinks", ] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "msgspec": ("https://jcristharif.com/msgspec/", None), "anyio": ("https://anyio.readthedocs.io/en/stable/", None), "multidict": ("https://multidict.aio-libs.org/en/stable/", None), "cryptography": ("https://cryptography.io/en/latest/", None), "sqlalchemy": ("https://docs.sqlalchemy.org/en/20/", None), "alembic": ("https://alembic.sqlalchemy.org/en/latest/", None), "click": ("https://click.palletsprojects.com/en/8.1.x/", None), "redis": ("https://redis-py.readthedocs.io/en/stable/", None), "picologging": ("https://microsoft.github.io/picologging", None), "structlog": ("https://www.structlog.org/en/stable/", None), "tortoise": ("https://tortoise.github.io/", None), "piccolo": ("https://piccolo-orm.readthedocs.io/en/latest", None), "opentelemetry": ("https://opentelemetry-python.readthedocs.io/en/latest/", None), "advanced-alchemy": ("https://docs.advanced-alchemy.litestar.dev/latest/", None), "jinja2": ("https://jinja.palletsprojects.com/en/latest/", None), "trio": ("https://trio.readthedocs.io/en/stable/", None), "pydantic": ("https://docs.pydantic.dev/latest/", None), "typing_extensions": ("https://typing-extensions.readthedocs.io/en/stable/", None), "valkey": ("https://valkey-py.readthedocs.io/en/latest/", None), } napoleon_google_docstring = True napoleon_include_special_with_doc = True napoleon_use_admonition_for_examples = True napoleon_use_admonition_for_notes = True napoleon_use_admonition_for_references = False napoleon_attr_annotations = True autoclass_content = "class" autodoc_class_signature = "separated" autodoc_default_options = {"special-members": "__init__", "show-inheritance": True, "members": True} autodoc_member_order = "bysource" autodoc_typehints_format = "short" autodoc_mock_imports = [] nitpicky = True nitpick_ignore = [ # external library / undocumented external (PY_CLASS, "BaseModel"), (PY_CLASS, "ExternalType"), (PY_CLASS, "TypeEngine"), (PY_CLASS, "UserDefinedType"), (PY_CLASS, "_RegistryType"), (PY_CLASS, "_orm.Mapper"), (PY_CLASS, "_orm.registry"), (PY_CLASS, "_schema.MetaData"), (PY_CLASS, "_schema.Table"), (PY_CLASS, "_types.TypeDecorator"), (PY_CLASS, "abc.Collection"), (PY_CLASS, "advanced_alchemy.utils.dataclass.Empty"), (PY_CLASS, "jinja2.environment.Environment"), (PY_CLASS, "pydantic.BaseModel"), (PY_CLASS, "pydantic.generics.GenericModel"), (PY_CLASS, "pydantic.main.BaseModel"), (PY_CLASS, "redis.asyncio.Redis"), (PY_CLASS, "sqlalchemy.dialects.postgresql.named_types.ENUM"), (PY_CLASS, "sqlalchemy.orm.decl_api.DeclarativeMeta"), (PY_CLASS, "sqlalchemy.sql.sqltypes.TupleType"), (PY_CLASS, "valkey.asyncio.Valkey"), (PY_METH, "_types.TypeDecorator.process_bind_param"), (PY_METH, "_types.TypeDecorator.process_result_value"), (PY_METH, "litestar.typing.ParsedType.is_subclass_of"), (PY_METH, "type_engine"), # type vars and aliases / intentionally undocumented (PY_CLASS, "AnyIOBackend"), (PY_CLASS, "BaseSessionBackendT"), (PY_CLASS, "C"), (PY_CLASS, "CollectionT"), (PY_CLASS, "ControllerRouterHandler"), (PY_CLASS, "EmptyType"), (PY_CLASS, "ModelT"), (PY_CLASS, "PathParameterDefinition"), (PY_CLASS, "RouteHandlerType"), (PY_CLASS, "SelectT"), (PY_CLASS, "T"), (PY_OBJ, "litestar.security.base.AuthType"), # investigate (PY_CLASS, "Environment"), (PY_CLASS, "P"), (PY_CLASS, "pydantic_v1.BaseModel"), (PY_CLASS, "pydantic_v2.BaseModel"), (PY_CLASS, "advanced_alchemy.config.types.Empty"), (PY_OBJ, "litestar.template.base.TemplateType_co"), (PY_OBJ, "litestar.template.base.ContextType_co"), (PY_CLASS, "litestar.template.base.TemplateType_co"), (PY_CLASS, "litestar.template.base.ContextType_co"), (PY_CLASS, "litestar.template.base.R"), (PY_ATTR, "litestar.openapi.controller.OpenAPIController.swagger_ui_init_oauth"), # intentionally undocumented (PY_CLASS, "BacklogStrategy"), (PY_CLASS, "ExceptionT"), (PY_CLASS, "NoneType"), (PY_CLASS, "litestar._openapi.schema_generation.schema.SchemaCreator"), (PY_CLASS, "litestar._signature.model.SignatureModel"), (PY_CLASS, "litestar.contrib.sqlalchemy.plugins.init.config.compat._CreateEngineMixin"), (PY_CLASS, "litestar.utils.signature.ParsedSignature"), (PY_CLASS, "litestar.utils.sync.AsyncCallable"), # types in changelog that no longer exist (PY_ATTR, "litestar.dto.factory.DTOConfig.underscore_fields_private"), (PY_CLASS, "anyio.abc.BlockingPortal"), (PY_CLASS, "litestar.contrib.msgspec.MsgspecDTO"), (PY_CLASS, "litestar.contrib.repository.filters.NotInCollectionFilter"), (PY_CLASS, "litestar.contrib.repository.filters.NotInSearchFilter"), (PY_CLASS, "litestar.contrib.repository.filters.OnBeforeAfter"), (PY_CLASS, "litestar.contrib.repository.filters.OrderBy"), (PY_CLASS, "litestar.contrib.repository.filters.SearchFilter"), (PY_CLASS, "litestar.dto.base_factory.AbstractDTOFactory"), (PY_CLASS, "litestar.dto.factory.DTOConfig"), (PY_CLASS, "litestar.dto.factory.DTOData"), (PY_CLASS, "litestar.dto.interface.DTOInterface"), (PY_CLASS, "litestar.partial.Partial"), (PY_CLASS, "litestar.response.RedirectResponse"), (PY_CLASS, "litestar.response_containers.Redirect"), (PY_CLASS, "litestar.response_containers.Template"), (PY_CLASS, "litestar.contrib.sqlalchemy.plugins.SQLAlchemyPlugin"), (PY_CLASS, "litestar.contrib.sqlalchemy.plugins.SQLAlchemySerializationPlugin"), (PY_CLASS, "litestar.contrib.sqlalchemy.plugins.SQLAlchemyInitPlugin"), (PY_CLASS, "litestar.contrib.sqlalchemy.dto.SQLAlchemyDTO"), (PY_CLASS, "litestar.contrib.sqlalchemy.types.BigIntIdentity"), (PY_CLASS, "litestar.contrib.sqlalchemy.types.JsonB"), (PY_CLASS, "litestar.contrib.htmx.request.HTMXRequest"), (PY_CLASS, "litestar.typing.ParsedType"), (PY_METH, "litestar.dto.factory.DTOData.create_instance"), (PY_METH, "litestar.dto.interface.DTOInterface.data_to_encodable_type"), (PY_CLASS, "MetaData"), (PY_FUNC, "sqlalchemy.get_engine"), (PY_OBJ, "litestar.template.base.T_co"), ("py:exc", "RepositoryError"), ("py:exc", "InternalServerError"), ("py:exc", "HTTPExceptions"), (PY_CLASS, "litestar.template.Template"), (PY_CLASS, "litestar.middleware.compression.gzip_facade.GzipCompression"), (PY_CLASS, "litestar.handlers.http_handlers.decorators._subclass_warning"), (PY_CLASS, "litestar.background_tasks.P"), (PY_CLASS, "P.args"), (PY_CLASS, "P.kwargs"), (PY_CLASS, "litestar.contrib.jinja.P"), (PY_CLASS, "litestar.contrib.mako.P"), (PY_CLASS, "JWTDecodeOptions"), (PY_CLASS, "litestar.template.base.P"), (PY_CLASS, "litestar.contrib.pydantic.PydanticDTO"), (PY_CLASS, "litestar.contrib.pydantic.PydanticPlugin"), (PY_CLASS, "typing.Self"), (PY_CLASS, "attr.AttrsInstance"), (PY_CLASS, "typing_extensions.TypeGuard"), (PY_CLASS, "advanced_alchemy.types.BigIntIdentity"), (PY_CLASS, "advanced_alchemy.types.JsonB"), (PY_CLASS, "advanced_alchemy.repository.SQLAlchemyAsyncRepository"), # docs in flux as we prepare for `advanced_alchemy` 1.0 release. re-enable when finished (PY_CLASS, "advanced_alchemy.base.UUIDBase"), (PY_CLASS, "advanced_alchemy.base.UUIDAuditBase"), (PY_CLASS, "advanced_alchemy.base.BigIntBase"), (PY_CLASS, "advanced_alchemy.base.BigIntAuditBase"), ] nitpick_ignore_regex = [ (PY_ATTR, "litestar.repository.testing.AsyncGenericMockRepository.id_attribute"), (PY_ATTR, "litestar.repository.AbstractAsyncRepository.id_attribute"), (PY_ATTR, "litestar.repository.AbstractSyncRepository.id_attribute"), # (PY_ATTR, "litestar.repository.AsyncGenericMockRepository.id_attribute"), (PY_OBJ, r"typing\..*"), (PY_RE, r".*R_co"), (PY_RE, r".*UserType"), (PY_RE, r"ModelT"), (PY_RE, r"litestar.*\.T"), (PY_RE, r"litestar.contrib.sqlalchemy.repository.ModelT"), (PY_RE, r"litestar\.middleware\.session\.base\.BaseSessionBackendT"), (PY_RE, r"litestar\.types.*"), (PY_RE, r"httpx.*"), # type vars (PY_RE, r"litestar.middleware.session.base.ConfigT"), (PY_RE, r"litestar\.connection\.base\.AuthT"), (PY_RE, r"litestar\.connection\.base\.HandlerT"), (PY_RE, r"litestar\.connection\.base\.StateT"), (PY_RE, r"litestar\.connection\.base\.UserT"), (PY_RE, r"litestar\.pagination\.C"), (PY_RE, r"multidict\..*"), (PY_RE, r"advanced_alchemy.*\.T"), (PY_RE, r"advanced_alchemy\.config.common\.EngineT"), (PY_RE, r"advanced_alchemy\.config.common\.SessionT"), (PY_RE, r".*R"), (PY_OBJ, r"litestar.security.jwt.auth.TokenT"), (PY_CLASS, "ExceptionToProblemDetailMapType"), (PY_CLASS, "litestar.security.jwt.token.JWTDecodeOptions"), ] # Warnings about missing references to those targets in the specified location will be ignored. # The source of the references is taken 1:1 from the warnings as reported by Sphinx, e.g # **/litestar/testing/client/async_client.py:docstring of litestar.testing.AsyncTestClient.exit_stack:1: WARNING: py:class reference target not found: AsyncExitStack # would be added as: "litestar.testing.AsyncTestClient.exit_stack": {"AsyncExitStack"}, ignore_missing_refs = { # No idea what autodoc is doing here. Possibly unfixable on our end "litestar.template.base.TemplateEngineProtocol.get_template": {"litestar.template.base.T_co"}, "litestar.template": {"litestar.template.base.T_co"}, "litestar.openapi.OpenAPIController.security": {"SecurityRequirement"}, "litestar.response.file.async_file_iterator": {"FileSystemAdapter"}, re.compile("litestar.response.redirect.*"): {"RedirectStatusType"}, re.compile(r"litestar\.plugins.*"): re.compile(".*ModelT"), re.compile(r"litestar\.(contrib|repository)\.*"): re.compile(".*T"), re.compile(r"litestar\.contrib\.sqlalchemy\.*"): re.compile( ".*(ConnectionT|EngineT|SessionT|SessionMakerT|SlotsBase)" ), re.compile(r"litestar\.dto.*"): re.compile(".*T|.*FieldDefinition|Empty"), re.compile(r"litestar\.template\.(config|TemplateConfig).*"): re.compile(".*EngineType"), "litestar.concurrency.set_asyncio_executor": {"ThreadPoolExecutor"}, "litestar.concurrency.get_asyncio_executor": {"ThreadPoolExecutor"}, re.compile(r"litestar\.channels\.backends\.asyncpg.*"): {"asyncpg.connection.Connection", "asyncpg.Connection"}, re.compile(r"litestar\.handlers\.websocket_handlers\.stream.*"): {"WebSocketMode"}, } # Do not warn about broken links to the following: linkcheck_ignore = [ r"http://localhost(:\d+)?", r"http://127.0.0.1(:\d+)?", "http://testserver", ] auto_pytabs_min_version = (3, 8) auto_pytabs_max_version = (3, 11) auto_pytabs_compat_mode = True autosectionlabel_prefix_document = True suppress_warnings = [ "autosectionlabel.*", "ref.python", # TODO: remove when https://github.com/sphinx-doc/sphinx/issues/4961 is fixed ] html_theme = "litestar_sphinx_theme" html_static_path = ["_static"] html_js_files = ["versioning.js"] html_css_files = ["style.css"] html_show_sourcelink = False html_title = "Litestar Framework" html_theme_options = { "use_page_nav": False, "github_repo_name": "litestar", "logo": { "link": "https://litestar.dev", }, "pygment_light_style": "xcode", "pygment_dark_style": "lightbulb", "navigation_with_keys": True, "extra_navbar_items": { "Documentation": "index", "Community": { "Contributing": { "description": "Learn how to contribute to the Litestar project", "link": "https://docs.litestar.dev/latest/contribution-guide.html", "icon": "contributing", }, "Code of Conduct": { "description": "Review the etiquette for interacting with the Litestar community", "link": "https://github.com/litestar-org/.github?tab=coc-ov-file", "icon": "coc", }, "Security": { "description": "Overview of Litestar's security protocols", "link": "https://github.com/litestar-org/.github?tab=coc-ov-file#security-ov-file", "icon": "coc", }, }, "About": { "Litestar Organization": { "description": "Details about the Litestar organization", "link": "https://litestar.dev/about/organization", "icon": "org", }, "Releases": { "description": "Explore the release process, versioning, and deprecation policy for Litestar", "link": "https://litestar.dev/about/litestar-releases", "icon": "releases", }, }, "Release notes": { "What's new in 2.0": "release-notes/whats-new-2", "2.x Changelog": "https://docs.litestar.dev/2/release-notes/changelog.html", "1.x Changelog": "https://docs.litestar.dev/1/release-notes/changelog.html", }, "Help": { "Discord Help Forum": { "description": "Dedicated Discord help forum", "link": "https://discord.gg/litestar", "icon": "coc", }, "GitHub Discussions": { "description": "GitHub Discussions ", "link": "https://github.com/orgs/litestar-org/discussions", "icon": "coc", }, "Stack Overflow": { "description": "We monitor the <code><b>litestar</b></code> tag on Stack Overflow", "link": "https://stackoverflow.com/questions/tagged/litestar", "icon": "coc", }, }, }, } def update_html_context( app: Sphinx, pagename: str, templatename: str, context: dict[str, Any], doctree: document ) -> None: context["generate_toctree_html"] = partial(context["generate_toctree_html"], startdepth=0) def delayed_setup(app: Sphinx) -> None: """ When running linkcheck pydata_sphinx_theme causes a build failure, and checking the builder in the initial `setup` function call is not possible, so the check and extension setup has to be delayed until the builder is initialized. """ if app.builder.name == "linkcheck": return app.setup_extension("pydata_sphinx_theme") app.connect("html-page-context", update_html_context) # type: ignore def setup(app: Sphinx) -> dict[str, bool]: app.connect("builder-inited", delayed_setup, priority=0) # type: ignore app.setup_extension("litestar_sphinx_theme") return {"parallel_read_safe": True, "parallel_write_safe": True} ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/contribution-guide.rst���������������������������������������������������������0000664�0000000�0000000�00000000053�15005643713�0020725�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������:orphan: .. include:: ../CONTRIBUTING.rst �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/����������������������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0016201�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/__init__.py�����������������������������������������������������������0000664�0000000�0000000�00000000000�15005643713�0020300�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/application_hooks/����������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0021707�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/application_hooks/__init__.py�����������������������������������������0000664�0000000�0000000�00000000000�15005643713�0024006�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/application_hooks/after_exception_hook.py�����������������������������0000664�0000000�0000000�00000002041�15005643713�0026455�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import logging from typing import TYPE_CHECKING from litestar import Litestar, get from litestar.exceptions import HTTPException from litestar.status_codes import HTTP_400_BAD_REQUEST logger = logging.getLogger() if TYPE_CHECKING: from litestar.types import Scope @get("/some-path", sync_to_thread=False) def my_handler() -> None: """Route handler that raises an exception.""" raise HTTPException(detail="bad request", status_code=HTTP_400_BAD_REQUEST) async def after_exception_handler(exc: Exception, scope: "Scope") -> None: """Hook function that will be invoked after each exception.""" state = Litestar.from_scope(scope).state if not hasattr(state, "error_count"): state.error_count = 1 else: state.error_count += 1 logger.info( "an exception of type %s has occurred for requested path %s and the application error count is %d.", type(exc).__name__, scope["path"], state.error_count, ) app = Litestar([my_handler], after_exception=[after_exception_handler]) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/application_hooks/before_send_hook.py���������������������������������0000664�0000000�0000000�00000002066�15005643713�0025560�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from typing import TYPE_CHECKING from litestar import Litestar, get from litestar.datastructures import MutableScopeHeaders if TYPE_CHECKING: from typing import Dict from litestar.types import Message, Scope @get("/test", sync_to_thread=False) def handler() -> Dict[str, str]: """Example Handler function.""" return {"key": "value"} async def before_send_hook_handler(message: Message, scope: Scope) -> None: """The function will be called on each ASGI message. We therefore ensure it runs only on the message start event. """ if message["type"] == "http.response.start": headers = MutableScopeHeaders.from_message(message=message) headers["My Header"] = Litestar.from_scope(scope).state.message def on_startup(app: Litestar) -> None: """A function that will populate the app state before any requests are received.""" app.state.message = "value injected during send" app = Litestar(route_handlers=[handler], on_startup=[on_startup], before_send=[before_send_hook_handler]) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/application_hooks/lifespan_manager.py���������������������������������0000664�0000000�0000000�00000001076�15005643713�0025560�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from contextlib import asynccontextmanager from typing import AsyncGenerator from sqlalchemy.ext.asyncio import create_async_engine from litestar import Litestar @asynccontextmanager async def db_connection(app: Litestar) -> AsyncGenerator[None, None]: engine = getattr(app.state, "engine", None) if engine is None: engine = create_async_engine("postgresql+asyncpg://postgres:mysecretpassword@pg.db:5432/db") app.state.engine = engine try: yield finally: await engine.dispose() app = Litestar(lifespan=[db_connection]) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/application_hooks/on_app_init.py��������������������������������������0000664�0000000�0000000�00000001225�15005643713�0024560�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import TYPE_CHECKING from litestar import Litestar if TYPE_CHECKING: from litestar.config.app import AppConfig async def close_db_connection() -> None: """Closes the database connection on application shutdown.""" def receive_app_config(app_config: "AppConfig") -> "AppConfig": """Receives parameters from the application. In reality, this would be a library of boilerplate that is carried from one application to another, or a third-party developed application configuration tool. """ app_config.on_shutdown.append(close_db_connection) return app_config app = Litestar([], on_app_init=[receive_app_config]) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/application_state/����������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0021704�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/application_state/__init__.py�����������������������������������������0000664�0000000�0000000�00000000000�15005643713�0024003�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/application_state/passing_initial_state.py����������������������������0000664�0000000�0000000�00000000432�15005643713�0026632�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Any, Dict from litestar import Litestar, get from litestar.datastructures import State @get("/", sync_to_thread=False) def handler(state: State) -> Dict[str, Any]: return state.dict() app = Litestar(route_handlers=[handler], state=State({"count": 100})) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/application_state/using_application_state.py��������������������������0000664�0000000�0000000�00000002705�15005643713�0027172�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import logging from typing import TYPE_CHECKING, Any from litestar import Litestar, Request, get from litestar.datastructures import State from litestar.di import Provide if TYPE_CHECKING: from litestar.types import ASGIApp, Receive, Scope, Send logger = logging.getLogger(__name__) def set_state_on_startup(app: Litestar) -> None: """Startup and shutdown hooks can receive `State` as a keyword arg.""" app.state.value = "abc123" def middleware_factory(*, app: "ASGIApp") -> "ASGIApp": """A middleware can access application state via `scope`.""" async def my_middleware(scope: "Scope", receive: "Receive", send: "Send") -> None: state = Litestar.from_scope(scope).state logger.info("state value in middleware: %s", state.value) await app(scope, receive, send) return my_middleware async def my_dependency(state: State) -> Any: """Dependencies can receive state via injection.""" logger.info("state value in dependency: %s", state.value) @get("/", dependencies={"dep": Provide(my_dependency)}, middleware=[middleware_factory], sync_to_thread=False) def get_handler(state: State, request: Request, dep: Any) -> None: """Handlers can receive state via injection.""" logger.info("state value in handler from `State`: %s", state.value) logger.info("state value in handler from `Request`: %s", request.app.state.value) app = Litestar(route_handlers=[get_handler], on_startup=[set_state_on_startup]) �����������������������������������������������������������litestar-2.16.0/docs/examples/application_state/using_custom_state.py�������������������������������0000664�0000000�0000000�00000000572�15005643713�0026201�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Any, Dict from litestar import Litestar, get from litestar.datastructures import State class MyState(State): count: int = 0 def increment(self) -> None: self.count += 1 @get("/", sync_to_thread=False) def handler(state: MyState) -> Dict[str, Any]: state.increment() return state.dict() app = Litestar(route_handlers=[handler]) ��������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/application_state/using_immutable_state.py����������������������������0000664�0000000�0000000�00000000507�15005643713�0026644�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Any, Dict from litestar import Litestar, get from litestar.datastructures import ImmutableState @get("/", sync_to_thread=False) def handler(state: ImmutableState) -> Dict[str, Any]: setattr(state, "count", 1) # raises AttributeError return state.dict() app = Litestar(route_handlers=[handler]) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/caching/��������������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0017575�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/caching/__init__.py���������������������������������������������������0000664�0000000�0000000�00000000000�15005643713�0021674�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/caching/cache.py������������������������������������������������������0000664�0000000�0000000�00000001050�15005643713�0021206�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar, get from litestar.config.response_cache import CACHE_FOREVER @get("/cached", cache=True) async def my_cached_handler() -> str: return "cached" @get("/cached-seconds", cache=120) # seconds async def my_cached_handler_seconds() -> str: return "cached for 120 seconds" @get("/cached-forever", cache=CACHE_FOREVER) async def my_cached_handler_forever() -> str: return "cached forever" app = Litestar( [my_cached_handler, my_cached_handler_seconds, my_cached_handler_forever], ) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/caching/cache_response_filter.py��������������������������������������0000664�0000000�0000000�00000000754�15005643713�0024503�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar from litestar.config.response_cache import ResponseCacheConfig from litestar.types import HTTPScope def custom_cache_response_filter(_: HTTPScope, status_code: int) -> bool: # Cache only 2xx responses return 200 <= status_code < 300 response_cache_config = ResponseCacheConfig(cache_response_filter=custom_cache_response_filter) # Create the app with a custom cache response filter app = Litestar( response_cache_config=response_cache_config, ) ��������������������litestar-2.16.0/docs/examples/caching/key_builder.py������������������������������������������������0000664�0000000�0000000�00000000467�15005643713�0022454�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar, Request from litestar.config.response_cache import ResponseCacheConfig def key_builder(request: Request) -> str: return request.url.path + request.headers.get("my-header", "") app = Litestar([], response_cache_config=ResponseCacheConfig(key_builder=key_builder)) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/caching/key_builder_for_route_handler.py������������������������������0000664�0000000�0000000�00000000504�15005643713�0026225�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar, Request, get def key_builder(request: Request) -> str: return request.url.path + request.headers.get("my-header", "") @get("/cached-path", cache=True, cache_key_builder=key_builder) async def cached_handler() -> str: return "cached" app = Litestar([cached_handler]) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/caching/redis_store.py������������������������������������������������0000664�0000000�0000000�00000001040�15005643713�0022464�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import asyncio from litestar import Litestar, get from litestar.config.response_cache import ResponseCacheConfig from litestar.stores.redis import RedisStore @get(cache=10) async def something() -> str: await asyncio.sleep(1) return "something" redis_store = RedisStore.with_client(url="redis://localhost/", port=6379, db=0) cache_config = ResponseCacheConfig(store="redis_backed_store") app = Litestar( [something], stores={"redis_backed_store": redis_store}, response_cache_config=cache_config, ) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/channels/�������������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0017774�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/channels/create_route_handlers.py�������������������������������������0000664�0000000�0000000�00000000504�15005643713�0024706�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar from litestar.channels import ChannelsPlugin from litestar.channels.backends.memory import MemoryChannelsBackend channels_plugin = ChannelsPlugin( backend=MemoryChannelsBackend(), channels=["foo", "bar"], create_ws_route_handlers=True, ) app = Litestar(plugins=[channels_plugin]) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/channels/create_route_handlers_send_history.py������������������������0000664�0000000�0000000�00000000742�15005643713�0027504�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar from litestar.channels import ChannelsPlugin from litestar.channels.backends.memory import MemoryChannelsBackend channels_plugin = ChannelsPlugin( backend=MemoryChannelsBackend(history=10), # set the amount of messages per channel # to keep in the backend channels=["foo", "bar"], create_ws_route_handlers=True, ws_handler_send_history=10, # send 10 entries of the history by default ) app = Litestar(plugins=[channels_plugin]) ������������������������������litestar-2.16.0/docs/examples/channels/iter_stream.py�����������������������������������������������0000664�0000000�0000000�00000001055�15005643713�0022665�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar, WebSocket, websocket from litestar.channels import ChannelsPlugin from litestar.channels.backends.memory import MemoryChannelsBackend @websocket("/ws") async def handler(socket: WebSocket, channels: ChannelsPlugin) -> None: await socket.accept() async with channels.start_subscription(["some_channel"]) as subscriber: async for message in subscriber.iter_events(): await socket.send_text(message) app = Litestar( [handler], plugins=[ChannelsPlugin(backend=MemoryChannelsBackend())], ) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/channels/put_history.py�����������������������������������������������0000664�0000000�0000000�00000001052�15005643713�0022735�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar, WebSocket, websocket from litestar.channels import ChannelsPlugin from litestar.channels.backends.memory import MemoryChannelsBackend @websocket("/ws") async def handler(socket: WebSocket, channels: ChannelsPlugin) -> None: await socket.accept() async with channels.start_subscription(["some_channel"]) as subscriber: await channels.put_subscriber_history(subscriber, ["some_channel"], limit=10) app = Litestar( [handler], plugins=[ChannelsPlugin(backend=MemoryChannelsBackend(history=20))], ) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/channels/run_in_background.py�����������������������������������������0000664�0000000�0000000�00000001227�15005643713�0024041�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar, WebSocket, websocket from litestar.channels import ChannelsPlugin from litestar.channels.backends.memory import MemoryChannelsBackend @websocket("/ws") async def handler(socket: WebSocket, channels: ChannelsPlugin) -> None: await socket.accept() async with channels.start_subscription(["some_channel"]) as subscriber, subscriber.run_in_background( socket.send_text ): while True: response = await socket.receive_text() await socket.send_text(response) app = Litestar( [handler], plugins=[ChannelsPlugin(backend=MemoryChannelsBackend(), channels=["some_channel"])], ) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/��������������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0017641�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/__init__.py���������������������������������������������������0000664�0000000�0000000�00000000000�15005643713�0021740�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/piccolo/������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0021271�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/piccolo/__init__.py�������������������������������������������0000664�0000000�0000000�00000000000�15005643713�0023370�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/piccolo/app.py������������������������������������������������0000664�0000000�0000000�00000003573�15005643713�0022433�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import List from piccolo.columns import Boolean, Varchar from piccolo.table import Table, create_db_tables from litestar import Litestar, MediaType, delete, get, patch, post from litestar.contrib.piccolo import PiccoloDTO from litestar.dto import DTOConfig, DTOData from litestar.exceptions import NotFoundException from .piccolo_conf import DB class Task(Table, db=DB): """ An example table. """ name = Varchar() completed = Boolean(default=False) class PatchDTO(PiccoloDTO[Task]): """Allow partial updates.""" config = DTOConfig(exclude={"id"}, partial=True) @get( "/tasks", return_dto=PiccoloDTO[Task], media_type=MediaType.JSON, tags=["Task"], ) async def tasks() -> List[Task]: return await Task.select().order_by(Task.id, ascending=False) @post( "/tasks", dto=PiccoloDTO[Task], return_dto=PiccoloDTO[Task], media_type=MediaType.JSON, tags=["Task"], ) async def create_task(data: Task) -> Task: await data.save() await data.refresh() return data @patch( "/tasks/{task_id:int}", dto=PatchDTO, return_dto=PiccoloDTO[Task], media_type=MediaType.JSON, tags=["Task"], ) async def update_task(task_id: int, data: DTOData[Task]) -> Task: task = await Task.objects().get(Task.id == task_id) if not task: raise NotFoundException("Task does not exist") result = data.update_instance(task) await result.save() return result @delete("/tasks/{task_id:int}", tags=["Task"]) async def delete_task(task_id: int) -> None: task = await Task.objects().get(Task.id == task_id) if not task: raise NotFoundException("Task does not exist") await task.remove() async def on_startup(): await create_db_tables(Task, if_not_exists=True) app = Litestar(route_handlers=[tasks, create_task, delete_task, update_task], on_startup=[on_startup], debug=True) �������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/piccolo/piccolo_conf.py���������������������������������������0000664�0000000�0000000�00000000104�15005643713�0024273�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from piccolo.engine.sqlite import SQLiteEngine DB = SQLiteEngine() ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/���������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0022003�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/__init__.py����������������������������������������0000664�0000000�0000000�00000000000�15005643713�0024102�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/�������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0023464�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/__init__.py��������������������������������0000664�0000000�0000000�00000000000�15005643713�0025563�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/sqlalchemy_async_before_send_handler.py����0000664�0000000�0000000�00000000701�15005643713�0033423�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from litestar import Litestar from litestar.plugins.sqlalchemy import ( SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin, async_autocommit_before_send_handler, ) config = SQLAlchemyAsyncConfig( connection_string="sqlite+aiosqlite:///:memory:", before_send_handler=async_autocommit_before_send_handler ) plugin = SQLAlchemyInitPlugin(config=config) app = Litestar(route_handlers=[], plugins=[plugin]) ���������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/sqlalchemy_async_dependencies.py�����������0000664�0000000�0000000�00000001443�15005643713�0032105�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy import select from litestar import Litestar, post from litestar.plugins.sqlalchemy import SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin if TYPE_CHECKING: from typing import Tuple from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession @post("/") async def handler(db_session: AsyncSession, db_engine: AsyncEngine) -> Tuple[int, int]: one = (await db_session.execute(select(1))).scalar() async with db_engine.begin() as conn: two = (await conn.execute(select(2))).scalar() return one, two config = SQLAlchemyAsyncConfig(connection_string="sqlite+aiosqlite:///async.sqlite") plugin = SQLAlchemyInitPlugin(config=config) app = Litestar(route_handlers=[handler], plugins=[plugin]) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/sqlalchemy_async_init_plugin_example.py����0000664�0000000�0000000�00000002467�15005643713�0033522�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy import select from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, post from litestar.plugins.sqlalchemy import SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin if TYPE_CHECKING: from typing import Any, Dict, List from sqlalchemy.ext.asyncio import AsyncSession class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_item" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] @post("/") async def add_item(data: Dict[str, Any], db_session: AsyncSession) -> List[Dict[str, Any]]: todo_item = TodoItem(**data) async with db_session.begin(): db_session.add(todo_item) return [ { "title": item.title, "done": item.done, } for item in (await db_session.execute(select(TodoItem))).scalars() ] async def init_db(app: Litestar) -> None: async with config.get_engine().begin() as conn: await conn.run_sync(Base.metadata.create_all) config = SQLAlchemyAsyncConfig(connection_string="sqlite+aiosqlite:///todo_async.sqlite") plugin = SQLAlchemyInitPlugin(config=config) app = Litestar(route_handlers=[add_item], plugins=[plugin], on_startup=[init_db], debug=True) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/sqlalchemy_async_plugin_example.py���������0000664�0000000�0000000�00000001760�15005643713�0032472�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from typing import TYPE_CHECKING, Sequence from sqlalchemy import select from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, post from litestar.plugins.sqlalchemy import SQLAlchemyAsyncConfig, SQLAlchemyPlugin if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_item" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] @post("/") async def add_item(data: TodoItem, db_session: AsyncSession) -> Sequence[TodoItem]: async with db_session.begin(): db_session.add(data) return (await db_session.execute(select(TodoItem))).scalars().all() config = SQLAlchemyAsyncConfig( connection_string="sqlite+aiosqlite:///todo_async.sqlite", create_all=True, metadata=Base.metadata ) plugin = SQLAlchemyPlugin(config=config) app = Litestar(route_handlers=[add_item], plugins=[plugin]) ����������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/sqlalchemy_async_serialization_dto.py������0000664�0000000�0000000�00000001133�15005643713�0033176�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, post from litestar.plugins.sqlalchemy import SQLAlchemyDTO if TYPE_CHECKING: from typing import List class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_item" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] @post("/", dto=SQLAlchemyDTO[TodoItem]) async def add_item(data: TodoItem) -> List[TodoItem]: return [data] app = Litestar(route_handlers=[add_item]) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/sqlalchemy_async_serialization_plugin.py���0000664�0000000�0000000�00000001171�15005643713�0033710�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, post from litestar.plugins.sqlalchemy import SQLAlchemySerializationPlugin if TYPE_CHECKING: from typing import List class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_item" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] @post("/") async def add_item(data: TodoItem) -> List[TodoItem]: return [data] app = Litestar(route_handlers=[add_item], plugins=[SQLAlchemySerializationPlugin()]) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������sqlalchemy_async_serialization_plugin_marking_fields.py���������������������������������������������0000664�0000000�0000000�00000001434�15005643713�0036671�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000�litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins���������������������������������������������������������������������������������������������������������������from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, post from litestar.dto import dto_field from litestar.plugins.sqlalchemy import SQLAlchemySerializationPlugin if TYPE_CHECKING: from typing import List class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_item" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] super_secret_value: Mapped[str] = mapped_column(info=dto_field("private")) @post("/") async def add_item(data: TodoItem) -> List[TodoItem]: data.super_secret_value = "This is a secret" return [data] app = Litestar(route_handlers=[add_item], plugins=[SQLAlchemySerializationPlugin()]) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_before_send_handler.py�����0000664�0000000�0000000�00000000647�15005643713�0033273�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from litestar import Litestar from litestar.plugins.sqlalchemy import SQLAlchemyInitPlugin, SQLAlchemySyncConfig, sync_autocommit_before_send_handler config = SQLAlchemySyncConfig( connection_string="sqlite:///:memory:", before_send_handler=sync_autocommit_before_send_handler, ) plugin = SQLAlchemyInitPlugin(config=config) app = Litestar(route_handlers=[], plugins=[plugin]) �����������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_dependencies.py������������0000664�0000000�0000000�00000001415�15005643713�0031743�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy import select from litestar import Litestar, post from litestar.plugins.sqlalchemy import SQLAlchemyInitPlugin, SQLAlchemySyncConfig if TYPE_CHECKING: from typing import Tuple from sqlalchemy import Engine from sqlalchemy.orm import Session @post("/", sync_to_thread=True) def handler(db_session: Session, db_engine: Engine) -> Tuple[int, int]: one = db_session.execute(select(1)).scalar() with db_engine.begin() as conn: two = conn.execute(select(2)).scalar() return one, two config = SQLAlchemySyncConfig(connection_string="sqlite:///sync.sqlite") plugin = SQLAlchemyInitPlugin(config=config) app = Litestar(route_handlers=[handler], plugins=[plugin]) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_init_plugin_example.py�����0000664�0000000�0000000�00000002362�15005643713�0033353�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy import select from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, post from litestar.plugins.sqlalchemy import SQLAlchemyInitPlugin, SQLAlchemySyncConfig if TYPE_CHECKING: from typing import Any, Dict, List from sqlalchemy.orm import Session class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_item" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] @post("/", sync_to_thread=True) def add_item(data: Dict[str, Any], db_session: Session) -> List[Dict[str, Any]]: todo_item = TodoItem(**data) with db_session.begin(): db_session.add(todo_item) return [ { "title": item.title, "done": item.done, } for item in db_session.execute(select(TodoItem)).scalars() ] def init_db(app: Litestar) -> None: with config.get_engine().begin() as conn: Base.metadata.create_all(conn) config = SQLAlchemySyncConfig(connection_string="sqlite:///todo_sync.sqlite") plugin = SQLAlchemyInitPlugin(config=config) app = Litestar(route_handlers=[add_item], plugins=[plugin], on_startup=[init_db]) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_plugin_example.py����������0000664�0000000�0000000�00000001714�15005643713�0032330�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from typing import TYPE_CHECKING, Sequence from sqlalchemy import select from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, post from litestar.plugins.sqlalchemy import SQLAlchemyPlugin, SQLAlchemySyncConfig if TYPE_CHECKING: from sqlalchemy.orm import Session class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_item" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] @post("/", sync_to_thread=True) def add_item(data: TodoItem, db_session: Session) -> Sequence[TodoItem]: with db_session.begin(): db_session.add(data) return db_session.execute(select(TodoItem)).scalars().all() config = SQLAlchemySyncConfig(connection_string="sqlite:///todo_sync.sqlite", create_all=True, metadata=Base.metadata) plugin = SQLAlchemyPlugin(config=config) app = Litestar(route_handlers=[add_item], plugins=[plugin]) ����������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_serialization_plugin.py����0000664�0000000�0000000�00000001211�15005643713�0033542�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, post from litestar.plugins.sqlalchemy import SQLAlchemySerializationPlugin if TYPE_CHECKING: from typing import List class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_item" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] @post("/", sync_to_thread=False) def add_item(data: TodoItem) -> List[TodoItem]: return [data] app = Litestar(route_handlers=[add_item], plugins=[SQLAlchemySerializationPlugin()]) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������sqlalchemy_sync_serialization_plugin_marking_fields.py����������������������������������������������0000664�0000000�0000000�00000001454�15005643713�0036532�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000�litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins���������������������������������������������������������������������������������������������������������������from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, post from litestar.dto import dto_field from litestar.plugins.sqlalchemy import SQLAlchemySerializationPlugin if TYPE_CHECKING: from typing import List class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_item" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] super_secret_value: Mapped[str] = mapped_column(info=dto_field("private")) @post("/", sync_to_thread=False) def add_item(data: TodoItem) -> List[TodoItem]: data.super_secret_value = "This is a secret" return [data] app = Litestar(route_handlers=[add_item], plugins=[SQLAlchemySerializationPlugin()]) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/tutorial/����������������������������������0000775�0000000�0000000�00000000000�15005643713�0025327�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/tutorial/__init__.py�����������������������0000664�0000000�0000000�00000000000�15005643713�0027426�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/tutorial/full_app_no_plugins.py������������0000664�0000000�0000000�00000006252�15005643713�0031745�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from contextlib import asynccontextmanager from typing import Any, AsyncGenerator, Dict, List, Optional, Sequence from sqlalchemy import select from sqlalchemy.exc import IntegrityError, NoResultFound from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, get, post, put from litestar.datastructures import State from litestar.exceptions import ClientException, NotFoundException from litestar.status_codes import HTTP_409_CONFLICT TodoType = Dict[str, Any] TodoCollectionType = List[TodoType] class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_items" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] @asynccontextmanager async def db_connection(app: Litestar) -> AsyncGenerator[None, None]: engine = getattr(app.state, "engine", None) if engine is None: engine = create_async_engine("sqlite+aiosqlite:///todo.sqlite", echo=True) app.state.engine = engine async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) try: yield finally: await engine.dispose() sessionmaker = async_sessionmaker(expire_on_commit=False) def serialize_todo(todo: TodoItem) -> TodoType: return {"title": todo.title, "done": todo.done} async def get_todo_by_title(todo_name: str, session: AsyncSession) -> TodoItem: query = select(TodoItem).where(TodoItem.title == todo_name) result = await session.execute(query) try: return result.scalar_one() except NoResultFound as e: raise NotFoundException(detail=f"TODO {todo_name!r} not found") from e async def get_todo_list(done: Optional[bool], session: AsyncSession) -> Sequence[TodoItem]: query = select(TodoItem) if done is not None: query = query.where(TodoItem.done.is_(done)) result = await session.execute(query) return result.scalars().all() @get("/") async def get_list(state: State, done: Optional[bool] = None) -> TodoCollectionType: async with sessionmaker(bind=state.engine) as session: return [serialize_todo(todo) for todo in await get_todo_list(done, session)] @post("/") async def add_item(data: TodoType, state: State) -> TodoType: new_todo = TodoItem(title=data["title"], done=data["done"]) async with sessionmaker(bind=state.engine) as session: try: async with session.begin(): session.add(new_todo) except IntegrityError as e: raise ClientException( status_code=HTTP_409_CONFLICT, detail=f"TODO {new_todo.title!r} already exists", ) from e return serialize_todo(new_todo) @put("/{item_title:str}") async def update_item(item_title: str, data: TodoType, state: State) -> TodoType: async with sessionmaker(bind=state.engine) as session, session.begin(): todo_item = await get_todo_by_title(item_title, session) todo_item.title = data["title"] todo_item.done = data["done"] return serialize_todo(todo_item) app = Litestar([get_list, add_item, update_item], lifespan=[db_connection]) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_init_plugin.py������0000664�0000000�0000000�00000005032�15005643713�0033137�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import AsyncGenerator, List, Optional from sqlalchemy import select from sqlalchemy.exc import IntegrityError, NoResultFound from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, get, post, put from litestar.exceptions import ClientException, NotFoundException from litestar.plugins.sqlalchemy import ( SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin, SQLAlchemySerializationPlugin, ) from litestar.status_codes import HTTP_409_CONFLICT class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_items" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] async def provide_transaction(db_session: AsyncSession) -> AsyncGenerator[AsyncSession, None]: try: async with db_session.begin(): yield db_session except IntegrityError as exc: raise ClientException( status_code=HTTP_409_CONFLICT, detail=str(exc), ) from exc async def get_todo_by_title(todo_name: str, session: AsyncSession) -> TodoItem: query = select(TodoItem).where(TodoItem.title == todo_name) result = await session.execute(query) try: return result.scalar_one() except NoResultFound as e: raise NotFoundException(detail=f"TODO {todo_name!r} not found") from e async def get_todo_list(done: Optional[bool], session: AsyncSession) -> List[TodoItem]: query = select(TodoItem) if done is not None: query = query.where(TodoItem.done.is_(done)) result = await session.execute(query) return list(result.scalars().all()) @get("/") async def get_list(transaction: AsyncSession, done: Optional[bool] = None) -> List[TodoItem]: return await get_todo_list(done, transaction) @post("/") async def add_item(data: TodoItem, transaction: AsyncSession) -> TodoItem: transaction.add(data) return data @put("/{item_title:str}") async def update_item(item_title: str, data: TodoItem, transaction: AsyncSession) -> TodoItem: todo_item = await get_todo_by_title(item_title, transaction) todo_item.title = data.title todo_item.done = data.done return todo_item db_config = SQLAlchemyAsyncConfig( connection_string="sqlite+aiosqlite:///todo.sqlite", metadata=Base.metadata, create_all=True ) app = Litestar( [get_list, add_item, update_item], dependencies={"transaction": provide_transaction}, plugins=[ SQLAlchemySerializationPlugin(), SQLAlchemyInitPlugin(db_config), ], ) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_plugin.py�����������0000664�0000000�0000000�00000004731�15005643713�0032121�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import AsyncGenerator, List, Optional from sqlalchemy import select from sqlalchemy.exc import IntegrityError, NoResultFound from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, get, post, put from litestar.exceptions import ClientException, NotFoundException from litestar.plugins.sqlalchemy import SQLAlchemyAsyncConfig, SQLAlchemyPlugin from litestar.status_codes import HTTP_409_CONFLICT class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_items" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] async def provide_transaction(db_session: AsyncSession) -> AsyncGenerator[AsyncSession, None]: try: async with db_session.begin(): yield db_session except IntegrityError as exc: raise ClientException( status_code=HTTP_409_CONFLICT, detail=str(exc), ) from exc async def get_todo_by_title(todo_name: str, session: AsyncSession) -> TodoItem: query = select(TodoItem).where(TodoItem.title == todo_name) result = await session.execute(query) try: return result.scalar_one() except NoResultFound as e: raise NotFoundException(detail=f"TODO {todo_name!r} not found") from e async def get_todo_list(done: Optional[bool], session: AsyncSession) -> List[TodoItem]: query = select(TodoItem) if done is not None: query = query.where(TodoItem.done.is_(done)) result = await session.execute(query) return list(result.scalars().all()) @get("/") async def get_list(transaction: AsyncSession, done: Optional[bool] = None) -> List[TodoItem]: return await get_todo_list(done, transaction) @post("/") async def add_item(data: TodoItem, transaction: AsyncSession) -> TodoItem: transaction.add(data) return data @put("/{item_title:str}") async def update_item(item_title: str, data: TodoItem, transaction: AsyncSession) -> TodoItem: todo_item = await get_todo_by_title(item_title, transaction) todo_item.title = data.title todo_item.done = data.done return todo_item db_config = SQLAlchemyAsyncConfig( connection_string="sqlite+aiosqlite:///todo.sqlite", metadata=Base.metadata, create_all=True, before_send_handler="autocommit", ) app = Litestar( [get_list, add_item, update_item], dependencies={"transaction": provide_transaction}, plugins=[SQLAlchemyPlugin(db_config)], ) ���������������������������������������full_app_with_serialization_plugin.py���������������������������������������������������������������0000664�0000000�0000000�00000005771�15005643713�0035004�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000�litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/tutorial������������������������������������������������������������������������������������������������������from contextlib import asynccontextmanager from typing import AsyncGenerator, List, Optional from sqlalchemy import select from sqlalchemy.exc import IntegrityError, NoResultFound from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, get, post, put from litestar.datastructures import State from litestar.exceptions import ClientException, NotFoundException from litestar.plugins.sqlalchemy import SQLAlchemySerializationPlugin from litestar.status_codes import HTTP_409_CONFLICT class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_items" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] @asynccontextmanager async def db_connection(app: Litestar) -> AsyncGenerator[None, None]: engine = getattr(app.state, "engine", None) if engine is None: engine = create_async_engine("sqlite+aiosqlite:///todo.sqlite", echo=True) app.state.engine = engine async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) try: yield finally: await engine.dispose() sessionmaker = async_sessionmaker(expire_on_commit=False) async def provide_transaction(state: State) -> AsyncGenerator[AsyncSession, None]: async with sessionmaker(bind=state.engine) as session: try: async with session.begin(): yield session except IntegrityError as exc: raise ClientException( status_code=HTTP_409_CONFLICT, detail=str(exc), ) from exc async def get_todo_by_title(todo_name: str, session: AsyncSession) -> TodoItem: query = select(TodoItem).where(TodoItem.title == todo_name) result = await session.execute(query) try: return result.scalar_one() except NoResultFound as e: raise NotFoundException(detail=f"TODO {todo_name!r} not found") from e async def get_todo_list(done: Optional[bool], session: AsyncSession) -> List[TodoItem]: query = select(TodoItem) if done is not None: query = query.where(TodoItem.done.is_(done)) result = await session.execute(query) return list(result.scalars().all()) @get("/") async def get_list(transaction: AsyncSession, done: Optional[bool] = None) -> List[TodoItem]: return await get_todo_list(done, transaction) @post("/") async def add_item(data: TodoItem, transaction: AsyncSession) -> TodoItem: transaction.add(data) return data @put("/{item_title:str}") async def update_item(item_title: str, data: TodoItem, transaction: AsyncSession) -> TodoItem: todo_item = await get_todo_by_title(item_title, transaction) todo_item.title = data.title todo_item.done = data.done return todo_item app = Litestar( [get_list, add_item, update_item], dependencies={"transaction": provide_transaction}, lifespan=[db_connection], plugins=[SQLAlchemySerializationPlugin()], ) �������litestar-2.16.0/docs/examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_session_di.py�������0000664�0000000�0000000�00000006263�15005643713�0032764�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from contextlib import asynccontextmanager from typing import Any, AsyncGenerator, Dict, List, Optional from sqlalchemy import select from sqlalchemy.exc import IntegrityError, NoResultFound from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, get, post, put from litestar.datastructures import State from litestar.exceptions import ClientException, NotFoundException from litestar.status_codes import HTTP_409_CONFLICT TodoType = Dict[str, Any] TodoCollectionType = List[TodoType] class Base(DeclarativeBase): ... class TodoItem(Base): __tablename__ = "todo_items" title: Mapped[str] = mapped_column(primary_key=True) done: Mapped[bool] @asynccontextmanager async def db_connection(app: Litestar) -> AsyncGenerator[None, None]: engine = getattr(app.state, "engine", None) if engine is None: engine = create_async_engine("sqlite+aiosqlite:///todo.sqlite") app.state.engine = engine async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) try: yield finally: await engine.dispose() sessionmaker = async_sessionmaker(expire_on_commit=False) async def provide_transaction(state: State) -> AsyncGenerator[AsyncSession, None]: async with sessionmaker(bind=state.engine) as session: try: async with session.begin(): yield session except IntegrityError as exc: raise ClientException( status_code=HTTP_409_CONFLICT, detail=str(exc), ) from exc def serialize_todo(todo: TodoItem) -> TodoType: return {"title": todo.title, "done": todo.done} async def get_todo_by_title(todo_name, session: AsyncSession) -> TodoItem: query = select(TodoItem).where(TodoItem.title == todo_name) result = await session.execute(query) try: return result.scalar_one() except NoResultFound as e: raise NotFoundException(detail=f"TODO {todo_name!r} not found") from e async def get_todo_list(done: Optional[bool], session: AsyncSession) -> List[TodoItem]: query = select(TodoItem) if done is not None: query = query.where(TodoItem.done.is_(done)) result = await session.execute(query) return result.scalars().all() @get("/") async def get_list(transaction: AsyncSession, done: Optional[bool] = None) -> TodoCollectionType: return [serialize_todo(todo) for todo in await get_todo_list(done, transaction)] @post("/") async def add_item(data: TodoType, transaction: AsyncSession) -> TodoType: new_todo = TodoItem(title=data["title"], done=data["done"]) transaction.add(new_todo) return serialize_todo(new_todo) @put("/{item_title:str}") async def update_item(item_title: str, data: TodoType, transaction: AsyncSession) -> TodoType: todo_item = await get_todo_by_title(item_title, transaction) todo_item.title = data["title"] todo_item.done = data["done"] return serialize_todo(todo_item) app = Litestar( [get_list, add_item, update_item], dependencies={"transaction": provide_transaction}, lifespan=[db_connection], ) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/sqlalchemy_async_repository.py���������������������0000664�0000000�0000000�00000015731�15005643713�0030222�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from datetime import date from typing import TYPE_CHECKING from uuid import UUID from pydantic import BaseModel as _BaseModel from pydantic import TypeAdapter from sqlalchemy import ForeignKey, select from sqlalchemy.orm import Mapped, mapped_column, relationship, selectinload from litestar import Litestar, get from litestar.controller import Controller from litestar.di import Provide from litestar.handlers.http_handlers.decorators import delete, patch, post from litestar.pagination import OffsetPagination from litestar.params import Parameter from litestar.plugins.sqlalchemy import ( AsyncSessionConfig, SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin, base, filters, repository, ) if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession class BaseModel(_BaseModel): """Extend Pydantic's BaseModel to enable ORM mode""" model_config = {"from_attributes": True} # The SQLAlchemy base includes a declarative model for you to use in your models. # The `UUIDBase` class includes a `UUID` based primary key (`id`) class AuthorModel(base.UUIDBase): # we can optionally provide the table name instead of auto-generating it __tablename__ = "author" # type: ignore[assignment] name: Mapped[str] dob: Mapped[date | None] books: Mapped[list[BookModel]] = relationship(back_populates="author", lazy="noload") # The `UUIDAuditBase` class includes the same UUID` based primary key (`id`) and 2 # additional columns: `created_at` and `updated_at`. `created_at` is a timestamp of when the # record created, and `updated_at` is the last time the record was modified. class BookModel(base.UUIDAuditBase): __tablename__ = "book" # type: ignore[assignment] title: Mapped[str] author_id: Mapped[UUID] = mapped_column(ForeignKey("author.id")) author: Mapped[AuthorModel] = relationship(lazy="joined", innerjoin=True, viewonly=True) # we will explicitly define the schema instead of using DTO objects for clarity. class Author(BaseModel): id: UUID | None name: str dob: date | None = None class AuthorCreate(BaseModel): name: str dob: date | None = None class AuthorUpdate(BaseModel): name: str | None = None dob: date | None = None class AuthorRepository(repository.SQLAlchemyAsyncRepository[AuthorModel]): """Author repository.""" model_type = AuthorModel async def provide_authors_repo(db_session: AsyncSession) -> AuthorRepository: """This provides the default Authors repository.""" return AuthorRepository(session=db_session) # we can optionally override the default `select` used for the repository to pass in # specific SQL options such as join details async def provide_author_details_repo(db_session: AsyncSession) -> AuthorRepository: """This provides a simple example demonstrating how to override the join options for the repository.""" return AuthorRepository( statement=select(AuthorModel).options(selectinload(AuthorModel.books)), session=db_session, ) def provide_limit_offset_pagination( current_page: int = Parameter(ge=1, query="currentPage", default=1, required=False), page_size: int = Parameter( query="pageSize", ge=1, default=10, required=False, ), ) -> filters.LimitOffset: """Add offset/limit pagination. Return type consumed by `Repository.apply_limit_offset_pagination()`. Parameters ---------- current_page : int LIMIT to apply to select. page_size : int OFFSET to apply to select. """ return filters.LimitOffset(page_size, page_size * (current_page - 1)) class AuthorController(Controller): """Author CRUD""" dependencies = {"authors_repo": Provide(provide_authors_repo)} @get(path="/authors") async def list_authors( self, authors_repo: AuthorRepository, limit_offset: filters.LimitOffset, ) -> OffsetPagination[Author]: """List authors.""" results, total = await authors_repo.list_and_count(limit_offset) type_adapter = TypeAdapter(list[Author]) return OffsetPagination[Author]( items=type_adapter.validate_python(results), total=total, limit=limit_offset.limit, offset=limit_offset.offset, ) @post(path="/authors") async def create_author( self, authors_repo: AuthorRepository, data: AuthorCreate, ) -> Author: """Create a new author.""" obj = await authors_repo.add( AuthorModel(**data.model_dump(exclude_unset=True, exclude_none=True)), ) await authors_repo.session.commit() return Author.model_validate(obj) # we override the authors_repo to use the version that joins the Books in @get(path="/authors/{author_id:uuid}", dependencies={"authors_repo": Provide(provide_author_details_repo)}) async def get_author( self, authors_repo: AuthorRepository, author_id: UUID = Parameter( title="Author ID", description="The author to retrieve.", ), ) -> Author: """Get an existing author.""" obj = await authors_repo.get(author_id) return Author.model_validate(obj) @patch( path="/authors/{author_id:uuid}", dependencies={"authors_repo": Provide(provide_author_details_repo)}, ) async def update_author( self, authors_repo: AuthorRepository, data: AuthorUpdate, author_id: UUID = Parameter( title="Author ID", description="The author to update.", ), ) -> Author: """Update an author.""" raw_obj = data.model_dump(exclude_unset=True, exclude_none=True) raw_obj.update({"id": author_id}) obj = await authors_repo.update(AuthorModel(**raw_obj)) await authors_repo.session.commit() return Author.from_orm(obj) @delete(path="/authors/{author_id:uuid}") async def delete_author( self, authors_repo: AuthorRepository, author_id: UUID = Parameter( title="Author ID", description="The author to delete.", ), ) -> None: """Delete a author from the system.""" _ = await authors_repo.delete(author_id) await authors_repo.session.commit() session_config = AsyncSessionConfig(expire_on_commit=False) sqlalchemy_config = SQLAlchemyAsyncConfig( connection_string="sqlite+aiosqlite:///test.sqlite", session_config=session_config ) # Create 'db_session' dependency. sqlalchemy_plugin = SQLAlchemyInitPlugin(config=sqlalchemy_config) async def on_startup() -> None: """Initializes the database.""" async with sqlalchemy_config.get_engine().begin() as conn: await conn.run_sync(base.UUIDBase.metadata.create_all) app = Litestar( route_handlers=[AuthorController], on_startup=[on_startup], plugins=[SQLAlchemyInitPlugin(config=sqlalchemy_config)], dependencies={"limit_offset": Provide(provide_limit_offset_pagination)}, ) ���������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py�������������������0000664�0000000�0000000�00000004572�15005643713�0030435�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations import uuid from datetime import date from typing import List from uuid import UUID from sqlalchemy import ForeignKey, func, select from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession from sqlalchemy.orm import Mapped, mapped_column, relationship from litestar import Litestar, get from litestar.plugins.sqlalchemy import AsyncSessionConfig, SQLAlchemyAsyncConfig, SQLAlchemyPlugin, base # The SQLAlchemy base includes a declarative model for you to use in your models. # The `UUIDBase` class includes a `UUID` based primary key (`id`) class Author(base.UUIDBase): __tablename__ = "author" name: Mapped[str] dob: Mapped[date] books: Mapped[List[Book]] = relationship(back_populates="author", lazy="selectin") # The `UUIDAuditBase` class includes the same UUID` based primary key (`id`) and 2 # additional columns: `created_at` and `updated_at`. `created_at` is a timestamp of when the # record created, and `updated_at` is the last time the record was modified. class Book(base.UUIDAuditBase): __tablename__ = "book" title: Mapped[str] author_id: Mapped[UUID] = mapped_column(ForeignKey("author.id")) author: Mapped[Author] = relationship(lazy="joined", innerjoin=True, viewonly=True) session_config = AsyncSessionConfig(expire_on_commit=False) sqlalchemy_config = SQLAlchemyAsyncConfig( connection_string="sqlite+aiosqlite:///test.sqlite", session_config=session_config, create_all=True ) # Create 'async_session' dependency. async def on_startup(app: Litestar) -> None: """Adds some dummy data if no data is present.""" async with sqlalchemy_config.get_session() as session: statement = select(func.count()).select_from(Author) count = await session.execute(statement) if not count.scalar(): author_id = uuid.uuid4() session.add(Author(name="Stephen King", dob=date(1954, 9, 21), id=author_id)) session.add(Book(title="It", author_id=author_id)) await session.commit() @get(path="/authors") async def get_authors(db_session: AsyncSession, db_engine: AsyncEngine) -> List[Author]: """Interact with SQLAlchemy engine and session.""" return list(await db_session.scalars(select(Author))) app = Litestar( route_handlers=[get_authors], on_startup=[on_startup], debug=True, plugins=[SQLAlchemyPlugin(config=sqlalchemy_config)], ) ��������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/sqlalchemy_repository_bulk_operations.py�����������0000664�0000000�0000000�00000005364�15005643713�0032306�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import json from pathlib import Path from typing import Any from rich import get_console from sqlalchemy import create_engine from sqlalchemy.orm import Mapped, Session, sessionmaker from litestar.plugins.sqlalchemy import base, repository from litestar.repository.filters import LimitOffset here = Path(__file__).parent console = get_console() class USState(base.UUIDBase): # you can optionally override the generated table name by manually setting it. __tablename__ = "us_state_lookup" # type: ignore[assignment] abbreviation: Mapped[str] name: Mapped[str] class USStateRepository(repository.SQLAlchemySyncRepository[USState]): """US State repository.""" model_type = USState engine = create_engine( "duckdb:///:memory:", future=True, ) session_factory: sessionmaker[Session] = sessionmaker(engine, expire_on_commit=False) def open_fixture(fixtures_path: Path, fixture_name: str) -> Any: """Loads JSON file with the specified fixture name Args: fixtures_path (Path): The path to look for fixtures fixture_name (str): The fixture name to load. Raises: FileNotFoundError: Fixtures not found. Returns: Any: The parsed JSON data """ fixture = Path(fixtures_path / f"{fixture_name}.json") if fixture.exists(): with fixture.open(mode="r", encoding="utf-8") as f: f_data = f.read() return json.loads(f_data) raise FileNotFoundError(f"Could not find the {fixture_name} fixture") def run_script() -> None: """Load data from a fixture.""" # Initializes the database. with engine.begin() as conn: USState.metadata.create_all(conn) with session_factory() as db_session: # 1) Load the JSON data into the US States table. repo = USStateRepository(session=db_session) fixture = open_fixture(here, USStateRepository.model_type.__tablename__) # type: ignore objs = repo.add_many([USStateRepository.model_type(**raw_obj) for raw_obj in fixture]) db_session.commit() console.print(f"Created {len(objs)} new objects.") # 2) Select paginated data and total row count. created_objs, total_objs = repo.list_and_count(LimitOffset(limit=10, offset=0)) console.print(f"Selected {len(created_objs)} records out of a total of {total_objs}.") # 3) Let's remove the batch of records selected. deleted_objs = repo.delete_many([new_obj.id for new_obj in created_objs]) console.print(f"Removed {len(deleted_objs)} records out of a total of {total_objs}.") # 4) Let's count the remaining rows remaining_count = repo.count() console.print(f"Found {remaining_count} remaining records after delete.") if __name__ == "__main__": run_script() ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/sqlalchemy_repository_crud.py����������������������0000664�0000000�0000000�00000006533�15005643713�0030042�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from contextlib import asynccontextmanager from datetime import date, datetime from typing import AsyncIterator from uuid import UUID import anyio from rich import get_console from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.orm import Mapped from litestar.plugins.sqlalchemy import base, repository console = get_console() # the SQLAlchemy base includes a declarative model for you to use in your models. # The `Base` class includes a `UUID` based primary key (`id`) class Author(base.UUIDBase): name: Mapped[str] dob: Mapped[date] dod: Mapped[date | None] class AuthorRepository(repository.SQLAlchemyAsyncRepository[Author]): """Author repository.""" model_type = Author engine = create_async_engine( "sqlite+aiosqlite:///test.sqlite", future=True, ) session_factory = async_sessionmaker(engine, expire_on_commit=False) # let's make a simple context manager as an example here. @asynccontextmanager async def repository_factory() -> AsyncIterator[AuthorRepository]: async with session_factory() as db_session: try: yield AuthorRepository(session=db_session) except Exception: # noqa: BLE001 await db_session.rollback() else: await db_session.commit() async def create_author() -> Author: async with repository_factory() as repo: obj = await repo.add( Author( name="F. Scott Fitzgerald", dob=datetime.strptime("1896-09-24", "%Y-%m-%d").date(), ) ) console.print(f"Created Author record for {obj.name} with primary key {obj.id}.") return obj async def update_author(obj: Author) -> Author: async with repository_factory() as repo: obj = await repo.update(obj) console.print(f"Updated Author record for {obj.name} with primary key {obj.id}.") return obj async def remove_author(id: UUID) -> Author: async with repository_factory() as repo: obj = await repo.delete(id) console.print(f"Deleted Author record for {obj.name} with primary key {obj.id}.") return obj async def get_author_if_exists(id: UUID) -> Author | None: async with repository_factory() as repo: obj = await repo.get_one_or_none(id=id) if obj is not None: console.print(f"Found Author record for {obj.name} with primary key {obj.id}.") else: console.print(f"Could not find Author with primary key {id}.") return obj async def run_script() -> None: """Load data from a fixture.""" async with engine.begin() as conn: await conn.run_sync(base.UUIDBase.metadata.create_all) # 1) create a new Author record. console.print("1) Adding a new record") author = await create_author() author_id = author.id # 2) Let's update the Author record. console.print("2) Updating a record.") author.dod = datetime.strptime("1940-12-21", "%Y-%m-%d").date() await update_author(author) # 3) Let's delete the record we just created. console.print("3) Removing a record.") await remove_author(author_id) # 4) Let's verify the record no longer exists. console.print("4) Select one or none.") _should_be_none = await get_author_if_exists(author_id) if __name__ == "__main__": anyio.run(run_script) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/sqlalchemy_repository_extension.py�����������������0000664�0000000�0000000�00000013642�15005643713�0031120�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations import random import re import string import unicodedata from typing import TYPE_CHECKING, Any from uuid import UUID from pydantic import BaseModel as _BaseModel from pydantic import TypeAdapter from sqlalchemy.orm import Mapped, declarative_mixin, mapped_column from sqlalchemy.types import String from litestar import Litestar, get, post from litestar.di import Provide from litestar.plugins.sqlalchemy import ( AsyncSessionConfig, SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin, base, repository, ) if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession class BaseModel(_BaseModel): """Extend Pydantic's BaseModel to enable ORM mode""" model_config = {"from_attributes": True} # we are going to add a simple "slug" to our model that is a URL safe surrogate key to # our database record. @declarative_mixin class SlugKey: """Slug unique Field Model Mixin.""" __abstract__ = True slug: Mapped[str] = mapped_column(String(length=100), nullable=False, unique=True, sort_order=-9) # this class can be re-used with any model that has the `SlugKey` Mixin class SQLAlchemyAsyncSlugRepository(repository.SQLAlchemyAsyncRepository[repository.ModelT]): """Extends the repository to include slug model features..""" async def get_available_slug( self, value_to_slugify: str, **kwargs: Any, ) -> str: """Get a unique slug for the supplied value. If the value is found to exist, a random 4 digit character is appended to the end. There may be a better way to do this, but I wanted to limit the number of additional database calls. Args: value_to_slugify (str): A string that should be converted to a unique slug. **kwargs: stuff Returns: str: a unique slug for the supplied value. This is safe for URLs and other unique identifiers. """ slug = self._slugify(value_to_slugify) if await self._is_slug_unique(slug): return slug # generate a random 4 digit alphanumeric string to make the slug unique and # avoid another DB lookup. random_string = "".join(random.choices(string.ascii_lowercase + string.digits, k=4)) return f"{slug}-{random_string}" @staticmethod def _slugify(value: str) -> str: """slugify. Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated dashes to single dashes. Remove characters that aren't alphanumerics, underscores, or hyphens. Convert to lowercase. Also strip leading and trailing whitespace, dashes, and underscores. Args: value (str): the string to slugify Returns: str: a slugified string of the value parameter """ value = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii") value = re.sub(r"[^\w\s-]", "", value.lower()) return re.sub(r"[-\s]+", "-", value).strip("-_") async def _is_slug_unique( self, slug: str, **kwargs: Any, ) -> bool: return await self.get_one_or_none(slug=slug) is None # The `UUIDAuditBase` class includes the same UUID` based primary key (`id`) and 2 # additional columns: `created_at` and `updated_at`. `created_at` is a timestamp of when the # record created, and `updated_at` is the last time the record was modified. class BlogPost(base.UUIDAuditBase, SlugKey): title: Mapped[str] content: Mapped[str] class BlogPostRepository(SQLAlchemyAsyncSlugRepository[BlogPost]): """Blog Post repository.""" model_type = BlogPost class BlogPostDTO(BaseModel): id: UUID | None slug: str title: str content: str class BlogPostCreate(BaseModel): title: str content: str # we can optionally override the default `select` used for the repository to pass in # specific SQL options such as join details async def provide_blog_post_repo(db_session: AsyncSession) -> BlogPostRepository: """This provides a simple example demonstrating how to override the join options for the repository.""" return BlogPostRepository(session=db_session) session_config = AsyncSessionConfig(expire_on_commit=False) sqlalchemy_config = SQLAlchemyAsyncConfig( connection_string="sqlite+aiosqlite:///test.sqlite", session_config=session_config ) # Create 'async_session' dependency. sqlalchemy_plugin = SQLAlchemyInitPlugin(config=sqlalchemy_config) async def on_startup() -> None: """Initializes the database.""" async with sqlalchemy_config.get_engine().begin() as conn: await conn.run_sync(base.UUIDAuditBase.metadata.create_all) @get(path="/") async def get_blogs( blog_post_repo: BlogPostRepository, ) -> list[BlogPostDTO]: """Interact with SQLAlchemy engine and session.""" objs = await blog_post_repo.list() type_adapter = TypeAdapter(list[BlogPostDTO]) return type_adapter.validate_python(objs) @get(path="/{post_slug:str}") async def get_blog_details( post_slug: str, blog_post_repo: BlogPostRepository, ) -> BlogPostDTO: """Interact with SQLAlchemy engine and session.""" obj = await blog_post_repo.get_one(slug=post_slug) return BlogPostDTO.model_validate(obj) @post(path="/") async def create_blog( blog_post_repo: BlogPostRepository, data: BlogPostCreate, ) -> BlogPostDTO: """Create a new blog post.""" _data = data.model_dump(exclude_unset=True, by_alias=False, exclude_none=True) _data["slug"] = await blog_post_repo.get_available_slug(_data["title"]) obj = await blog_post_repo.add(BlogPost(**_data)) await blog_post_repo.session.commit() return BlogPostDTO.model_validate(obj) app = Litestar( route_handlers=[create_blog, get_blogs, get_blog_details], dependencies={"blog_post_repo": Provide(provide_blog_post_repo, sync_to_thread=False)}, on_startup=[on_startup], plugins=[SQLAlchemyInitPlugin(config=sqlalchemy_config)], ) ����������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/sqlalchemy_sync_repository.py����������������������0000664�0000000�0000000�00000015505�15005643713�0030060�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from datetime import date from typing import TYPE_CHECKING from uuid import UUID from pydantic import BaseModel as _BaseModel from pydantic import TypeAdapter from sqlalchemy import ForeignKey, select from sqlalchemy.orm import Mapped, mapped_column, relationship, selectinload from litestar import Litestar, get from litestar.controller import Controller from litestar.di import Provide from litestar.handlers.http_handlers.decorators import delete, patch, post from litestar.pagination import OffsetPagination from litestar.params import Parameter from litestar.plugins.sqlalchemy import ( SQLAlchemyInitPlugin, SQLAlchemySyncConfig, base, repository, ) from litestar.repository.filters import LimitOffset if TYPE_CHECKING: from sqlalchemy.orm import Session class BaseModel(_BaseModel): """Extend Pydantic's BaseModel to enable ORM mode""" model_config = {"from_attributes": True} # The SQLAlchemy base includes a declarative model for you to use in your models. # The `UUIDBase` class includes a `UUID` based primary key (`id`) class AuthorModel(base.UUIDBase): # we can optionally provide the table name instead of auto-generating it __tablename__ = "author" # type: ignore[assignment] name: Mapped[str] dob: Mapped[date | None] books: Mapped[list[BookModel]] = relationship(back_populates="author", lazy="noload") # The `UUIDAuditBase` class includes the same UUID` based primary key (`id`) and 2 # additional columns: `created_at` and `updated_at`. `created_at` is a timestamp of when the # record created, and `updated_at` is the last time the record was modified. class BookModel(base.UUIDAuditBase): __tablename__ = "book" # type: ignore[assignment] title: Mapped[str] author_id: Mapped[UUID] = mapped_column(ForeignKey("author.id")) author: Mapped[AuthorModel] = relationship(lazy="joined", innerjoin=True, viewonly=True) # we will explicitly define the schema instead of using DTO objects for clarity. class Author(BaseModel): id: UUID | None name: str dob: date | None = None class AuthorCreate(BaseModel): name: str dob: date | None = None class AuthorUpdate(BaseModel): name: str | None = None dob: date | None = None class AuthorRepository(repository.SQLAlchemySyncRepository[AuthorModel]): """Author repository.""" model_type = AuthorModel async def provide_authors_repo(db_session: Session) -> AuthorRepository: """This provides the default Authors repository.""" return AuthorRepository(session=db_session) # we can optionally override the default `select` used for the repository to pass in # specific SQL options such as join details async def provide_author_details_repo(db_session: Session) -> AuthorRepository: """This provides a simple example demonstrating how to override the join options for the repository.""" return AuthorRepository( statement=select(AuthorModel).options(selectinload(AuthorModel.books)), session=db_session, ) def provide_limit_offset_pagination( current_page: int = Parameter(ge=1, query="currentPage", default=1, required=False), page_size: int = Parameter( query="pageSize", ge=1, default=10, required=False, ), ) -> LimitOffset: """Add offset/limit pagination. Return type consumed by `Repository.apply_limit_offset_pagination()`. Parameters ---------- current_page : int LIMIT to apply to select. page_size : int OFFSET to apply to select. """ return LimitOffset(page_size, page_size * (current_page - 1)) class AuthorController(Controller): """Author CRUD""" dependencies = {"authors_repo": Provide(provide_authors_repo, sync_to_thread=False)} @get(path="/authors") def list_authors( self, authors_repo: AuthorRepository, limit_offset: LimitOffset, ) -> OffsetPagination[Author]: """List authors.""" results, total = authors_repo.list_and_count(limit_offset) type_adapter = TypeAdapter(list[Author]) return OffsetPagination[Author]( items=type_adapter.validate_python(results), total=total, limit=limit_offset.limit, offset=limit_offset.offset, ) @post(path="/authors") def create_author( self, authors_repo: AuthorRepository, data: AuthorCreate, ) -> Author: """Create a new author.""" obj = authors_repo.add( AuthorModel(**data.model_dump(exclude_unset=True, exclude_none=True)), ) authors_repo.session.commit() return Author.model_validate(obj) # we override the authors_repo to use the version that joins the Books in @get( path="/authors/{author_id:uuid}", dependencies={"authors_repo": Provide(provide_author_details_repo, sync_to_thread=False)}, ) def get_author( self, authors_repo: AuthorRepository, author_id: UUID = Parameter( title="Author ID", description="The author to retrieve.", ), ) -> Author: """Get an existing author.""" obj = authors_repo.get(author_id) return Author.model_validate(obj) @patch( path="/authors/{author_id:uuid}", dependencies={"authors_repo": Provide(provide_author_details_repo, sync_to_thread=False)}, ) def update_author( self, authors_repo: AuthorRepository, data: AuthorUpdate, author_id: UUID = Parameter( title="Author ID", description="The author to update.", ), ) -> Author: """Update an author.""" raw_obj = data.model_dump(exclude_unset=True, exclude_none=True) raw_obj.update({"id": author_id}) obj = authors_repo.update(AuthorModel(**raw_obj)) authors_repo.session.commit() return Author.model_validate(obj) @delete(path="/authors/{author_id:uuid}") def delete_author( self, authors_repo: AuthorRepository, author_id: UUID = Parameter( title="Author ID", description="The author to delete.", ), ) -> None: """Delete a author from the system.""" _ = authors_repo.delete(author_id) authors_repo.session.commit() sqlalchemy_config = SQLAlchemySyncConfig(connection_string="sqlite:///test.sqlite") # Create 'db_session' dependency. sqlalchemy_plugin = SQLAlchemyInitPlugin(config=sqlalchemy_config) def on_startup() -> None: """Initializes the database.""" with sqlalchemy_config.get_engine().begin() as conn: base.UUIDBase.metadata.create_all(conn) app = Litestar( route_handlers=[AuthorController], on_startup=[on_startup], plugins=[SQLAlchemyInitPlugin(config=sqlalchemy_config)], dependencies={"limit_offset": Provide(provide_limit_offset_pagination)}, ) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/contrib/sqlalchemy/us_state_lookup.json�������������������������������0000664�0000000�0000000�00000006135�15005643713�0026123�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[ { "name": "Alabama", "abbreviation": "AL" }, { "name": "Alaska", "abbreviation": "AK" }, { "name": "Arizona", "abbreviation": "AZ" }, { "name": "Arkansas", "abbreviation": "AR" }, { "name": "California", "abbreviation": "CA" }, { "name": "Colorado", "abbreviation": "CO" }, { "name": "Connecticut", "abbreviation": "CT" }, { "name": "Delaware", "abbreviation": "DE" }, { "name": "District Of Columbia", "abbreviation": "DC" }, { "name": "Florida", "abbreviation": "FL" }, { "name": "Georgia", "abbreviation": "GA" }, { "name": "Guam", "abbreviation": "GU" }, { "name": "Hawaii", "abbreviation": "HI" }, { "name": "Idaho", "abbreviation": "ID" }, { "name": "Illinois", "abbreviation": "IL" }, { "name": "Indiana", "abbreviation": "IN" }, { "name": "Iowa", "abbreviation": "IA" }, { "name": "Kansas", "abbreviation": "KS" }, { "name": "Kentucky", "abbreviation": "KY" }, { "name": "Louisiana", "abbreviation": "LA" }, { "name": "Maine", "abbreviation": "ME" }, { "name": "Maryland", "abbreviation": "MD" }, { "name": "Massachusetts", "abbreviation": "MA" }, { "name": "Michigan", "abbreviation": "MI" }, { "name": "Minnesota", "abbreviation": "MN" }, { "name": "Mississippi", "abbreviation": "MS" }, { "name": "Missouri", "abbreviation": "MO" }, { "name": "Montana", "abbreviation": "MT" }, { "name": "Nebraska", "abbreviation": "NE" }, { "name": "Nevada", "abbreviation": "NV" }, { "name": "New Hampshire", "abbreviation": "NH" }, { "name": "New Jersey", "abbreviation": "NJ" }, { "name": "New Mexico", "abbreviation": "NM" }, { "name": "New York", "abbreviation": "NY" }, { "name": "North Carolina", "abbreviation": "NC" }, { "name": "North Dakota", "abbreviation": "ND" }, { "name": "Ohio", "abbreviation": "OH" }, { "name": "Oklahoma", "abbreviation": "OK" }, { "name": "Oregon", "abbreviation": "OR" }, { "name": "Palau", "abbreviation": "PW" }, { "name": "Pennsylvania", "abbreviation": "PA" }, { "name": "Puerto Rico", "abbreviation": "PR" }, { "name": "Rhode Island", "abbreviation": "RI" }, { "name": "South Carolina", "abbreviation": "SC" }, { "name": "South Dakota", "abbreviation": "SD" }, { "name": "Tennessee", "abbreviation": "TN" }, { "name": "Texas", "abbreviation": "TX" }, { "name": "Utah", "abbreviation": "UT" }, { "name": "Vermont", "abbreviation": "VT" }, { "name": "Virginia", "abbreviation": "VA" }, { "name": "Washington", "abbreviation": "WA" }, { "name": "West Virginia", "abbreviation": "WV" }, { "name": "Wisconsin", "abbreviation": "WI" }, { "name": "Wyoming", "abbreviation": "WY" } ] �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0022527�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/__init__.py�������������������������������������0000664�0000000�0000000�00000000000�15005643713�0024626�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/defining_dtos_on_layers.py����������������������0000664�0000000�0000000�00000002537�15005643713�0027777�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass, field from typing import List from uuid import UUID, uuid4 from litestar import Controller, delete, get, post, put from litestar.app import Litestar from litestar.dto import DataclassDTO from litestar.dto.config import DTOConfig @dataclass class User: name: str email: str age: int id: UUID = field(default_factory=uuid4) class UserWriteDTO(DataclassDTO[User]): config = DTOConfig(exclude={"id"}) class UserReadDTO(DataclassDTO[User]): ... class UserController(Controller): dto = UserWriteDTO return_dto = UserReadDTO @post("/", sync_to_thread=False) def create_user(self, data: User) -> User: return data @get("/", sync_to_thread=False) def get_users(self) -> List[User]: return [User(name="Mr Sunglass", email="mr.sunglass@example.com", age=30)] @get("/{user_id:uuid}", sync_to_thread=False) def get_user(self, user_id: UUID) -> User: return User(id=user_id, name="Mr Sunglass", email="mr.sunglass@example.com", age=30) @put("/{user_id:uuid}", sync_to_thread=False) def update_user(self, data: User) -> User: return data @delete("/{user_id:uuid}", return_dto=None, sync_to_thread=False) def delete_user(self, user_id: UUID) -> None: return None app = Litestar([UserController]) �����������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/����������������������������������������0000775�0000000�0000000�00000000000�15005643713�0024176�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/__init__.py�����������������������������0000664�0000000�0000000�00000000000�15005643713�0026275�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/dto_data_problem_statement.py�����������0000664�0000000�0000000�00000001671�15005643713�0032140�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass, field from uuid import UUID, uuid4 from litestar import Litestar, post from litestar.dto import DataclassDTO, DTOConfig @dataclass class User: name: str email: str age: int id: UUID = field(default_factory=uuid4) class UserWriteDTO(DataclassDTO[User]): """Don't allow client to set the id.""" config = DTOConfig(exclude={"id"}) # We need a dto for the handler to parse the request data per the configuration, however, # we don't need a return DTO as we are returning a dataclass, and Litestar already knows # how to serialize dataclasses. @post("/users", dto=UserWriteDTO, return_dto=None, sync_to_thread=False) def create_user(data: User) -> User: """Create an user.""" return data app = Litestar(route_handlers=[create_user]) # run: /users -H "Content-Type: application/json" -d '{"name":"Peter","email": "peter@example.com", "age":41}' �����������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/dto_data_usage.py�����������������������0000664�0000000�0000000�00000001313�15005643713�0027511�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from dataclasses import dataclass from uuid import UUID, uuid4 from litestar import Litestar, post from litestar.dto import DataclassDTO, DTOConfig, DTOData @dataclass class User: name: str email: str age: int id: UUID class UserWriteDTO(DataclassDTO[User]): """Don't allow client to set the id.""" config = DTOConfig(exclude={"id"}) @post("/users", dto=UserWriteDTO, return_dto=None, sync_to_thread=False) def create_user(data: DTOData[User]) -> User: """Create an user.""" return data.create_instance(id=uuid4()) app = Litestar(route_handlers=[create_user]) # run: /users -H "Content-Type: application/json" -d '{"name":"Peter", "email": "peter@example.com", "age":41}' ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/enveloping_return_data.py���������������0000664�0000000�0000000�00000001712�15005643713�0031307�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from dataclasses import dataclass from datetime import datetime from typing import Generic, List, TypeVar from sqlalchemy.orm import Mapped from litestar import Litestar, get from litestar.dto import DTOConfig from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base T = TypeVar("T") @dataclass class WithCount(Generic[T]): count: int data: List[T] class User(Base): name: Mapped[str] password: Mapped[str] created_at: Mapped[datetime] class UserDTO(SQLAlchemyDTO[User]): config = DTOConfig(exclude={"password", "created_at"}) @get("/users", dto=UserDTO, sync_to_thread=False) def get_users() -> WithCount[User]: return WithCount( count=1, data=[ User( id=1, name="Litestar User", password="xyz", created_at=datetime.now(), ), ], ) app = Litestar(route_handlers=[get_users]) # run: /users ������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/excluding_fields.py���������������������0000664�0000000�0000000�00000003351�15005643713�0030062�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from datetime import datetime from typing import List from uuid import UUID from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship from typing_extensions import Annotated from litestar import Litestar, post from litestar.dto import DTOConfig, dto_field from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base class Address(Base): street: Mapped[str] city: Mapped[str] state: Mapped[str] zip: Mapped[str] class Pets(Base): name: Mapped[str] user_id: Mapped[UUID] = mapped_column(ForeignKey("user.id")) class User(Base): name: Mapped[str] password: Mapped[str] = mapped_column(info=dto_field("private")) created_at: Mapped[datetime] = mapped_column(info=dto_field("read-only")) address_id: Mapped[UUID] = mapped_column(ForeignKey("address.id"), info=dto_field("private")) address: Mapped[Address] = relationship(info=dto_field("read-only")) pets: Mapped[List[Pets]] = relationship(info=dto_field("read-only")) UserDTO = SQLAlchemyDTO[User] config = DTOConfig( exclude={ "id", "address.id", "address.street", "pets.0.id", "pets.0.user_id", } ) ReadUserDTO = SQLAlchemyDTO[Annotated[User, config]] @post("/users", dto=UserDTO, return_dto=ReadUserDTO, sync_to_thread=False) def create_user(data: User) -> User: data.created_at = datetime.min data.address = Address(street="123 Main St", city="Anytown", state="NY", zip="12345") data.pets = [Pets(id=1, name="Fido"), Pets(id=2, name="Spot")] return data app = Litestar(route_handlers=[create_user]) # run: /users -H "Content-Type: application/json" -d '{"name":"Litestar User","password":"xyz","created_at":"2023-04-24T00:00:00Z"}' ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/included_fields.py����������������������0000664�0000000�0000000�00000003255�15005643713�0027672�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from datetime import datetime from typing import List from uuid import UUID from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship from typing_extensions import Annotated from litestar import Litestar, post from litestar.dto import DTOConfig, dto_field from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base class Address(Base): street: Mapped[str] city: Mapped[str] state: Mapped[str] zip: Mapped[str] class Pets(Base): name: Mapped[str] user_id: Mapped[UUID] = mapped_column(ForeignKey("user.id")) class User(Base): name: Mapped[str] password: Mapped[str] = mapped_column(info=dto_field("private")) created_at: Mapped[datetime] = mapped_column(info=dto_field("read-only")) address_id: Mapped[UUID] = mapped_column(ForeignKey("address.id"), info=dto_field("private")) address: Mapped[Address] = relationship(info=dto_field("read-only")) pets: Mapped[List[Pets]] = relationship(info=dto_field("read-only")) UserDTO = SQLAlchemyDTO[User] config = DTOConfig( include={ "address.street", "pets.0.name", } ) ReadUserDTO = SQLAlchemyDTO[Annotated[User, config]] @post("/users", dto=UserDTO, return_dto=ReadUserDTO, sync_to_thread=False) def create_user(data: User) -> User: data.created_at = datetime.min data.address = Address(street="123 Main St", city="Anytown", state="NY", zip="12345") data.pets = [Pets(id=1, name="Fido"), Pets(id=2, name="Spot")] return data app = Litestar(route_handlers=[create_user]) # run: /users -H "Content-Type: application/json" -d '{"name":"Litestar User","password":"xyz","created_at":"2023-04-24T00:00:00Z"}' ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/leading_underscore_private.py�����������0000664�0000000�0000000�00000000635�15005643713�0032142�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from dataclasses import dataclass from litestar import Litestar, post from litestar.dto import DataclassDTO @dataclass class Foo: this_will: str _this_will: str = "Mars" @post("/", dto=DataclassDTO[Foo], sync_to_thread=False) def handler(data: Foo) -> Foo: return data app = Litestar(route_handlers=[handler]) # run: / -H "Content-Type: application/json" -d '{"bar":"stay","_baz":"go_away!"}' ���������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/leading_underscore_private_override.py��0000664�0000000�0000000�00000001012�15005643713�0034027�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from dataclasses import dataclass from litestar import Litestar, post from litestar.dto import DataclassDTO, DTOConfig @dataclass class Foo: this_will: str _this_will: str = "not_go_away!" class DTO(DataclassDTO[Foo]): config = DTOConfig(underscore_fields_private=False) @post("/", dto=DTO, sync_to_thread=False) def handler(data: Foo) -> Foo: return data app = Litestar(route_handlers=[handler]) # run: / -H "Content-Type: application/json" -d '{"this_will":"stay","_this_will":"not_go_away!"}' ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/marking_fields.py�����������������������0000664�0000000�0000000�00000002436�15005643713�0027533�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from datetime import datetime from sqlalchemy.orm import Mapped, mapped_column from litestar import Litestar, post from litestar.dto import dto_field from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base class User(Base): # `Base` defines `id` field as: # id: Mapped[UUID] = mapped_column(default=uuid4, primary_key=True) name: Mapped[str] password: Mapped[str] = mapped_column(info=dto_field("private")) created_at: Mapped[datetime] = mapped_column(info=dto_field("read-only")) UserDTO = SQLAlchemyDTO[User] @post("/users", dto=UserDTO, sync_to_thread=False) def create_user(data: User) -> User: # even though the client did not send the id field, # since it is a primary key it is autogenerated assert "id" in vars(data) # even though the client sent the password and created_at field, it is not in the data object assert "password" not in vars(data) assert "created_at" not in vars(data) # normally the database would set the created_at timestamp data.created_at = datetime.min return data # the response includes the created_at field app = Litestar(route_handlers=[create_user]) # run: /users -H "Content-Type: application/json" -d '{"name":"Litestar User","password":"xyz","created_at":"2023-04-24T00:00:00Z"}' ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/my_lib.py�������������������������������0000664�0000000�0000000�00000001166�15005643713�0026027�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from typing import Any from sqlalchemy.orm import DeclarativeBase from litestar.plugins.sqlalchemy import base, mixins class _Base(base.CommonTableAttributes, mixins.UUIDPrimaryKey, DeclarativeBase): """Fake base SQLAlchemy model for typing purposes.""" Base: _Base def __getattr__(name: str) -> Any: if name == "Base": return type( "Base", (base.CommonTableAttributes, mixins.UUIDPrimaryKey, DeclarativeBase), {"registry": base.create_registry()}, ) raise AttributeError(f"module {__name__!r} has no attribute {name!r}") ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/paginated_return_data.py����������������0000664�0000000�0000000�00000001630�15005643713�0031074�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from datetime import datetime from sqlalchemy.orm import Mapped from litestar import Litestar, get from litestar.dto import DTOConfig from litestar.pagination import ClassicPagination from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base class User(Base): name: Mapped[str] password: Mapped[str] created_at: Mapped[datetime] class UserDTO(SQLAlchemyDTO[User]): config = DTOConfig(exclude={"password", "created_at"}) @get("/users", dto=UserDTO, sync_to_thread=False) def get_users() -> ClassicPagination[User]: return ClassicPagination( page_size=10, total_pages=1, current_page=1, items=[ User( id=1, name="Litestar User", password="xyz", created_at=datetime.now(), ), ], ) app = Litestar(route_handlers=[get_users]) # run: /users ��������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/patch_requests.py�����������������������0000664�0000000�0000000�00000001713�15005643713�0027604�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from uuid import UUID from litestar import Litestar, patch from litestar.dto import DataclassDTO, DTOConfig, DTOData @dataclass class Person: id: UUID name: str age: int class PatchDTO(DataclassDTO[Person]): """Don't allow client to set the id, and allow partial updates.""" config = DTOConfig(exclude={"id"}, partial=True) peter_uuid = UUID("f32ff2ce-e32f-4537-9dc0-26e7599f1380") database = {peter_uuid: Person(id=peter_uuid, name="Peter", age=40)} @patch("/person/{person_id:uuid}", dto=PatchDTO, return_dto=None, sync_to_thread=False) def update_person(person_id: UUID, data: DTOData[Person]) -> Person: """Partially update a person.""" return data.update_instance(database[person_id]) app = Litestar(route_handlers=[update_person]) # run: /person/f32ff2ce-e32f-4537-9dc0-26e7599f1380 -X PATCH -H "Content-Type: application/json" -d '{"name":"Peter Pan"}' �����������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/providing_values_for_nested_data.py�����0000664�0000000�0000000�00000001537�15005643713�0033337�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from litestar import Litestar, post from litestar.dto import DataclassDTO, DTOConfig, DTOData @dataclass class Address: id: int street: str @dataclass class Person: id: int name: str age: int address: Address class ReadDTO(DataclassDTO[Person]): config = DTOConfig() class WriteDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"id", "address.id"}) @post("/person", dto=WriteDTO, return_dto=ReadDTO, sync_to_thread=False) def create_person(data: DTOData[Person]) -> Person: # Logic for persisting the person goes here return data.create_instance(id=1, address__id=2) app = Litestar(route_handlers=[create_person]) # run: /person -H "Content-Type: application/json" -d '{"name":"Peter","age":41, "address": {"street": "Fake Street"}}' �����������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/related_items.py������������������������0000664�0000000�0000000�00000002606�15005643713�0027375�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from uuid import UUID from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship from typing_extensions import Annotated from litestar import Litestar, put from litestar.dto import DTOConfig from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base class A(Base): b_id: Mapped[UUID] = mapped_column(ForeignKey("b.id")) b: Mapped[B] = relationship(back_populates="a") class B(Base): a: Mapped[A] = relationship(back_populates="b") data_config = DTOConfig(max_nested_depth=0) DataDTO = SQLAlchemyDTO[Annotated[A, data_config]] # default config sets max_nested_depth to 1 ReturnDTO = SQLAlchemyDTO[A] @put("/a", dto=DataDTO, return_dto=ReturnDTO, sync_to_thread=False) def update_a(data: A) -> A: # this shows that "b" was not parsed out of the inbound data assert "b" not in vars(data) # Now we'll create an instance of B and assign it" # This includes a reference back to ``a`` which is not serialized in the return data # because default ``max_nested_depth`` is set to 1 data.b = B(id=data.b_id, a=data) return data app = Litestar(route_handlers=[update_a]) # run: /a -H "Content-Type: application/json" -X PUT -d '{"id": "6955e63c-c2bc-4707-8fa4-2144d1764746", "b_id": "9cf3518d-7e19-4215-9ec2-e056cac55bf7", "b": {"id": "9cf3518d-7e19-4215-9ec2-e056cac55bf7"}}' ��������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/renaming_all_fields.py������������������0000664�0000000�0000000�00000002034�15005643713�0030525�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from datetime import datetime from sqlalchemy.orm import Mapped, mapped_column from typing_extensions import Annotated from litestar import Litestar, post from litestar.dto import DTOConfig, dto_field from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base class User(Base): first_name: Mapped[str] password: Mapped[str] = mapped_column(info=dto_field("private")) created_at: Mapped[datetime] = mapped_column(info=dto_field("read-only")) config = DTOConfig(rename_strategy="camel") # another rename strategy with a custom callback: # config = DTOConfig(rename_strategy=lambda x: f"-{x}-") UserDTO = SQLAlchemyDTO[Annotated[User, config]] @post("/users", dto=UserDTO, sync_to_thread=False) def create_user(data: User) -> User: assert data.first_name == "Litestar User" data.created_at = datetime.min return data app = Litestar(route_handlers=[create_user]) # run: /users -H "Content-Type: application/json" -d '{"firstName":"Litestar User","password":"xyz","createdAt":"2023-04-24T00:00:00Z"}' ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/renaming_fields.py����������������������0000664�0000000�0000000�00000001660�15005643713�0027701�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from datetime import datetime from sqlalchemy.orm import Mapped, mapped_column from typing_extensions import Annotated from litestar import Litestar, post from litestar.dto import DTOConfig, dto_field from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base class User(Base): name: Mapped[str] password: Mapped[str] = mapped_column(info=dto_field("private")) created_at: Mapped[datetime] = mapped_column(info=dto_field("read-only")) config = DTOConfig(rename_fields={"name": "userName"}) UserDTO = SQLAlchemyDTO[Annotated[User, config]] @post("/users", dto=UserDTO, sync_to_thread=False) def create_user(data: User) -> User: assert data.name == "Litestar User" data.created_at = datetime.min return data app = Litestar(route_handlers=[create_user]) # run: /users -H "Content-Type: application/json" -d '{"userName":"Litestar User","password":"xyz","created_at":"2023-04-24T00:00:00Z"}' ��������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/response_return_data.py�����������������0000664�0000000�0000000�00000001426�15005643713�0031001�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from datetime import datetime from sqlalchemy.orm import Mapped from litestar import Litestar, Response, get from litestar.dto import DTOConfig from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base class User(Base): name: Mapped[str] password: Mapped[str] created_at: Mapped[datetime] class UserDTO(SQLAlchemyDTO[User]): config = DTOConfig(exclude={"password", "created_at"}) @get("/users", dto=UserDTO, sync_to_thread=False) def get_users() -> Response[User]: return Response( content=User( id=1, name="Litestar User", password="xyz", created_at=datetime.now(), ), headers={"X-Total-Count": "1"}, ) app = Litestar(route_handlers=[get_users]) # run: /users ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/simple_dto_factory_example.py�����������0000664�0000000�0000000�00000001306�15005643713�0032151�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from datetime import datetime from sqlalchemy.orm import Mapped from litestar import Litestar, post from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base class User(Base): # `Base` defines `id` field as: # id: Mapped[UUID] = mapped_column(default=uuid4, primary_key=True) name: Mapped[str] password: Mapped[str] created_at: Mapped[datetime] UserDTO = SQLAlchemyDTO[User] @post("/users", dto=UserDTO, sync_to_thread=False) def create_user(data: User) -> User: return data app = Litestar(route_handlers=[create_user]) # run: /users -H "Content-Type: application/json" -d '{"name":"Litestar User","password":"xyz","created_at":"2023-04-24T00:00:00Z"}' ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/�������������������������������0000775�0000000�0000000�00000000000�15005643713�0026041�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/__init__.py��������������������0000664�0000000�0000000�00000000000�15005643713�0030140�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/controller.py������������������0000664�0000000�0000000�00000003016�15005643713�0030576�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from litestar import Controller, Litestar, patch, post, put from litestar.dto import DataclassDTO, DTOConfig, DTOData @dataclass class Person: name: str age: int email: str id: int class ReadDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"email"}) class WriteDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"id"}) class PatchDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"id"}, partial=True) class PersonController(Controller): dto = WriteDTO return_dto = ReadDTO @post("/person", sync_to_thread=False) def create_person(self, data: DTOData[Person]) -> Person: # Logic for persisting the person goes here return data.create_instance(id=1) @put("/person/{person_id:int}", sync_to_thread=False) def update_person(self, person_id: int, data: DTOData[Person]) -> Person: # Usually the Person would be retrieved from a database person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com") return data.update_instance(person) @patch("/person/{person_id:int}", dto=PatchDTO, sync_to_thread=False) def patch_person(self, person_id: int, data: DTOData[Person]) -> Person: # Usually the Person would be retrieved from a database person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com") return data.update_instance(person) app = Litestar(route_handlers=[PersonController]) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/dto_data.py��������������������0000664�0000000�0000000�00000001234�15005643713�0030172�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from litestar import Litestar, post from litestar.dto import DataclassDTO, DTOConfig, DTOData @dataclass class Person: name: str age: int email: str id: int class ReadDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"email"}) class WriteDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"id"}) @post("/person", dto=WriteDTO, return_dto=ReadDTO, sync_to_thread=False) def create_person(data: DTOData[Person]) -> Person: # Logic for persisting the person goes here return data.create_instance(id=1) app = Litestar(route_handlers=[create_person]) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/explicit_field_renaming.py�����0000664�0000000�0000000�00000002432�15005643713�0033260�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from typing import List from litestar import Litestar, get from litestar.dto import DataclassDTO, DTOConfig @dataclass class Address: street: str city: str country: str @dataclass class Person: name: str age: int email: str address: Address children: List[Person] class ReadDTO(DataclassDTO[Person]): config = DTOConfig( exclude={"email", "address.street", "children.0.email", "children.0.address"}, rename_fields={"address": "location"}, ) @get("/person/{name:str}", return_dto=ReadDTO, sync_to_thread=False) def get_person(name: str) -> Person: # Your logic to retrieve the person goes here # For demonstration purposes, a placeholder Person instance is returned address = Address(street="123 Main St", city="Cityville", country="Countryland") child1 = Person(name="Child1", age=10, email="child1@example.com", address=address, children=[]) child2 = Person(name="Child2", age=8, email="child2@example.com", address=address, children=[]) return Person( name=name, age=30, email=f"email_of_{name}@example.com", address=address, children=[child1, child2], ) app = Litestar(route_handlers=[get_person]) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/field_renaming_strategy.py�����0000664�0000000�0000000�00000002414�15005643713�0033301�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from typing import List from litestar import Litestar, get from litestar.dto import DataclassDTO, DTOConfig @dataclass class Address: street: str city: str country: str @dataclass class Person: name: str age: int email: str address: Address children: List[Person] class ReadDTO(DataclassDTO[Person]): config = DTOConfig( exclude={"email", "address.street", "children.0.email", "children.0.address"}, rename_strategy="upper", ) @get("/person/{name:str}", return_dto=ReadDTO, sync_to_thread=False) def get_person(name: str) -> Person: # Your logic to retrieve the person goes here # For demonstration purposes, a placeholder Person instance is returned address = Address(street="123 Main St", city="Cityville", country="Countryland") child1 = Person(name="Child1", age=10, email="child1@example.com", address=address, children=[]) child2 = Person(name="Child2", age=8, email="child2@example.com", address=address, children=[]) return Person( name=name, age=30, email=f"email_of_{name}@example.com", address=address, children=[child1, child2], ) app = Litestar(route_handlers=[get_person]) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/initial_pattern.py�������������0000664�0000000�0000000�00000000776�15005643713�0031613�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from litestar import Litestar, get @dataclass class Person: name: str age: int email: str @get("/person/{name:str}", sync_to_thread=False) def get_person(name: str) -> Person: # Your logic to retrieve the person goes here # For demonstration purposes, a placeholder Person instance is returned return Person(name=name, age=30, email=f"email_of_{name}@example.com") app = Litestar(route_handlers=[get_person]) ��litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/max_nested_depth.py������������0000664�0000000�0000000�00000002407�15005643713�0031731�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from typing import List from litestar import Litestar, get from litestar.dto import DataclassDTO, DTOConfig @dataclass class Address: street: str city: str country: str @dataclass class Person: name: str age: int email: str address: Address children: List[Person] class ReadDTO(DataclassDTO[Person]): config = DTOConfig( exclude={"email", "address.street", "children.0.email", "children.0.address"}, max_nested_depth=2, ) @get("/person/{name:str}", return_dto=ReadDTO, sync_to_thread=False) def get_person(name: str) -> Person: # Your logic to retrieve the person goes here # For demonstration purposes, a placeholder Person instance is returned address = Address(street="123 Main St", city="Cityville", country="Countryland") child1 = Person(name="Child1", age=10, email="child1@example.com", address=address, children=[]) child2 = Person(name="Child2", age=8, email="child2@example.com", address=address, children=[]) return Person( name=name, age=30, email=f"email_of_{name}@example.com", address=address, children=[child1, child2], ) app = Litestar(route_handlers=[get_person]) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/multiple_handlers.py�����������0000664�0000000�0000000�00000002733�15005643713�0032133�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from litestar import Litestar, patch, post, put from litestar.dto import DataclassDTO, DTOConfig, DTOData @dataclass class Person: name: str age: int email: str id: int class ReadDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"email"}) class WriteDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"id"}) class PatchDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"id"}, partial=True) @post("/person", dto=WriteDTO, return_dto=ReadDTO, sync_to_thread=False) def create_person(data: DTOData[Person]) -> Person: # Logic for persisting the person goes here return data.create_instance(id=1) @put("/person/{person_id:int}", dto=WriteDTO, return_dto=ReadDTO, sync_to_thread=False) def update_person(person_id: int, data: DTOData[Person]) -> Person: # Usually the Person would be retrieved from a database person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com") return data.update_instance(person) @patch("/person/{person_id:int}", dto=PatchDTO, return_dto=ReadDTO, sync_to_thread=False) def patch_person(person_id: int, data: DTOData[Person]) -> Person: # Usually the Person would be retrieved from a database person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com") return data.update_instance(person) app = Litestar(route_handlers=[create_person, update_person, patch_person]) �������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/nested_collection_exclude.py���0000664�0000000�0000000�00000002334�15005643713�0033623�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from typing import List from litestar import Litestar, get from litestar.dto import DataclassDTO, DTOConfig @dataclass class Address: street: str city: str country: str @dataclass class Person: name: str age: int email: str address: Address children: List[Person] class ReadDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"email", "address.street", "children.0.email", "children.0.address"}) @get("/person/{name:str}", return_dto=ReadDTO, sync_to_thread=False) def get_person(name: str) -> Person: # Your logic to retrieve the person goes here # For demonstration purposes, a placeholder Person instance is returned address = Address(street="123 Main St", city="Cityville", country="Countryland") child1 = Person(name="Child1", age=10, email="child1@example.com", address=address, children=[]) child2 = Person(name="Child2", age=8, email="child2@example.com", address=address, children=[]) return Person( name=name, age=30, email=f"email_of_{name}@example.com", address=address, children=[child1, child2], ) app = Litestar(route_handlers=[get_person]) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/nested_exclude.py��������������0000664�0000000�0000000�00000001554�15005643713�0031413�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from litestar import Litestar, get from litestar.dto import DataclassDTO, DTOConfig @dataclass class Address: street: str city: str country: str @dataclass class Person: name: str age: int email: str address: Address class ReadDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"email", "address.street"}) @get("/person/{name:str}", return_dto=ReadDTO, sync_to_thread=False) def get_person(name: str) -> Person: # Your logic to retrieve the person goes here # For demonstration purposes, a placeholder Person instance is returned address = Address(street="123 Main St", city="Cityville", country="Countryland") return Person(name=name, age=30, email=f"email_of_{name}@example.com", address=address) app = Litestar(route_handlers=[get_person]) ����������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/patch_handlers.py��������������0000664�0000000�0000000�00000001464�15005643713�0031377�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from litestar import Litestar, patch from litestar.dto import DataclassDTO, DTOConfig, DTOData @dataclass class Person: name: str age: int email: str id: int class ReadDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"email"}) class PatchDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"id"}, partial=True) @patch("/person/{person_id:int}", dto=PatchDTO, return_dto=ReadDTO, sync_to_thread=False) def update_person(person_id: int, data: DTOData[Person]) -> Person: # Usually the Person would be retrieved from a database person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com") return data.update_instance(person) app = Litestar(route_handlers=[update_person]) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/put_handlers.py����������������0000664�0000000�0000000�00000001442�15005643713�0031104�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from litestar import Litestar, put from litestar.dto import DataclassDTO, DTOConfig, DTOData @dataclass class Person: name: str age: int email: str id: int class ReadDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"email"}) class WriteDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"id"}) @put("/person/{person_id:int}", dto=WriteDTO, return_dto=ReadDTO, sync_to_thread=False) def update_person(person_id: int, data: DTOData[Person]) -> Person: # Usually the Person would be retrieved from a database person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com") return data.update_instance(person) app = Litestar(route_handlers=[update_person]) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/read_only_fields_error.py������0000664�0000000�0000000�00000001164�15005643713�0033130�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from litestar import Litestar, post from litestar.dto import DataclassDTO, DTOConfig @dataclass class Person: name: str age: int email: str id: int class ReadDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"email"}) class WriteDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"id"}) @post("/person", dto=WriteDTO, return_dto=ReadDTO, sync_to_thread=False) def create_person(data: Person) -> Person: # Logic for persisting the person goes here return data app = Litestar(route_handlers=[create_person]) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/simple_dto_exclude.py����������0000664�0000000�0000000�00000001224�15005643713�0032262�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from litestar import Litestar, get from litestar.dto import DataclassDTO, DTOConfig @dataclass class Person: name: str age: int email: str class ReadDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"email"}) @get("/person/{name:str}", return_dto=ReadDTO, sync_to_thread=False) def get_person(name: str) -> Person: # Your logic to retrieve the person goes here # For demonstration purposes, a placeholder Person instance is returned return Person(name=name, age=30, email=f"email_of_{name}@example.com") app = Litestar(route_handlers=[get_person]) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/tutorial/simple_receiving_data.py�������0000664�0000000�0000000�00000001013�15005643713�0032723�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from litestar import Litestar, post from litestar.dto import DataclassDTO, DTOConfig @dataclass class Person: name: str age: int email: str class ReadDTO(DataclassDTO[Person]): config = DTOConfig(exclude={"email"}) @post("/person", return_dto=ReadDTO, sync_to_thread=False) def create_person(data: Person) -> Person: # Logic for persisting the person goes here return data app = Litestar(route_handlers=[create_person]) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/type_checking.py������������������������0000664�0000000�0000000�00000001253�15005643713�0027365�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from datetime import datetime from sqlalchemy.orm import Mapped, mapped_column from litestar import Litestar, post from litestar.dto import dto_field from litestar.plugins.sqlalchemy import SQLAlchemyDTO from .my_lib import Base class User(Base): name: Mapped[str] password: Mapped[str] = mapped_column(info=dto_field("private")) created_at: Mapped[datetime] = mapped_column(info=dto_field("read-only")) class Foo(Base): foo: Mapped[str] UserDTO = SQLAlchemyDTO[User] @post("/users", dto=UserDTO) def create_user(data: Foo) -> Foo: return data # This will raise an exception at handler registration time. app = Litestar(route_handlers=[create_user]) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/factory/unknown_fields.py�����������������������0000664�0000000�0000000�00000001013�15005643713�0027570�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from typing_extensions import Annotated from litestar import Litestar, post from litestar.dto import DataclassDTO, DTOConfig @dataclass class User: id: str UserDTO = DataclassDTO[Annotated[User, DTOConfig(forbid_unknown_fields=True)]] @post("/users", dto=UserDTO) async def create_user(data: User) -> User: return data app = Litestar([create_user]) # run: /users -H "Content-Type: application/json" -d '{"id": "1", "name": "Peter"}' ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/models.py���������������������������������������0000664�0000000�0000000�00000000371�15005643713�0024365�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from uuid import UUID from litestar.dto import DataclassDTO @dataclass class User: id: UUID name: str UserDTO = DataclassDTO[User] UserReturnDTO = DataclassDTO[User] �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/overriding_implicit_return_dto.py���������������0000664�0000000�0000000�00000000717�15005643713�0031415�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from dataclasses import dataclass, field from uuid import UUID, uuid4 from litestar import Litestar, post from litestar.dto import DataclassDTO @dataclass class User: name: str email: str age: int id: UUID = field(default_factory=uuid4) UserDTO = DataclassDTO[User] @post(dto=UserDTO, return_dto=None, sync_to_thread=False) def create_user(data: User) -> bytes: return data.name.encode(encoding="utf-8") app = Litestar([create_user]) �������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/the_dto_parameter.py����������������������������0000664�0000000�0000000�00000000207�15005643713�0026566�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import post from .models import User, UserDTO @post(dto=UserDTO) def create_user(data: User) -> User: return data �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/data_transfer_objects/the_return_dto_parameter.py���������������������0000664�0000000�0000000�00000000260�15005643713�0030164�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import post from .models import User, UserDTO, UserReturnDTO @post(dto=UserDTO, return_dto=UserReturnDTO) def create_user(data: User) -> User: return data ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/datastructures/�������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0021256�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/datastructures/__init__.py��������������������������������������������0000664�0000000�0000000�00000000000�15005643713�0023355�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/datastructures/headers/�����������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0022671�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/datastructures/headers/__init__.py������������������������������������0000664�0000000�0000000�00000000000�15005643713�0024770�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/datastructures/headers/cache_control.py�������������������������������0000664�0000000�0000000�00000001762�15005643713�0026054�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import time from litestar import Controller, Litestar, get from litestar.datastructures import CacheControlHeader class MyController(Controller): cache_control = CacheControlHeader(max_age=86_400, public=True) @get("/chance_of_rain", sync_to_thread=False) def get_chance_of_rain(self) -> float: """This endpoint uses the cache control value defined in the controller which overrides the app value.""" return 0.5 @get("/timestamp", cache_control=CacheControlHeader(no_store=True), sync_to_thread=False) def get_server_time(self) -> float: """This endpoint overrides the cache control value defined in the controller.""" return time.time() @get("/population", sync_to_thread=False) def get_population_count() -> int: """This endpoint will use the cache control defined in the app.""" return 100000 app = Litestar( route_handlers=[MyController, get_population_count], cache_control=CacheControlHeader(max_age=2_628_288, public=True), ) ��������������litestar-2.16.0/docs/examples/datastructures/headers/etag.py����������������������������������������0000664�0000000�0000000�00000002772�15005643713�0024173�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import random import time from litestar import Controller, Litestar, get from litestar.datastructures import ETag from litestar.enums import MediaType from litestar.response import Response class MyController(Controller): etag = ETag(value="foo") @get("/chance_of_rain") def get_chance_of_rain(self) -> float: """This endpoint uses the etag value in the controller which overrides the app value. The returned header will be `etag: "foo"` """ return 0.5 @get("/timestamp", etag=ETag(value="bar")) def get_server_time(self) -> float: """This endpoint overrides the etag defined in the controller. The returned header will be `etag: W/"bar"` """ return time.time() @get("/population") def get_population_count() -> int: """This endpoint will use the etag defined in the app. The returned header will be `etag: "bar"` """ return 100000 @get("/population-dynamic", etag=ETag(documentation_only=True)) def get_population_count_dynamic() -> Response[str]: """The etag defined in this route handler will not be returned, and does not need a value. It will only be used for OpenAPI generation. """ population_count = random.randint(0, 1000) return Response( content=str(population_count), headers={"etag": str(population_count)}, media_type=MediaType.TEXT, status_code=200, ) app = Litestar(route_handlers=[MyController, get_population_count], etag=ETag(value="bar")) ������litestar-2.16.0/docs/examples/datastructures/headers/etag_parsing.py��������������������������������0000664�0000000�0000000�00000000243�15005643713�0025705�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar.datastructures import ETag assert ETag.from_header('"foo"') == ETag(value="foo") assert ETag.from_header('W/"foo"') == ETag(value="foo", weak=True) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/datastructures/secrets/�����������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0022726�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/datastructures/secrets/__init__.py������������������������������������0000664�0000000�0000000�00000000000�15005643713�0025025�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/datastructures/secrets/secret_body.py���������������������������������0000664�0000000�0000000�00000000420�15005643713�0025576�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from dataclasses import dataclass from litestar import post from litestar.datastructures.secret_values import SecretString @dataclass class Sensitive: value: SecretString @post(sync_to_thread=False) def post_handler(data: Sensitive) -> Sensitive: return data ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/datastructures/secrets/secret_header.py�������������������������������0000664�0000000�0000000�00000001351�15005643713�0026075�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from dataclasses import dataclass from secrets import compare_digest from typing_extensions import Annotated from litestar import get from litestar.datastructures.secret_values import SecretString from litestar.exceptions import NotAuthorizedException from litestar.params import Parameter SECRET_VALUE = "super-secret" # An example secret value - this should be stored securely in production. @dataclass class Sensitive: value: str @get(sync_to_thread=False) def get_handler(secret: Annotated[SecretString, Parameter(header="x-secret")]) -> Sensitive: if not compare_digest(secret.get_secret(), SECRET_VALUE): raise NotAuthorizedException return Sensitive(value="sensitive data") ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/dependency_injection/�������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0022361�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/dependency_injection/__init__.py��������������������������������������0000664�0000000�0000000�00000000000�15005643713�0024460�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/dependency_injection/dependency_non_optional_not_provided.py����������0000664�0000000�0000000�00000001222�15005643713�0032401�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Any from typing_extensions import Annotated from litestar import Litestar, get from litestar.params import Dependency @get("/") def hello_world(non_optional_dependency: Annotated[int, Dependency()]) -> dict[str, Any]: """Notice we haven't provided the dependency to the route. This is not great, however by explicitly marking dependencies, Litestar won't let the app start. """ return {"hello": non_optional_dependency} app = Litestar(route_handlers=[hello_world]) # ImproperlyConfiguredException: 500: Explicit dependency 'non_optional_dependency' for 'hello_world' has no default # value, or provided dependency. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/dependency_injection/dependency_skip_validation.py��������������������0000664�0000000�0000000�00000001103�15005643713�0030304�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Any, Dict from typing_extensions import Annotated from litestar import Litestar, get from litestar.di import Provide from litestar.params import Dependency async def provide_str() -> str: """Returns a string.""" return "whoops" @get("/", dependencies={"injected": Provide(provide_str)}, sync_to_thread=False) def hello_world(injected: Annotated[int, Dependency(skip_validation=True)]) -> Dict[str, Any]: """Handler expects an `int`, but we've provided a `str`.""" return {"hello": injected} app = Litestar(route_handlers=[hello_world]) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/dependency_injection/dependency_validation_error.py�������������������0000664�0000000�0000000�00000000706�15005643713�0030477�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Any, Dict from litestar import Litestar, get from litestar.di import Provide async def provide_str() -> str: """Returns a string.""" return "whoops" @get("/", dependencies={"injected": Provide(provide_str)}, sync_to_thread=False) def hello_world(injected: int) -> Dict[str, Any]: """Handler expects an `int`, but we've provided a `str`.""" return {"hello": injected} app = Litestar(route_handlers=[hello_world]) ����������������������������������������������������������litestar-2.16.0/docs/examples/dependency_injection/dependency_with_default.py�����������������������0000664�0000000�0000000�00000000630�15005643713�0027607�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Any, Dict from litestar import Litestar, get @get("/", sync_to_thread=False) def hello_world(optional_dependency: int = 3) -> Dict[str, Any]: """Notice we haven't provided the dependency to the route. This is OK, because of the default value, but the parameter shows in the docs. """ return {"hello": optional_dependency} app = Litestar(route_handlers=[hello_world]) ��������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/dependency_injection/dependency_with_dependency_fn_and_default.py�����0000664�0000000�0000000�00000001022�15005643713�0033306�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Any, Dict from typing_extensions import Annotated from litestar import Litestar, get from litestar.params import Dependency @get("/", sync_to_thread=False) def hello_world(optional_dependency: Annotated[int, Dependency(default=3)]) -> Dict[str, Any]: """Notice we haven't provided the dependency to the route. This is OK, because of the default value, and now the parameter is excluded from the docs. """ return {"hello": optional_dependency} app = Litestar(route_handlers=[hello_world]) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/dependency_injection/dependency_yield_exceptions.py�������������������0000664�0000000�0000000�00000001612�15005643713�0030500�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Dict, Generator from litestar import Litestar, get from litestar.di import Provide STATE = {"result": None, "connection": "closed"} def generator_function() -> Generator[str, None, None]: """Set the connection state to open and close it after the handler returns. If an error occurs, set `result` to `"error"`, else set it to `"OK"`. """ try: STATE["connection"] = "open" yield "hello" STATE["result"] = "OK" except ValueError: STATE["result"] = "error" finally: STATE["connection"] = "closed" @get("/{name:str}", dependencies={"message": Provide(generator_function)}) def index(name: str, message: str) -> Dict[str, str]: """If `name` is "John", return a message, otherwise raise an error.""" if name == "John": return {name: message} raise ValueError() app = Litestar(route_handlers=[index]) ����������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/dependency_injection/dependency_yield_simple.py�����������������������0000664�0000000�0000000�00000001103�15005643713�0027603�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Dict, Generator from litestar import Litestar, get from litestar.di import Provide CONNECTION = {"open": False} def generator_function() -> Generator[Dict[str, bool], None, None]: """Set connection to open and close it after the handler returns.""" CONNECTION["open"] = True yield CONNECTION CONNECTION["open"] = False @get("/", dependencies={"conn": Provide(generator_function)}) def index(conn: Dict[str, bool]) -> Dict[str, bool]: """Return the current connection state.""" return conn app = Litestar(route_handlers=[index]) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/deployment/�����������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0020361�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/deployment/nginx-unit/������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0022461�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/deployment/nginx-unit/install-macos.sh��������������������������������0000664�0000000�0000000�00000000101�15005643713�0025553�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������brew install unit-python311 brew install nginx/unit/unit-python3 ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/deployment/nginx-unit/unit.json���������������������������������������0000664�0000000�0000000�00000000644�15005643713�0024337�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "listeners": { "*:8080": { "pass": "applications/litestar" } }, "applications": { "litestar": { "type": "python 3.11", "home": "/Users/user/project/litestar/.venv/", "path": "/Users/user/project/litestar/src/app", "module": "run", "callable": "app", "stderr": "/Users/user/project/litestar/error.log", "user": "user", "processes": 1 } } } ��������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/encoding_decoding/����������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0021623�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/encoding_decoding/__init__.py�����������������������������������������0000664�0000000�0000000�00000000000�15005643713�0023722�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/encoding_decoding/custom_type_encoding_decoding.py��������������������0000664�0000000�0000000�00000004037�15005643713�0030256�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Any, Type from msgspec import Struct from litestar import Litestar, post class TenantUser: """Custom Type that represents a user associated to a tenant Parsed from / serialized to a combined tenant + user id string of the form TENANTPREFIX_USERID i.e. separated by underscore. """ tenant_prefix: str user_id: str def __init__(self, tenant_prefix: str, user_id: str) -> None: self.tenant_prefix = tenant_prefix self.user_id = user_id @classmethod def from_string(cls, s: str) -> "TenantUser": splits = s.split("_", maxsplit=1) if len(splits) < 2: raise ValueError( "Could not split up tenant user id string. " "Expecting underscore for separation of tenant prefix and user id." ) return cls(tenant_prefix=splits[0], user_id=splits[1]) def to_combined_str(self) -> str: return self.tenant_prefix + "_" + self.user_id def tenant_user_type_predicate(type: Type) -> bool: return type is TenantUser def tenant_user_enc_hook(u: TenantUser) -> Any: return u.to_combined_str() def tenant_user_dec_hook(tenant_user_id_str: str) -> TenantUser: return TenantUser.from_string(tenant_user_id_str) def general_dec_hook(type: Type, obj: Any) -> Any: if tenant_user_type_predicate(type): return tenant_user_dec_hook(obj) raise NotImplementedError(f"Encountered unknown type during decoding: {type!s}") class UserAsset(Struct): user: TenantUser name: str @post("/asset", sync_to_thread=False) def create_asset( data: UserAsset, ) -> UserAsset: assert isinstance(data.user, TenantUser) return data app = Litestar( [create_asset], type_encoders={TenantUser: tenant_user_enc_hook}, # tell litestar how to encode TenantUser type_decoders=[(tenant_user_type_predicate, general_dec_hook)], # tell litestar how to decode TenantUser ) # run: /asset -X POST -H "Content-Type: application/json" -d '{"name":"SomeAsset","user":"TenantA_Somebody"}' �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/encoding_decoding/custom_type_pydantic.py�����������������������������0000664�0000000�0000000�00000003343�15005643713�0026446�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from pydantic import BaseModel, BeforeValidator, ConfigDict, PlainSerializer, WithJsonSchema from typing_extensions import Annotated from litestar import Litestar, post class TenantUser: """Custom Type that represents a user associated to a tenant Parsed from / serialized to a combined tenant + user id string of the form TENANTPREFIX_USERID i.e. separated by underscore. """ tenant_prefix: str user_id: str def __init__(self, tenant_prefix: str, user_id: str) -> None: self.tenant_prefix = tenant_prefix self.user_id = user_id @classmethod def from_string(cls, s: str) -> "TenantUser": splits = s.split("_", maxsplit=1) if len(splits) < 2: raise ValueError( "Could not split up tenant user id string. " "Expecting underscore for separation of tenant prefix and user id." ) return cls(tenant_prefix=splits[0], user_id=splits[1]) def to_combined_str(self) -> str: return self.tenant_prefix + "_" + self.user_id PydAnnotatedTenantUser = Annotated[ TenantUser, BeforeValidator(lambda x: TenantUser.from_string(x)), PlainSerializer(lambda x: x.to_combined_str(), return_type=str), WithJsonSchema({"type": "string"}, mode="serialization"), ] class UserAsset(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) user: PydAnnotatedTenantUser name: str @post("/asset", sync_to_thread=False) def create_asset( data: UserAsset, ) -> UserAsset: assert isinstance(data.user, TenantUser) return data app = Litestar( [create_asset], ) # run: /asset -X POST -H "Content-Type: application/json" -d '{"name":"SomeAsset","user":"TenantA_Somebody"}' ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/exceptions/�����������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0020362�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/exceptions/__init__.py������������������������������������������������0000664�0000000�0000000�00000000000�15005643713�0022461�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/exceptions/implicit_media_type.py�������������������������������������0000664�0000000�0000000�00000000147�15005643713�0024750�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import get @get(sync_to_thread=False) def handler(q: int) -> str: raise ValueError �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/exceptions/layered_handlers.py����������������������������������������0000664�0000000�0000000�00000002035�15005643713�0024241�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar, Request, Response, get from litestar.exceptions import HTTPException, ValidationException def app_exception_handler(request: Request, exc: HTTPException) -> Response: return Response( content={ "error": "server error", "path": request.url.path, "detail": exc.detail, "status_code": exc.status_code, }, status_code=500, ) def router_handler_exception_handler(request: Request, exc: ValidationException) -> Response: return Response( content={"error": "validation error", "path": request.url.path}, status_code=400, ) @get("/") async def index() -> None: raise HTTPException("something's gone wrong") @get( "/greet", exception_handlers={ValidationException: router_handler_exception_handler}, ) async def greet(name: str) -> str: return f"hello {name}" app = Litestar( route_handlers=[index, greet], exception_handlers={HTTPException: app_exception_handler}, ) # run: / # run: /greet ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/exceptions/override_default_handler.py��������������������������������0000664�0000000�0000000�00000001436�15005643713�0025760�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar, MediaType, Request, Response, get from litestar.exceptions import HTTPException from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR def plain_text_exception_handler(_: Request, exc: Exception) -> Response: """Default handler for exceptions subclassed from HTTPException.""" status_code = getattr(exc, "status_code", HTTP_500_INTERNAL_SERVER_ERROR) detail = getattr(exc, "detail", "") return Response( media_type=MediaType.TEXT, content=detail, status_code=status_code, ) @get("/") async def index() -> None: raise HTTPException(detail="an error occurred", status_code=400) app = Litestar( route_handlers=[index], exception_handlers={HTTPException: plain_text_exception_handler}, ) # run: / ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/exceptions/per_exception_handlers.py����������������������������������0000664�0000000�0000000�00000002716�15005643713�0025466�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar, MediaType, Request, Response, get from litestar.exceptions import HTTPException, ValidationException from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR def validation_exception_handler(request: Request, exc: ValidationException) -> Response: return Response( media_type=MediaType.TEXT, content=f"validation error: {exc.detail}", status_code=400, ) def internal_server_error_handler(request: Request, exc: Exception) -> Response: return Response( media_type=MediaType.TEXT, content=f"server error: {exc}", status_code=500, ) def value_error_handler(request: Request, exc: ValueError) -> Response: return Response( media_type=MediaType.TEXT, content=f"value error: {exc}", status_code=400, ) @get("/validation-error") async def validation_error(some_query_param: str) -> str: return some_query_param @get("/server-error") async def server_error() -> None: raise HTTPException() @get("/value-error") async def value_error() -> None: raise ValueError("this is wrong") app = Litestar( route_handlers=[validation_error, server_error, value_error], exception_handlers={ ValidationException: validation_exception_handler, HTTP_500_INTERNAL_SERVER_ERROR: internal_server_error_handler, ValueError: value_error_handler, }, ) # run: /validation-error # run: /server-error # run: /value-error ��������������������������������������������������litestar-2.16.0/docs/examples/hello_world.py��������������������������������������������������������0000664�0000000�0000000�00000000411�15005643713�0021061�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Dict from litestar import Litestar, get @get("/") async def hello_world() -> Dict[str, str]: """Handler function that returns a greeting dictionary.""" return {"hello": "world"} app = Litestar(route_handlers=[hello_world]) # run: / �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/lifecycle_hooks/������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0021343�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/lifecycle_hooks/__init__.py�������������������������������������������0000664�0000000�0000000�00000000000�15005643713�0023442�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/lifecycle_hooks/after_request.py��������������������������������������0000664�0000000�0000000�00000001025�15005643713�0024564�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Dict from litestar import Litestar, MediaType, Response, get async def after_request(response: Response) -> Response: if response.media_type == MediaType.TEXT: return Response({"message": response.content}) return response @get("/hello") async def hello() -> str: return "Hello, world" @get("/goodbye") async def goodbye() -> Dict[str, str]: return {"message": "Goodbye"} app = Litestar(route_handlers=[hello, goodbye], after_request=after_request) # run: /hello # run: /goodbye �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/lifecycle_hooks/after_response.py�������������������������������������0000664�0000000�0000000�00000000635�15005643713�0024740�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from collections import defaultdict from typing import Dict from litestar import Litestar, Request, get COUNTER: Dict[str, int] = defaultdict(int) async def after_response(request: Request) -> None: COUNTER[request.url.path] += 1 @get("/hello") async def hello() -> Dict[str, int]: return COUNTER app = Litestar(route_handlers=[hello], after_response=after_response) # run: /hello # run: /hello ���������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/lifecycle_hooks/before_request.py�������������������������������������0000664�0000000�0000000�00000001205�15005643713�0024725�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Dict, Optional from litestar import Litestar, Request, get async def before_request_handler(request: Request) -> Optional[Dict[str, str]]: name = request.query_params["name"] if name == "Ben": return {"message": "These are not the bytes you are looking for"} request.state["message"] = f"Use the handler, {name}" return None @get("/") async def handler(request: Request, name: str) -> Dict[str, str]: message: str = request.state["message"] return {"message": message} app = Litestar(route_handlers=[handler], before_request=before_request_handler) # run: /?name=Luke # run: /?name=Ben �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/lifecycle_hooks/layered_hooks.py��������������������������������������0000664�0000000�0000000�00000001124�15005643713�0024543�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar, Response, get def after_request_app(response: Response) -> Response: return Response(content=b"app after request") def after_request_handler(response: Response) -> Response: return Response(content=b"handler after request") @get("/") async def handler() -> str: return "hello, world" @get("/override", after_request=after_request_handler) async def handler_with_override() -> str: return "hello, world" app = Litestar( route_handlers=[handler, handler_with_override], after_request=after_request_app, ) # run: / # run: /override ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/�����������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0020316�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/__init__.py������������������������������������������������0000664�0000000�0000000�00000000000�15005643713�0022415�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/abstract_middleware_migration_new.py�����������������������0000664�0000000�0000000�00000001007�15005643713�0027610�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import anyio from litestar import Litestar from litestar.middleware import ASGIMiddleware from litestar.types import ASGIApp, Receive, Scope, Send class TimeoutMiddleware(ASGIMiddleware): def __init__(self, timeout: float): self.timeout = timeout async def handle(self, scope: Scope, receive: Receive, send: Send, next_app: ASGIApp) -> None: with anyio.move_on_after(self.timeout): await next_app(scope, receive, send) app = Litestar(middleware=[TimeoutMiddleware(timeout=5)]) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/abstract_middleware_migration_old.py�����������������������0000664�0000000�0000000�00000001563�15005643713�0027604�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import anyio from litestar import Litestar from litestar.middleware import AbstractMiddleware, DefineMiddleware from litestar.types import ASGIApp, Receive, Scope, Scopes, Send class TimeoutMiddleware(AbstractMiddleware): def __init__( self, app: ASGIApp, timeout: float, exclude: str | list[str] | None = None, exclude_opt_key: str | None = None, scopes: Scopes | None = None, ): self.timeout = timeout super().__init__(app=app, exclude=exclude, exclude_opt_key=exclude_opt_key, scopes=scopes) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: with anyio.move_on_after(self.timeout): await self.app(scope, receive, send) app = Litestar( middleware=[ DefineMiddleware( TimeoutMiddleware, timeout=5, ) ] ) ���������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/base.py����������������������������������������������������0000664�0000000�0000000�00000004530�15005643713�0021604�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import time from typing import Dict from litestar import Litestar, WebSocket, get, websocket from litestar.datastructures import MutableScopeHeaders from litestar.enums import ScopeType from litestar.middleware import ASGIMiddleware from litestar.types import ASGIApp, Message, Receive, Scope, Send class MyMiddleware(ASGIMiddleware): scopes = (ScopeType.HTTP,) exclude_path_pattern = ("first_path", "second_path") exclude_opt_key = "exclude_from_my_middleware" async def handle(self, scope: Scope, receive: Receive, send: Send, next_app: ASGIApp) -> None: start_time = time.monotonic() async def send_wrapper(message: "Message") -> None: if message["type"] == "http.response.start": process_time = time.monotonic() - start_time headers = MutableScopeHeaders.from_message(message=message) headers["X-Process-Time"] = str(process_time) await send(message) await next_app(scope, receive, send_wrapper) @websocket("/my-websocket") async def websocket_handler(socket: WebSocket) -> None: """ Websocket handler - is excluded because the middleware scopes includes 'ScopeType.HTTP' """ await socket.accept() await socket.send_json({"hello": "websocket"}) await socket.close() @get("/first_path", sync_to_thread=False) def first_handler() -> Dict[str, str]: """Handler is excluded due to regex pattern matching "first_path".""" return {"hello": "first"} @get("/second_path", sync_to_thread=False) def second_handler() -> Dict[str, str]: """Handler is excluded due to regex pattern matching "second_path".""" return {"hello": "second"} @get("/third_path", exclude_from_my_middleware=True, sync_to_thread=False) def third_handler() -> Dict[str, str]: """Handler is excluded due to the opt key 'exclude_from_my_middleware' matching the middleware 'exclude_opt_key'.""" return {"hello": "third"} @get("/greet", sync_to_thread=False) def not_excluded_handler() -> Dict[str, str]: """This handler is not excluded, and thus the middleware will execute on every incoming request to it.""" return {"hello": "world"} app = Litestar( route_handlers=[ websocket_handler, first_handler, second_handler, third_handler, not_excluded_handler, ], middleware=[MyMiddleware()], ) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/call_order.py����������������������������������������������0000664�0000000�0000000�00000002676�15005643713�0023011�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import TYPE_CHECKING, List, Type from litestar import Controller, Litestar, Router, get from litestar.datastructures import State from litestar.middleware import MiddlewareProtocol if TYPE_CHECKING: from litestar.types import ASGIApp, Receive, Scope, Send def create_test_middleware(middleware_id: int) -> Type[MiddlewareProtocol]: class TestMiddleware(MiddlewareProtocol): def __init__(self, app: "ASGIApp") -> None: self.app = app async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: litestar_app = scope["app"] litestar_app.state.setdefault("middleware_calls", []) litestar_app.state["middleware_calls"].append(middleware_id) await self.app(scope, receive, send) return TestMiddleware class MyController(Controller): path = "/controller" middleware = [create_test_middleware(4), create_test_middleware(5)] @get( "/handler", middleware=[create_test_middleware(6), create_test_middleware(7)], ) async def my_handler(self, state: State) -> List[int]: return state["middleware_calls"] router = Router( path="/router", route_handlers=[MyController], middleware=[create_test_middleware(2), create_test_middleware(3)], ) app = Litestar( route_handlers=[router], middleware=[create_test_middleware(0), create_test_middleware(1)], ) # run: /router/controller/handler ������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/logging_middleware.py��������������������������������������0000664�0000000�0000000�00000000731�15005643713�0024514�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Dict from litestar import Litestar, get from litestar.logging.config import LoggingConfig from litestar.middleware.logging import LoggingMiddlewareConfig logging_middleware_config = LoggingMiddlewareConfig() @get("/", sync_to_thread=False) def my_handler() -> Dict[str, str]: return {"hello": "world"} app = Litestar( route_handlers=[my_handler], logging_config=LoggingConfig(), middleware=[logging_middleware_config.middleware], ) ���������������������������������������litestar-2.16.0/docs/examples/middleware/middleware_protocol_migration_new.py�����������������������0000664�0000000�0000000�00000000461�15005643713�0027651�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar.middleware import ASGIMiddleware from litestar.types import ASGIApp, Receive, Scope, Send class MyMiddleware(ASGIMiddleware): async def handle(self, scope: Scope, receive: Receive, send: Send, next_app: ASGIApp) -> None: # do stuff await next_app(scope, receive, send) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/middleware_protocol_migration_old.py�����������������������0000664�0000000�0000000�00000000556�15005643713�0027643�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar.middleware import MiddlewareProtocol from litestar.types import ASGIApp, Receive, Scope, Send class MyMiddleware(MiddlewareProtocol): def __init__(self, app: ASGIApp) -> None: self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: # do stuff await self.app(scope, receive, send) ��������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/rate_limit.py����������������������������������������������0000664�0000000�0000000�00000000700�15005643713�0023016�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from litestar import Litestar, MediaType, get from litestar.middleware.rate_limit import RateLimitConfig rate_limit_config = RateLimitConfig(rate_limit=("minute", 1), exclude=["/schema"]) @get("/", media_type=MediaType.TEXT, sync_to_thread=False) def handler() -> str: """Handler which should not be accessed more than once per minute.""" return "ok" app = Litestar(route_handlers=[handler], middleware=[rate_limit_config.middleware]) ����������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/request_timing.py������������������������������������������0000664�0000000�0000000�00000001517�15005643713�0023733�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import time from litestar.datastructures import MutableScopeHeaders from litestar.enums import ScopeType from litestar.middleware import ASGIMiddleware from litestar.types import ASGIApp, Message, Receive, Scope, Send class ProcessTimeHeader(ASGIMiddleware): scopes = (ScopeType.HTTP, ScopeType.ASGI) async def handle(self, scope: Scope, receive: Receive, send: Send, next_app: ASGIApp) -> None: start_time = time.monotonic() async def send_wrapper(message: Message) -> None: if message["type"] == "http.response.start": process_time = time.monotonic() - start_time headers = MutableScopeHeaders.from_message(message=message) headers["X-Process-Time"] = str(process_time) await send(message) await next_app(scope, receive, send_wrapper) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/session/���������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0022001�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/session/__init__.py����������������������������������������0000664�0000000�0000000�00000000000�15005643713�0024100�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/session/cookie_backend.py����������������������������������0000664�0000000�0000000�00000000400�15005643713�0025265�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from os import urandom from litestar import Litestar from litestar.middleware.session.client_side import CookieBackendConfig session_config = CookieBackendConfig(secret=urandom(16)) # type: ignore app = Litestar(middleware=[session_config.middleware]) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/session/cookies_full_example.py����������������������������0000664�0000000�0000000�00000002325�15005643713�0026546�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from os import urandom from typing import Dict from litestar import Litestar, Request, delete, get, post from litestar.middleware.session.client_side import CookieBackendConfig # we initialize to config with a 16 byte key, i.e. 128 a bit key. # in real world usage we should inject the secret from the environment session_config = CookieBackendConfig(secret=urandom(16)) # type: ignore[arg-type] @get("/session", sync_to_thread=False) def check_session_handler(request: Request) -> Dict[str, bool]: """Handler function that accesses request.session.""" return {"has_session": request.session != {}} @post("/session", sync_to_thread=False) def create_session_handler(request: Request) -> None: """Handler to set the session.""" if not request.session: # value can be a dictionary or pydantic model request.set_session({"username": "moishezuchmir"}) @delete("/session", sync_to_thread=False) def delete_session_handler(request: Request) -> None: """Handler to clear the session.""" if request.session: request.clear_session() app = Litestar( route_handlers=[check_session_handler, create_session_handler, delete_session_handler], middleware=[session_config.middleware], ) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/session/file_store.py��������������������������������������0000664�0000000�0000000�00000000470�15005643713�0024507�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from pathlib import Path from litestar import Litestar from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.stores.file import FileStore app = Litestar( middleware=[ServerSideSessionConfig().middleware], stores={"sessions": FileStore(path=Path("session_data"))}, ) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/middleware/using_asgi_middleware.py�����������������������������������0000664�0000000�0000000�00000003777�15005643713�0025233�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import anyio from litestar import Litestar, get from litestar.enums import ScopeType from litestar.exceptions import ClientException from litestar.middleware import ASGIMiddleware from litestar.types import ASGIApp, Receive, Scope, Send class TimeoutMiddleware(ASGIMiddleware): # we can configure some things on the class level here, related to when our # middleware should be applied. # if the requests' 'scope["type"]' is not "http", the middleware will be skipped scopes = (ScopeType.HTTP,) # if the handler for a request has set an opt of 'no_timeout=True', the middleware # will be skipped exclude_opt_key = "no_timeout" # the base class does not define an '__init__' method, so we're free to overwrite # this, which we're making use of to add some configuration def __init__( self, timeout: float, exclude_path_pattern: str | tuple[str, ...] | None = None, ) -> None: self.timeout = timeout # we can also dynamically configure the options provided by the base class on # the instance level self.exclude_path_pattern = exclude_path_pattern async def handle(self, scope: Scope, receive: Receive, send: Send, next_app: ASGIApp) -> None: try: with anyio.fail_after(self.timeout): # call the next app in the chain await next_app(scope, receive, send) except TimeoutError: # if the request has timed out, raise an exception. since the whole # application is wrapped in an exception handling middleware, it will # transform this exception into a response for us raise ClientException(status_code=408) from None @get("/", no_timeout=True) async def handler_with_opt_skip() -> None: pass @get("/not-this-path") async def handler_with_path_skip() -> None: pass app = Litestar( route_handlers=[ handler_with_opt_skip, handler_with_path_skip, ], middleware=[TimeoutMiddleware(timeout=5)], ) �litestar-2.16.0/docs/examples/openapi/��������������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0017634�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/openapi/__init__.py���������������������������������������������������0000664�0000000�0000000�00000000000�15005643713�0021733�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/openapi/customize_operation_class.py����������������������������������0000664�0000000�0000000�00000003242�15005643713�0025476�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from dataclasses import dataclass, field from typing import Dict, List, Optional from litestar import Litestar, MediaType, Request, post from litestar.exceptions import HTTPException from litestar.openapi.spec import OpenAPIMediaType, OpenAPIType, Operation, RequestBody, Schema from litestar.status_codes import HTTP_400_BAD_REQUEST @dataclass class CustomOperation(Operation): """Custom Operation class which includes a non-standard field which is part of an OpenAPI extension.""" x_code_samples: Optional[List[Dict[str, str]]] = field(default=None, metadata={"alias": "x-codeSamples"}) def __post_init__(self) -> None: self.tags = ["ok"] self.description = "Requires OK, Returns OK" self.request_body = RequestBody( content={ "text": OpenAPIMediaType( schema=Schema( title="Body", type=OpenAPIType.STRING, example="OK", ) ), }, description="OK is the only accepted value", ) self.x_codeSamples = [ {"lang": "Python", "source": "import requests; requests.get('localhost/example')", "label": "Python"}, {"lang": "cURL", "source": "curl -XGET localhost/example", "label": "curl"}, ] @post("/", operation_class=CustomOperation, media_type=MediaType.TEXT) async def route(request: Request) -> str: """ Returns: OK """ if (await request.body()) == b"OK": return "OK" raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail="request payload must be OK") app = Litestar(route_handlers=[route]) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/openapi/customize_path.py���������������������������������������������0000664�0000000�0000000�00000000644�15005643713�0023250�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from typing import Dict from litestar import Litestar, get from litestar.openapi.config import OpenAPIConfig @get("/") def hello_world() -> Dict[str, str]: return {"message": "Hello World"} app = Litestar( route_handlers=[hello_world], openapi_config=OpenAPIConfig( title="My API", description="This is the description of my API", version="0.1.0", path="/docs", ), ) ��������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/openapi/customize_pydantic_model_name.py������������������������������0000664�0000000�0000000�00000000571�15005643713�0026306�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from uuid import UUID, uuid4 from pydantic import BaseModel from litestar import Litestar, get class IdModel(BaseModel): __schema_name__ = "IdContainer" id: UUID @get("/id", sync_to_thread=False) def retrieve_id_handler() -> IdModel: """ Returns: An IdModel """ return IdModel(id=uuid4()) app = Litestar(route_handlers=[retrieve_id_handler]) ���������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/openapi/plugins/������������������������������������������������������0000775�0000000�0000000�00000000000�15005643713�0021315�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/openapi/plugins/__init__.py�������������������������������������������0000664�0000000�0000000�00000000000�15005643713�0023414�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������litestar-2.16.0/docs/examples/openapi/plugins/custom_plugin.py��������������������������������������0000664�0000000�0000000�00000003222�15005643713�0024556�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import annotations from collections.abc import Sequence from typing import Any from litestar.connection import Request from litestar.openapi.plugins import OpenAPIRenderPlugin class ScalarRenderPlugin(OpenAPIRenderPlugin): def __init__( self, *, version: str = "1.19.5", js_url: str | None = None, css_url: str | None = None, path: str | Sequence[str] = "/scalar", **kwargs: Any, ) -> None: self.js_url = js_url or f"https://cdn.jsdelivr.net/npm/@scalar/api-reference@{version}" self.css_url = css_url super().__init__(path=path, **kwargs) def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes: style_sheet_link = f'<link rel="stylesheet" type="text/css" href="{self.css_url}">' if self.css_url else "" head = f""" <head> <title>{openapi_schema["info"]["title"]} {self.style} {self.favicon} {style_sheet_link} """ body = f""" """ return f""" {head} {body} """.encode() litestar-2.16.0/docs/examples/openapi/plugins/rapidoc_config.py000066400000000000000000000002011500564371300246260ustar00rootroot00000000000000from litestar.openapi.plugins import RapidocRenderPlugin rapidoc_plugin = RapidocRenderPlugin(version="9.3.4", path="/rapidoc") litestar-2.16.0/docs/examples/openapi/plugins/rapidoc_simple.py000066400000000000000000000010111500564371300246520ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, get from litestar.openapi.config import OpenAPIConfig from litestar.openapi.plugins import RapidocRenderPlugin @get("/", sync_to_thread=False) def hello_world() -> Dict[str, str]: return {"message": "Hello World"} app = Litestar( route_handlers=[hello_world], openapi_config=OpenAPIConfig( title="Litestar Example", description="Example of litestar", version="0.0.1", render_plugins=[RapidocRenderPlugin()], ), ) litestar-2.16.0/docs/examples/openapi/plugins/receive_router.py000066400000000000000000000012421500564371300247100ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar import get from litestar.enums import MediaType from litestar.openapi.plugins import OpenAPIRenderPlugin if TYPE_CHECKING: from litestar.connection import Request from litestar.router import Router class MyOpenAPIPlugin(OpenAPIRenderPlugin): def render(self, request: Request, openapi_schema: dict[str, str]) -> bytes: return b"My UI of Choice!" def receive_router(self, router: Router) -> None: @get("/something", media_type=MediaType.TEXT) def something() -> str: return "Something" router.register(something) litestar-2.16.0/docs/examples/openapi/plugins/redoc_config.py000066400000000000000000000002131500564371300243040ustar00rootroot00000000000000from litestar.openapi.plugins import RedocRenderPlugin redoc_plugin = RedocRenderPlugin(version="next", google_fonts=True, path="/redoc") litestar-2.16.0/docs/examples/openapi/plugins/redoc_simple.py000066400000000000000000000010051500564371300243300ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, get from litestar.openapi.config import OpenAPIConfig from litestar.openapi.plugins import RedocRenderPlugin @get("/", sync_to_thread=False) def hello_world() -> Dict[str, str]: return {"message": "Hello World"} app = Litestar( route_handlers=[hello_world], openapi_config=OpenAPIConfig( title="Litestar Example", description="Example of litestar", version="0.0.1", render_plugins=[RedocRenderPlugin()], ), ) litestar-2.16.0/docs/examples/openapi/plugins/scalar_config.py000066400000000000000000000001761500564371300244650ustar00rootroot00000000000000from litestar.openapi.plugins import ScalarRenderPlugin scalar_plugin = ScalarRenderPlugin(version="1.19.5", path="/scalar") litestar-2.16.0/docs/examples/openapi/plugins/scalar_customized.py000066400000000000000000000003411500564371300254000ustar00rootroot00000000000000from litestar.openapi.plugins import ScalarRenderPlugin scalar_plugin = ScalarRenderPlugin( js_url="https://example.com/my-custom-scalar.js", css_url="https://example.com/my-custom-scalar.css", path="/scalar", ) litestar-2.16.0/docs/examples/openapi/plugins/scalar_simple.py000066400000000000000000000010401500564371300245000ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, get from litestar.openapi.config import OpenAPIConfig from litestar.openapi.plugins import ScalarRenderPlugin @get("/", sync_to_thread=False) def hello_world() -> Dict[str, str]: return {"message": "Hello World"} app = Litestar( route_handlers=[hello_world], openapi_config=OpenAPIConfig( title="Litestar Example", description="Example of Litestar with Scalar OpenAPI docs", version="0.0.1", render_plugins=[ScalarRenderPlugin()], ), ) litestar-2.16.0/docs/examples/openapi/plugins/serving_multiple_uis.py000066400000000000000000000010651500564371300261410ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, get from litestar.openapi.config import OpenAPIConfig from litestar.openapi.plugins import RapidocRenderPlugin, SwaggerRenderPlugin @get("/", sync_to_thread=False) def hello_world() -> Dict[str, str]: return {"message": "Hello World"} app = Litestar( route_handlers=[hello_world], openapi_config=OpenAPIConfig( title="Litestar Example", description="Example of litestar", version="0.0.1", render_plugins=[RapidocRenderPlugin(), SwaggerRenderPlugin()], ), ) litestar-2.16.0/docs/examples/openapi/plugins/stoplight_config.py000066400000000000000000000002111500564371300252230ustar00rootroot00000000000000from litestar.openapi.plugins import StoplightRenderPlugin stoplight_plugin = StoplightRenderPlugin(version="7.7.18", path="/elements") litestar-2.16.0/docs/examples/openapi/plugins/stoplight_simple.py000066400000000000000000000010151500564371300252520ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, get from litestar.openapi.config import OpenAPIConfig from litestar.openapi.plugins import StoplightRenderPlugin @get("/", sync_to_thread=False) def hello_world() -> Dict[str, str]: return {"message": "Hello World"} app = Litestar( route_handlers=[hello_world], openapi_config=OpenAPIConfig( title="Litestar Example", description="Example of litestar", version="0.0.1", render_plugins=[StoplightRenderPlugin()], ), ) litestar-2.16.0/docs/examples/openapi/plugins/swagger_ui_config.py000066400000000000000000000002021500564371300253420ustar00rootroot00000000000000from litestar.openapi.plugins import SwaggerRenderPlugin swagger_plugin = SwaggerRenderPlugin(version="5.18.2", path="/swagger") litestar-2.16.0/docs/examples/openapi/plugins/swagger_ui_oauth.py000066400000000000000000000016361500564371300252310ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, get from litestar.openapi.config import OpenAPIConfig from litestar.openapi.plugins import SwaggerRenderPlugin @get("/", sync_to_thread=False) def hello_world() -> Dict[str, str]: return {"message": "Hello World"} app = Litestar( route_handlers=[hello_world], openapi_config=OpenAPIConfig( title="Litestar Example", description="Example of litestar", version="0.0.1", render_plugins=[ SwaggerRenderPlugin( init_oauth={ "clientId": "your-client-id", "appName": "your-app-name", "scopeSeparator": " ", "scopes": "openid profile", "useBasicAuthenticationWithAccessCodeGrant": True, "usePkceWithAuthorizationCodeGrant": True, } ) ], ), ) litestar-2.16.0/docs/examples/openapi/plugins/swagger_ui_simple.py000066400000000000000000000010111500564371300253650ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, get from litestar.openapi.config import OpenAPIConfig from litestar.openapi.plugins import SwaggerRenderPlugin @get("/", sync_to_thread=False) def hello_world() -> Dict[str, str]: return {"message": "Hello World"} app = Litestar( route_handlers=[hello_world], openapi_config=OpenAPIConfig( title="Litestar Example", description="Example of litestar", version="0.0.1", render_plugins=[SwaggerRenderPlugin()], ), ) litestar-2.16.0/docs/examples/openapi/plugins/yaml_simple.py000066400000000000000000000010031500564371300241740ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, get from litestar.openapi.config import OpenAPIConfig from litestar.openapi.plugins import YamlRenderPlugin @get("/", sync_to_thread=False) def hello_world() -> Dict[str, str]: return {"message": "Hello World"} app = Litestar( route_handlers=[hello_world], openapi_config=OpenAPIConfig( title="Litestar Example", description="Example of litestar", version="0.0.1", render_plugins=[YamlRenderPlugin()], ), ) litestar-2.16.0/docs/examples/pagination/000077500000000000000000000000001500564371300203325ustar00rootroot00000000000000litestar-2.16.0/docs/examples/pagination/__init__.py000066400000000000000000000000001500564371300224310ustar00rootroot00000000000000litestar-2.16.0/docs/examples/pagination/using_classic_pagination.py000066400000000000000000000026311500564371300257450ustar00rootroot00000000000000from typing import List from polyfactory.factories.pydantic_factory import ModelFactory from pydantic import BaseModel from litestar import Litestar, get from litestar.pagination import AbstractSyncClassicPaginator, ClassicPagination class Person(BaseModel): id: str name: str class PersonFactory(ModelFactory[Person]): __model__ = Person # we will implement a paginator - the paginator must implement two methods 'get_total' and 'get_items' # we would usually use a database for this, but for our case we will "fake" the dataset using a factory. class PersonClassicPaginator(AbstractSyncClassicPaginator[Person]): def __init__(self) -> None: self.data = PersonFactory.batch(50) def get_total(self, page_size: int) -> int: return round(len(self.data) / page_size) def get_items(self, page_size: int, current_page: int) -> List[Person]: return [self.data[i : i + page_size] for i in range(0, len(self.data), page_size)][current_page - 1] paginator = PersonClassicPaginator() # we now create a regular handler. The handler will receive two query parameters - 'page_size' and 'current_page', which # we will pass to the paginator. @get("/people", sync_to_thread=False) def people_handler(page_size: int, current_page: int) -> ClassicPagination[Person]: return paginator(page_size=page_size, current_page=current_page) app = Litestar(route_handlers=[people_handler]) litestar-2.16.0/docs/examples/pagination/using_cursor_pagination.py000066400000000000000000000023301500564371300256350ustar00rootroot00000000000000from typing import List, Optional, Tuple from polyfactory.factories.pydantic_factory import ModelFactory from pydantic import BaseModel from litestar import Litestar, get from litestar.pagination import AbstractSyncCursorPaginator, CursorPagination class Person(BaseModel): id: str name: str class PersonFactory(ModelFactory[Person]): __model__ = Person # we will implement a paginator - the paginator must implement the method 'get_items'. class PersonCursorPaginator(AbstractSyncCursorPaginator[str, Person]): def __init__(self) -> None: self.data = PersonFactory.batch(50) def get_items(self, cursor: Optional[str], results_per_page: int) -> Tuple[List[Person], Optional[str]]: results = self.data[:results_per_page] return results, results[-1].id paginator = PersonCursorPaginator() # we now create a regular handler. The handler will receive a single query parameter - 'cursor', which # we will pass to the paginator. @get("/people", sync_to_thread=False) def people_handler(cursor: Optional[str], results_per_page: int) -> CursorPagination[str, Person]: return paginator(cursor=cursor, results_per_page=results_per_page) app = Litestar(route_handlers=[people_handler]) litestar-2.16.0/docs/examples/pagination/using_offset_pagination.py000066400000000000000000000024621500564371300256140ustar00rootroot00000000000000from itertools import islice from typing import List from polyfactory.factories.pydantic_factory import ModelFactory from pydantic import BaseModel from litestar import Litestar, get from litestar.pagination import AbstractSyncOffsetPaginator, OffsetPagination class Person(BaseModel): id: str name: str class PersonFactory(ModelFactory[Person]): __model__ = Person # we will implement a paginator - the paginator must implement two methods 'get_total' and 'get_items' # we would usually use a database for this, but for our case we will "fake" the dataset using a factory. class PersonOffsetPaginator(AbstractSyncOffsetPaginator[Person]): def __init__(self) -> None: self.data = PersonFactory.batch(50) def get_total(self) -> int: return len(self.data) def get_items(self, limit: int, offset: int) -> List[Person]: return list(islice(islice(self.data, offset, None), limit)) paginator = PersonOffsetPaginator() # we now create a regular handler. The handler will receive two query parameters - 'limit' and 'offset', which # we will pass to the paginator. @get("/people", sync_to_thread=False) def people_handler(limit: int, offset: int) -> OffsetPagination[Person]: return paginator(limit=limit, offset=offset) app = Litestar(route_handlers=[people_handler]) litestar-2.16.0/docs/examples/pagination/using_offset_pagination_with_sqlalchemy.py000066400000000000000000000040741500564371300310720ustar00rootroot00000000000000from typing import TYPE_CHECKING, List, cast from sqlalchemy import func, select from sqlalchemy.orm import Mapped from litestar import Litestar, get from litestar.di import Provide from litestar.pagination import AbstractAsyncOffsetPaginator, OffsetPagination from litestar.plugins.sqlalchemy import SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin, base if TYPE_CHECKING: from sqlalchemy.engine.result import ScalarResult from sqlalchemy.ext.asyncio import AsyncSession class Person(base.UUIDBase): name: Mapped[str] class PersonOffsetPaginator(AbstractAsyncOffsetPaginator[Person]): def __init__(self, async_session: AsyncSession) -> None: # 'async_session' dependency will be injected here. self.async_session = async_session async def get_total(self) -> int: return cast("int", await self.async_session.scalar(select(func.count(Person.id)))) async def get_items(self, limit: int, offset: int) -> List[Person]: people: ScalarResult = await self.async_session.scalars(select(Person).slice(offset, limit)) return list(people.all()) # Create a route handler. The handler will receive two query parameters - 'limit' and 'offset', which is passed # to the paginator instance. Also create a dependency 'paginator' which will be injected into the handler. @get("/people", dependencies={"paginator": Provide(PersonOffsetPaginator)}) async def people_handler(paginator: PersonOffsetPaginator, limit: int, offset: int) -> OffsetPagination[Person]: return await paginator(limit=limit, offset=offset) sqlalchemy_config = SQLAlchemyAsyncConfig( connection_string="sqlite+aiosqlite:///test.sqlite", session_dependency_key="async_session" ) # Create 'async_session' dependency. sqlalchemy_plugin = SQLAlchemyInitPlugin(config=sqlalchemy_config) async def on_startup() -> None: """Initializes the database.""" async with sqlalchemy_config.get_engine().begin() as conn: await conn.run_sync(base.UUIDBase.metadata.create_all) app = Litestar(route_handlers=[people_handler], on_startup=[on_startup], plugins=[sqlalchemy_plugin]) litestar-2.16.0/docs/examples/parameters/000077500000000000000000000000001500564371300203445ustar00rootroot00000000000000litestar-2.16.0/docs/examples/parameters/__init__.py000066400000000000000000000000001500564371300224430ustar00rootroot00000000000000litestar-2.16.0/docs/examples/parameters/header_and_cookie_parameters.py000066400000000000000000000014411500564371300265440ustar00rootroot00000000000000from pydantic import BaseModel from typing_extensions import Annotated from litestar import Litestar, get from litestar.exceptions import NotAuthorizedException from litestar.params import Parameter USER_DB = { 1: { "id": 1, "name": "John Doe", }, } VALID_TOKEN = "super-secret-secret" VALID_COOKIE_VALUE = "cookie-secret" class User(BaseModel): id: int name: str @get(path="/users/{user_id:int}/") async def get_user( user_id: int, token: Annotated[str, Parameter(header="X-API-KEY")], cookie: Annotated[str, Parameter(cookie="my-cookie-param")], ) -> User: if token != VALID_TOKEN or cookie != VALID_COOKIE_VALUE: raise NotAuthorizedException return User.model_validate(USER_DB[user_id]) app = Litestar(route_handlers=[get_user]) litestar-2.16.0/docs/examples/parameters/layered_parameters.py000066400000000000000000000021111500564371300245610ustar00rootroot00000000000000from typing import Dict, Union from typing_extensions import Annotated from litestar import Controller, Litestar, Router, get from litestar.params import Parameter class MyController(Controller): path = "/controller" parameters = { "controller_param": Parameter(int, lt=100), } @get("/{path_param:int}", sync_to_thread=False) def my_handler( self, path_param: int, local_param: str, router_param: str, controller_param: Annotated[int, Parameter(int, lt=50)], ) -> Dict[str, Union[str, int]]: return { "path_param": path_param, "local_param": local_param, "router_param": router_param, "controller_param": controller_param, } router = Router( path="/router", route_handlers=[MyController], parameters={ "router_param": Parameter(str, pattern="^[a-zA-Z]$", header="MyHeader", required=False), }, ) app = Litestar( route_handlers=[router], parameters={ "app_param": Parameter(str, cookie="special-cookie"), }, ) litestar-2.16.0/docs/examples/parameters/path_parameters_1.py000066400000000000000000000005311500564371300243140ustar00rootroot00000000000000from pydantic import BaseModel from litestar import Litestar, get USER_DB = {1: {"id": 1, "name": "John Doe"}} class User(BaseModel): id: int name: str @get("/user/{user_id:int}", sync_to_thread=False) def get_user(user_id: int) -> User: return User.model_validate(USER_DB[user_id]) app = Litestar(route_handlers=[get_user]) litestar-2.16.0/docs/examples/parameters/path_parameters_2.py000066400000000000000000000010531500564371300243150ustar00rootroot00000000000000from datetime import datetime, timezone from typing import List from pydantic import BaseModel from litestar import Litestar, get class Order(BaseModel): id: int customer_id: int ORDERS_BY_DATETIME = { datetime.fromtimestamp(1667924386, tz=timezone.utc): [ Order(id=1, customer_id=2), Order(id=2, customer_id=2), ] } @get(path="/orders/{from_date:int}", sync_to_thread=False) def get_orders(from_date: datetime) -> List[Order]: return ORDERS_BY_DATETIME[from_date] app = Litestar(route_handlers=[get_orders]) litestar-2.16.0/docs/examples/parameters/path_parameters_3.py000066400000000000000000000020651500564371300243220ustar00rootroot00000000000000from pydantic import BaseModel, Json, conint from typing_extensions import Annotated from litestar import Litestar, get from litestar.openapi.spec.example import Example from litestar.openapi.spec.external_documentation import ExternalDocumentation from litestar.params import Parameter class Version(BaseModel): id: conint(ge=1, le=10) # type: ignore[valid-type] specs: Json VERSIONS = {1: Version(id=1, specs='{"some": "value"}')} @get(path="/versions/{version:int}", sync_to_thread=False) def get_product_version( version: Annotated[ int, Parameter( ge=1, le=10, title="Available Product Versions", description="Get a specific version spec from the available specs", examples=[Example(value=1)], external_docs=ExternalDocumentation( url="https://mywebsite.com/documentation/product#versions", # type: ignore[arg-type] ), ), ], ) -> Version: return VERSIONS[version] app = Litestar(route_handlers=[get_product_version]) litestar-2.16.0/docs/examples/parameters/query_params.py000066400000000000000000000003631500564371300234300ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, get @get("/", sync_to_thread=False) def index(param: str) -> Dict[str, str]: return {"param": param} app = Litestar(route_handlers=[index]) # run: /?param=foo # run: /?param=bar litestar-2.16.0/docs/examples/parameters/query_params_constraints.py000066400000000000000000000004671500564371300260640ustar00rootroot00000000000000from typing import Dict from typing_extensions import Annotated from litestar import Litestar, get from litestar.params import Parameter @get("/", sync_to_thread=False) def index(param: Annotated[int, Parameter(gt=5)]) -> Dict[str, int]: return {"param": param} app = Litestar(route_handlers=[index]) litestar-2.16.0/docs/examples/parameters/query_params_default.py000066400000000000000000000003651500564371300251360ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, get @get("/", sync_to_thread=False) def index(param: str = "hello") -> Dict[str, str]: return {"param": param} app = Litestar(route_handlers=[index]) # run: / # run: /?param=john litestar-2.16.0/docs/examples/parameters/query_params_optional.py000066400000000000000000000004231500564371300253320ustar00rootroot00000000000000from typing import Dict, Optional from litestar import Litestar, get @get("/", sync_to_thread=False) def index(param: Optional[str] = None) -> Dict[str, Optional[str]]: return {"param": param} app = Litestar(route_handlers=[index]) # run: / # run: /?param=goodbye litestar-2.16.0/docs/examples/parameters/query_params_remap.py000066400000000000000000000005461500564371300246170ustar00rootroot00000000000000from typing import Dict from typing_extensions import Annotated from litestar import Litestar, get from litestar.params import Parameter @get("/", sync_to_thread=False) def index(snake_case: Annotated[str, Parameter(query="camelCase")]) -> Dict[str, str]: return {"param": snake_case} app = Litestar(route_handlers=[index]) # run: /?camelCase=foo litestar-2.16.0/docs/examples/parameters/query_params_types.py000066400000000000000000000010211500564371300246440ustar00rootroot00000000000000from datetime import datetime, timedelta from typing import Any, Dict, List from litestar import Litestar, get @get("/", sync_to_thread=False) def index(date: datetime, number: int, floating_number: float, strings: List[str]) -> Dict[str, Any]: return { "datetime": date + timedelta(days=1), "int": number, "float": floating_number, "list": strings, } app = Litestar(route_handlers=[index]) # run: /?date=2022-11-28T13:22:06.916540&floating_number=0.1&number=42&strings=1&strings=2 litestar-2.16.0/docs/examples/plugins/000077500000000000000000000000001500564371300176625ustar00rootroot00000000000000litestar-2.16.0/docs/examples/plugins/__init__.py000066400000000000000000000000001500564371300217610ustar00rootroot00000000000000litestar-2.16.0/docs/examples/plugins/di_plugin.py000066400000000000000000000015601500564371300222100ustar00rootroot00000000000000from inspect import Parameter, Signature from typing import Any, Dict, Tuple from litestar import Litestar, get from litestar.di import Provide from litestar.plugins import DIPlugin class MyBaseType: def __init__(self, param): self.param = param class MyDIPlugin(DIPlugin): def has_typed_init(self, type_: Any) -> bool: return issubclass(type_, MyBaseType) def get_typed_init(self, type_: Any) -> Tuple[Signature, Dict[str, Any]]: signature = Signature([Parameter(name="param", kind=Parameter.POSITIONAL_OR_KEYWORD)]) annotations = {"param": str} return signature, annotations @get("/", dependencies={"injected": Provide(MyBaseType, sync_to_thread=False)}) async def handler(injected: MyBaseType) -> str: return injected.param app = Litestar(route_handlers=[handler], plugins=[MyDIPlugin()]) # run: /?param=hello litestar-2.16.0/docs/examples/plugins/flash_messages/000077500000000000000000000000001500564371300226465ustar00rootroot00000000000000litestar-2.16.0/docs/examples/plugins/flash_messages/__init__.py000066400000000000000000000000001500564371300247450ustar00rootroot00000000000000litestar-2.16.0/docs/examples/plugins/flash_messages/jinja.py000066400000000000000000000010341500564371300243110ustar00rootroot00000000000000from litestar import Litestar from litestar.contrib.jinja import JinjaTemplateEngine from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.plugins.flash import FlashConfig, FlashPlugin from litestar.template.config import TemplateConfig template_config = TemplateConfig(engine=JinjaTemplateEngine, directory="templates") flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config)) app = Litestar( plugins=[flash_plugin], middleware=[ServerSideSessionConfig().middleware], ) litestar-2.16.0/docs/examples/plugins/flash_messages/mako.py000066400000000000000000000010311500564371300241420ustar00rootroot00000000000000from litestar import Litestar from litestar.contrib.mako import MakoTemplateEngine from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.plugins.flash import FlashConfig, FlashPlugin from litestar.template.config import TemplateConfig template_config = TemplateConfig(engine=MakoTemplateEngine, directory="templates") flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config)) app = Litestar( plugins=[flash_plugin], middleware=[ServerSideSessionConfig().middleware], ) litestar-2.16.0/docs/examples/plugins/flash_messages/minijinja.py000066400000000000000000000010501500564371300251640ustar00rootroot00000000000000from litestar import Litestar from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.plugins.flash import FlashConfig, FlashPlugin from litestar.template.config import TemplateConfig template_config = TemplateConfig(engine=MiniJinjaTemplateEngine, directory="templates") flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config)) app = Litestar( plugins=[flash_plugin], middleware=[ServerSideSessionConfig().middleware], ) litestar-2.16.0/docs/examples/plugins/flash_messages/usage.py000066400000000000000000000020531500564371300243240ustar00rootroot00000000000000from litestar import Litestar, Request, get from litestar.contrib.jinja import JinjaTemplateEngine from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.plugins.flash import FlashConfig, FlashPlugin, flash from litestar.response import Template from litestar.template.config import TemplateConfig template_config = TemplateConfig(engine=JinjaTemplateEngine, directory="templates") flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config)) @get() async def index(request: Request) -> Template: """Example of adding and displaying a flash message.""" flash(request, "Oh no! I've been flashed!", category="error") return Template( template_str="""

Flash Message Example

{% for message in get_flashes() %}

{{ message.message }} (Category:{{ message.category }})

{% endfor %} """ ) app = Litestar( plugins=[flash_plugin], route_handlers=[index], template_config=template_config, middleware=[ServerSideSessionConfig().middleware], ) litestar-2.16.0/docs/examples/plugins/init_plugin_protocol.py000066400000000000000000000011711500564371300244760ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, get from litestar.config.app import AppConfig from litestar.di import Provide from litestar.plugins import InitPlugin @get("/", sync_to_thread=False) def route_handler(name: str) -> Dict[str, str]: return {"hello": name} def get_name() -> str: return "world" class MyPlugin(InitPlugin): def on_app_init(self, app_config: AppConfig) -> AppConfig: app_config.dependencies["name"] = Provide(get_name, sync_to_thread=False) app_config.route_handlers.append(route_handler) return app_config app = Litestar(plugins=[MyPlugin()]) # run: / litestar-2.16.0/docs/examples/plugins/problem_details/000077500000000000000000000000001500564371300230275ustar00rootroot00000000000000litestar-2.16.0/docs/examples/plugins/problem_details/basic_usage.py000066400000000000000000000017431500564371300256530ustar00rootroot00000000000000from dataclasses import dataclass from litestar import Litestar, post from litestar.plugins.problem_details import ProblemDetailsConfig, ProblemDetailsException, ProblemDetailsPlugin @dataclass class PurchaseItem: item_id: int quantity: int @post("/purchase") async def purchase(data: PurchaseItem) -> None: # Logic to check if the user has enough credit to buy the item. # We assume the user does not have enough credit. raise ProblemDetailsException( type_="https://example.com/probs/out-of-credit", title="You do not have enough credit.", detail="Your current balance is 30, but that costs 50.", instance="/account/12345/msgs/abc", extra={"balance": 30}, ) problem_details_plugin = ProblemDetailsPlugin(ProblemDetailsConfig()) app = Litestar(route_handlers=[purchase], plugins=[problem_details_plugin]) # run: /purchase --header "Content-Type: application/json" --request POST --data '{"item_id": 1234, "quantity": 2}' litestar-2.16.0/docs/examples/plugins/problem_details/convert_exceptions.py000066400000000000000000000030111500564371300273150ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from litestar import Litestar, post from litestar.plugins.problem_details import ProblemDetailsConfig, ProblemDetailsException, ProblemDetailsPlugin @dataclass class PurchaseItem: item_id: int quantity: int class PurchaseNotAllowedError(Exception): def __init__(self, account_id: int, balance: int, detail: str) -> None: self.account_id = account_id self.balance = balance self.detail = detail @post("/purchase") async def purchase(data: PurchaseItem) -> None: raise PurchaseNotAllowedError( account_id=12345, balance=30, detail="Your current balance is 30, but that costs 50.", ) def convert_purchase_not_allowed_to_problem_details(exc: PurchaseNotAllowedError) -> ProblemDetailsException: return ProblemDetailsException( type_="https://example.com/probs/out-of-credit", title="You do not have enough credit.", detail=exc.detail, instance=f"/account/{exc.account_id}/msgs/abc", extra={"balance": exc.balance}, ) problem_details_plugin = ProblemDetailsPlugin( ProblemDetailsConfig( enable_for_all_http_exceptions=True, exception_to_problem_detail_map={PurchaseNotAllowedError: convert_purchase_not_allowed_to_problem_details}, ) ) app = Litestar(route_handlers=[purchase], plugins=[problem_details_plugin]) # run: /purchase --header "Content-Type: application/json" --request POST --data '{"item_id": 1234, "quantity": 2}' litestar-2.16.0/docs/examples/plugins/problem_details/convert_http_exceptions.py000066400000000000000000000015741500564371300303700ustar00rootroot00000000000000from dataclasses import dataclass from litestar import Litestar, post from litestar.exceptions.http_exceptions import NotFoundException from litestar.plugins.problem_details import ProblemDetailsConfig, ProblemDetailsPlugin @dataclass class PurchaseItem: item_id: int quantity: int @post("/purchase") async def purchase(data: PurchaseItem) -> None: # Logic to check if the user has enough credit to buy the item. # We assume the user does not have enough credit. raise NotFoundException(detail="No item with the given ID was found", extra={"item_id": data.item_id}) problem_details_plugin = ProblemDetailsPlugin(ProblemDetailsConfig(enable_for_all_http_exceptions=True)) app = Litestar(route_handlers=[purchase], plugins=[problem_details_plugin]) # run: /purchase --header "Content-Type: application/json" --request POST --data '{"item_id": 1234, "quantity": 2}' litestar-2.16.0/docs/examples/plugins/prometheus/000077500000000000000000000000001500564371300220555ustar00rootroot00000000000000litestar-2.16.0/docs/examples/plugins/prometheus/__init__.py000066400000000000000000000000001500564371300241540ustar00rootroot00000000000000litestar-2.16.0/docs/examples/plugins/prometheus/using_prometheus_exporter.py000066400000000000000000000012441500564371300277600ustar00rootroot00000000000000from litestar import Litestar from litestar.plugins.prometheus import PrometheusConfig, PrometheusController def create_app(group_path: bool = False): # Default app name and prefix is litestar. prometheus_config = PrometheusConfig(group_path=group_path) # By default the metrics are available in prometheus format and the path is set to '/metrics'. # If you want to change the path and format you can do it by subclassing the PrometheusController class. # Creating the litestar app instance with our custom PrometheusConfig and PrometheusController. return Litestar(route_handlers=[PrometheusController], middleware=[prometheus_config.middleware]) litestar-2.16.0/docs/examples/plugins/prometheus/using_prometheus_exporter_with_extra_configs.py000066400000000000000000000031521500564371300337260ustar00rootroot00000000000000from typing import Any, Dict from litestar import Litestar, Request from litestar.plugins.prometheus import PrometheusConfig, PrometheusController # We can modify the path of our custom handler and override the metrics format by subclassing the PrometheusController. class CustomPrometheusController(PrometheusController): path = "/custom-path" openmetrics_format = True # Let's assume this as our extra custom labels which we want our metrics to have. # The values can be either a string or a callable that returns a string. def custom_label_callable(request: Request[Any, Any, Any]) -> str: return "v2.0" extra_labels = { "version_no": custom_label_callable, "location": "earth", } # Customizing the buckets for the histogram. buckets = [0.1, 0.2, 0.3, 0.4, 0.5] # Adding exemplars to the metrics. # Note that this supported only in openmetrics format. def custom_exemplar(request: Request[Any, Any, Any]) -> Dict[str, str]: return {"trace_id": "1234"} # Creating the instance of PrometheusConfig with our own custom options. # The given options are not necessary, you can use the default ones # as well by just creating a raw instance PrometheusConfig() prometheus_config = PrometheusConfig( app_name="litestar-example", prefix="litestar", labels=extra_labels, buckets=buckets, # pyright: ignore[reportArgumentType] exemplars=custom_exemplar, excluded_http_methods=["POST"], ) # Creating the litestar app instance with our custom PrometheusConfig and PrometheusController. app = Litestar(route_handlers=[CustomPrometheusController], middleware=[prometheus_config.middleware]) litestar-2.16.0/docs/examples/plugins/sqlalchemy/000077500000000000000000000000001500564371300220245ustar00rootroot00000000000000litestar-2.16.0/docs/examples/plugins/sqlalchemy/configure.py000066400000000000000000000003441500564371300243600ustar00rootroot00000000000000from litestar.plugins.sqlalchemy import SQLAlchemyAsyncConfig, SQLAlchemyPlugin sqlalchemy_config = SQLAlchemyAsyncConfig(connection_string="sqlite+aiosqlite:///test.sqlite") plugin = SQLAlchemyPlugin(config=sqlalchemy_config) litestar-2.16.0/docs/examples/plugins/sqlalchemy/modelling.py000066400000000000000000000002371500564371300243520ustar00rootroot00000000000000from sqlalchemy.orm import Mapped from litestar.plugins.sqlalchemy import base class TodoItem(base.UUIDBase): title: Mapped[str] done: Mapped[bool] litestar-2.16.0/docs/examples/plugins/sqlalchemy_init_plugin/000077500000000000000000000000001500564371300244255ustar00rootroot00000000000000litestar-2.16.0/docs/examples/plugins/sqlalchemy_init_plugin/__init__.py000066400000000000000000000000001500564371300265240ustar00rootroot00000000000000litestar-2.16.0/docs/examples/plugins/sqlalchemy_init_plugin/sqlalchemy_async.py000066400000000000000000000016101500564371300303340ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy import text from litestar import Litestar, get from litestar.plugins.sqlalchemy import SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession @get(path="/sqlalchemy-app") async def async_sqlalchemy_init(db_session: AsyncSession, db_engine: AsyncEngine) -> str: """Interact with SQLAlchemy engine and session.""" one = (await db_session.execute(text("SELECT 1"))).scalar_one() async with db_engine.begin() as conn: two = (await conn.execute(text("SELECT 2"))).scalar_one() return f"{one} {two}" sqlalchemy_config = SQLAlchemyAsyncConfig(connection_string="sqlite+aiosqlite:///test.sqlite") app = Litestar( route_handlers=[async_sqlalchemy_init], plugins=[SQLAlchemyInitPlugin(config=sqlalchemy_config)], ) litestar-2.16.0/docs/examples/plugins/sqlalchemy_init_plugin/sqlalchemy_sync.py000066400000000000000000000015651500564371300302040ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from sqlalchemy import text from litestar import Litestar, get from litestar.plugins.sqlalchemy import SQLAlchemyInitPlugin, SQLAlchemySyncConfig if TYPE_CHECKING: from sqlalchemy import Engine from sqlalchemy.orm import Session @get(path="/sqlalchemy-app", sync_to_thread=True) def async_sqlalchemy_init(db_session: Session, db_engine: Engine) -> str: """Interact with SQLAlchemy engine and session.""" one = db_session.execute(text("SELECT 1")).scalar_one() with db_engine.connect() as conn: two = conn.execute(text("SELECT 2")).scalar_one() return f"{one} {two}" sqlalchemy_config = SQLAlchemySyncConfig(connection_string="sqlite:///test.sqlite") app = Litestar( route_handlers=[async_sqlalchemy_init], plugins=[SQLAlchemyInitPlugin(config=sqlalchemy_config)], ) litestar-2.16.0/docs/examples/request_data/000077500000000000000000000000001500564371300206625ustar00rootroot00000000000000litestar-2.16.0/docs/examples/request_data/__init__.py000066400000000000000000000000001500564371300227610ustar00rootroot00000000000000litestar-2.16.0/docs/examples/request_data/custom_request.py000066400000000000000000000016641500564371300243250ustar00rootroot00000000000000from litestar import Litestar, Request, get from litestar.connection.base import empty_receive, empty_send from litestar.enums import HttpMethod from litestar.types import Receive, Scope, Send KITTEN_NAMES_MAP = { HttpMethod.GET: "Whiskers", } class CustomRequest(Request): """Enrich request with the kitten name.""" __slots__ = ("kitten_name",) def __init__(self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send) -> None: """Initialize CustomRequest class.""" super().__init__(scope=scope, receive=receive, send=send) self.kitten_name = KITTEN_NAMES_MAP.get(scope["method"], "Mittens") @get(path="/kitten-name", sync_to_thread=False) def get_kitten_name(request: CustomRequest) -> str: """Get kitten name based on the HTTP method.""" return request.kitten_name app = Litestar( route_handlers=[get_kitten_name], request_class=CustomRequest, debug=True, ) litestar-2.16.0/docs/examples/request_data/msgpack_request.py000066400000000000000000000010151500564371300244260ustar00rootroot00000000000000from typing import Any, Dict from typing_extensions import Annotated from litestar import Litestar, post from litestar.enums import RequestEncodingType from litestar.params import Body @post(path="/", sync_to_thread=False) def msgpack_handler( data: Annotated[Dict[str, Any], Body(media_type=RequestEncodingType.MESSAGEPACK)], ) -> Dict[str, Any]: # This will try to parse the request body as `MessagePack` regardless of the # `Content-Type` return data app = Litestar(route_handlers=[msgpack_handler]) litestar-2.16.0/docs/examples/request_data/request_data_1.py000066400000000000000000000003011500564371300241270ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, post @post(path="/") async def index(data: Dict[str, str]) -> Dict[str, str]: return data app = Litestar(route_handlers=[index]) litestar-2.16.0/docs/examples/request_data/request_data_10.py000066400000000000000000000012111500564371300242100ustar00rootroot00000000000000from typing import Any, Dict, List, Tuple from typing_extensions import Annotated from litestar import Litestar, post from litestar.datastructures import UploadFile from litestar.enums import RequestEncodingType from litestar.params import Body @post(path="/") async def handle_file_upload( data: Annotated[List[UploadFile], Body(media_type=RequestEncodingType.MULTI_PART)], ) -> Dict[str, Tuple[str, str, Any]]: result = {} for file in data: content = await file.read() result[file.filename] = (len(content), file.content_type, file.headers) return result app = Litestar(route_handlers=[handle_file_upload]) litestar-2.16.0/docs/examples/request_data/request_data_2.py000066400000000000000000000003521500564371300241360ustar00rootroot00000000000000from dataclasses import dataclass from litestar import Litestar, post @dataclass class User: id: int name: str @post(path="/") async def index(data: User) -> User: return data app = Litestar(route_handlers=[index]) litestar-2.16.0/docs/examples/request_data/request_data_3.py000066400000000000000000000006171500564371300241430ustar00rootroot00000000000000from dataclasses import dataclass from typing_extensions import Annotated from litestar import Litestar, post from litestar.params import Body @dataclass class User: id: int name: str @post(path="/") async def create_user( data: Annotated[User, Body(title="Create User", description="Create a new user.")], ) -> User: return data app = Litestar(route_handlers=[create_user]) litestar-2.16.0/docs/examples/request_data/request_data_4.py000066400000000000000000000006631500564371300241450ustar00rootroot00000000000000from dataclasses import dataclass from typing_extensions import Annotated from litestar import Litestar, post from litestar.enums import RequestEncodingType from litestar.params import Body @dataclass class User: id: int name: str @post(path="/") async def create_user( data: Annotated[User, Body(media_type=RequestEncodingType.URL_ENCODED)], ) -> User: return data app = Litestar(route_handlers=[create_user]) litestar-2.16.0/docs/examples/request_data/request_data_5.py000066400000000000000000000013121500564371300241360ustar00rootroot00000000000000from dataclasses import dataclass from typing import Dict from typing_extensions import Annotated from litestar import Litestar, post from litestar.datastructures import UploadFile from litestar.enums import RequestEncodingType from litestar.params import Body @dataclass class User: id: int name: str form_input_name: UploadFile @post(path="/") async def create_user( data: Annotated[User, Body(media_type=RequestEncodingType.MULTI_PART)], ) -> Dict[str, str]: content = await data.form_input_name.read() filename = data.form_input_name.filename return {"id": data.id, "name": data.name, "filename": filename, "size": len(content)} app = Litestar(route_handlers=[create_user]) litestar-2.16.0/docs/examples/request_data/request_data_6.py000066400000000000000000000010411500564371300241360ustar00rootroot00000000000000from typing_extensions import Annotated from litestar import Litestar, MediaType, post from litestar.datastructures import UploadFile from litestar.enums import RequestEncodingType from litestar.params import Body @post(path="/", media_type=MediaType.TEXT) async def handle_file_upload( data: Annotated[UploadFile, Body(media_type=RequestEncodingType.MULTI_PART)], ) -> str: content = await data.read() filename = data.filename return f"{filename},length: {len(content)}" app = Litestar(route_handlers=[handle_file_upload]) litestar-2.16.0/docs/examples/request_data/request_data_7.py000066400000000000000000000010601500564371300241400ustar00rootroot00000000000000from typing_extensions import Annotated from litestar import Litestar, MediaType, post from litestar.datastructures import UploadFile from litestar.enums import RequestEncodingType from litestar.params import Body @post(path="/", media_type=MediaType.TEXT, sync_to_thread=False) def handle_file_upload( data: Annotated[UploadFile, Body(media_type=RequestEncodingType.MULTI_PART)], ) -> str: content = data.file.read() filename = data.filename return f"{filename},length: {len(content)}" app = Litestar(route_handlers=[handle_file_upload]) litestar-2.16.0/docs/examples/request_data/request_data_8.py000066400000000000000000000014021500564371300241410ustar00rootroot00000000000000from typing import Dict from pydantic import BaseModel, ConfigDict from typing_extensions import Annotated from litestar import Litestar, post from litestar.datastructures import UploadFile from litestar.enums import RequestEncodingType from litestar.params import Body class FormData(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) cv: UploadFile diploma: UploadFile @post(path="/") async def handle_file_upload( data: Annotated[FormData, Body(media_type=RequestEncodingType.MULTI_PART)], ) -> Dict[str, str]: cv_content = await data.cv.read() diploma_content = await data.diploma.read() return {"cv": cv_content.decode(), "diploma": diploma_content.decode()} app = Litestar(route_handlers=[handle_file_upload]) litestar-2.16.0/docs/examples/request_data/request_data_9.py000066400000000000000000000011521500564371300241440ustar00rootroot00000000000000from typing import Dict from typing_extensions import Annotated from litestar import Litestar, post from litestar.datastructures import UploadFile from litestar.enums import RequestEncodingType from litestar.params import Body @post(path="/") async def handle_file_upload( data: Annotated[Dict[str, UploadFile], Body(media_type=RequestEncodingType.MULTI_PART)], ) -> Dict[str, str]: file_contents = {} for name, file in data.items(): content = await file.read() file_contents[file.filename] = len(content) return file_contents app = Litestar(route_handlers=[handle_file_upload]) litestar-2.16.0/docs/examples/responses/000077500000000000000000000000001500564371300202225ustar00rootroot00000000000000litestar-2.16.0/docs/examples/responses/__init__.py000066400000000000000000000000001500564371300223210ustar00rootroot00000000000000litestar-2.16.0/docs/examples/responses/background_tasks_1.py000066400000000000000000000010711500564371300243370ustar00rootroot00000000000000import logging from typing import Dict from litestar import Litestar, Response, get from litestar.background_tasks import BackgroundTask logger = logging.getLogger(__name__) async def logging_task(identifier: str, message: str) -> None: logger.info("%s: %s", identifier, message) @get("/", sync_to_thread=False) def greeter(name: str) -> Response[Dict[str, str]]: return Response( {"hello": name}, background=BackgroundTask(logging_task, "greeter", message=f"was called with name {name}"), ) app = Litestar(route_handlers=[greeter]) litestar-2.16.0/docs/examples/responses/background_tasks_2.py000066400000000000000000000007541500564371300243470ustar00rootroot00000000000000import logging from typing import Dict from litestar import Litestar, get from litestar.background_tasks import BackgroundTask logger = logging.getLogger(__name__) async def logging_task(identifier: str, message: str) -> None: logger.info("%s: %s", identifier, message) @get("/", background=BackgroundTask(logging_task, "greeter", message="was called"), sync_to_thread=False) def greeter() -> Dict[str, str]: return {"hello": "world"} app = Litestar(route_handlers=[greeter]) litestar-2.16.0/docs/examples/responses/background_tasks_3.py000066400000000000000000000013171500564371300243440ustar00rootroot00000000000000import logging from typing import Dict from litestar import Litestar, Response, get from litestar.background_tasks import BackgroundTask, BackgroundTasks logger = logging.getLogger(__name__) greeted = set() async def logging_task(name: str) -> None: logger.info("%s was greeted", name) async def saving_task(name: str) -> None: greeted.add(name) @get("/", sync_to_thread=False) def greeter(name: str) -> Response[Dict[str, str]]: return Response( {"hello": name}, background=BackgroundTasks( [ BackgroundTask(logging_task, name), BackgroundTask(saving_task, name), ] ), ) app = Litestar(route_handlers=[greeter]) litestar-2.16.0/docs/examples/responses/custom_responses.py000066400000000000000000000005361500564371300242130ustar00rootroot00000000000000from litestar import Litestar, Response, get from litestar.datastructures import MultiDict class MultiDictResponse(Response): type_encoders = {MultiDict: lambda d: d.dict()} @get("/") async def index() -> MultiDict: return MultiDict([("foo", "bar"), ("foo", "baz")]) app = Litestar([index], response_class=MultiDictResponse) # run: / litestar-2.16.0/docs/examples/responses/json_suffix_responses.py000066400000000000000000000007711500564371300252370ustar00rootroot00000000000000from typing import Any, Dict import litestar.status_codes from litestar import Litestar, get @get( "/resources", status_code=litestar.status_codes.HTTP_418_IM_A_TEAPOT, media_type="application/vnd.example.resource+json", ) async def retrieve_resource() -> Dict[str, Any]: return { "title": "Server thinks it is a teapot", "type": "Server delusion", "status": litestar.status_codes.HTTP_418_IM_A_TEAPOT, } app = Litestar(route_handlers=[retrieve_resource]) litestar-2.16.0/docs/examples/responses/response_content.py000066400000000000000000000014771500564371300241750ustar00rootroot00000000000000from litestar import Litestar, MediaType, Request, Response, get @get("/resource", sync_to_thread=False) def retrieve_resource(request: Request) -> Response[bytes]: provided_types = [MediaType.TEXT, MediaType.HTML, "application/xml"] preferred_type = request.accept.best_match(provided_types, default=MediaType.TEXT) content = None if preferred_type == MediaType.TEXT: content = b"Hello World!" elif preferred_type == MediaType.HTML: content = b"

Hello World!

" elif preferred_type == "application/xml": content = b"Hello World!" return Response(content=content, media_type=preferred_type) app = Litestar(route_handlers=[retrieve_resource]) # run: /resource # run: /resource -H "Accept: text/html" # run: /resource -H "Accept: application/xml" litestar-2.16.0/docs/examples/responses/response_cookies_1.py000066400000000000000000000020511500564371300243640ustar00rootroot00000000000000from litestar import Controller, Litestar, MediaType, Router, get from litestar.datastructures import Cookie class MyController(Controller): path = "/controller-path" response_cookies = [ Cookie( key="controller-cookie", value="controller value", description="controller level cookie", ) ] @get( path="/", response_cookies=[ Cookie( key="local-cookie", value="local value", description="route handler level cookie", ) ], media_type=MediaType.TEXT, sync_to_thread=False, ) def my_route_handler(self) -> str: return "hello world" router = Router( path="/router-path", route_handlers=[MyController], response_cookies=[Cookie(key="router-cookie", value="router value", description="router level cookie")], ) app = Litestar( route_handlers=[router], response_cookies=[Cookie(key="app-cookie", value="app value", description="app level cookie")], ) litestar-2.16.0/docs/examples/responses/response_cookies_2.py000066400000000000000000000007741500564371300243770ustar00rootroot00000000000000from litestar import Controller, Litestar, MediaType, get from litestar.datastructures import Cookie class MyController(Controller): path = "/controller-path" response_cookies = [Cookie(key="my-cookie", value="123")] @get( path="/", response_cookies=[Cookie(key="my-cookie", value="456")], media_type=MediaType.TEXT, sync_to_thread=False, ) def my_route_handler(self) -> str: return "hello world" app = Litestar(route_handlers=[MyController]) litestar-2.16.0/docs/examples/responses/response_cookies_3.py000066400000000000000000000013301500564371300243650ustar00rootroot00000000000000from random import randint from pydantic import BaseModel from litestar import Litestar, Response, get from litestar.datastructures import Cookie class Resource(BaseModel): id: int name: str @get( "/resources", response_cookies=[ Cookie( key="Random-Cookie", description="a random number in the range 1 - 100", documentation_only=True, ) ], sync_to_thread=False, ) def retrieve_resource() -> Response[Resource]: return Response( Resource( id=1, name="my resource", ), cookies=[Cookie(key="Random-Cookie", value=str(randint(1, 100)))], ) app = Litestar(route_handlers=[retrieve_resource]) litestar-2.16.0/docs/examples/responses/response_cookies_4.py000066400000000000000000000015501500564371300243720ustar00rootroot00000000000000from random import randint from pydantic import BaseModel from litestar import Litestar, Response, Router, get from litestar.datastructures import Cookie class Resource(BaseModel): id: int name: str @get("/resources", sync_to_thread=False) def retrieve_resource() -> Resource: return Resource( id=1, name="my resource", ) def after_request_handler(response: Response) -> Response: response.set_cookie(key="Random-Cookie", value=str(randint(1, 100))) return response router = Router( path="/router-path", route_handlers=[retrieve_resource], after_request=after_request_handler, response_cookies=[ Cookie( key="Random-Cookie", description="a random number in the range 1 - 100", documentation_only=True, ) ], ) app = Litestar(route_handlers=[router]) litestar-2.16.0/docs/examples/responses/response_cookies_5.py000066400000000000000000000022611500564371300243730ustar00rootroot00000000000000from random import randint from pydantic import BaseModel from litestar import Litestar, Response, Router, get from litestar.datastructures import Cookie class Resource(BaseModel): id: int name: str @get( "/resources", response_cookies=[ Cookie( key="Random-Cookie", description="a random number in the range 100 - 1000", documentation_only=True, ) ], sync_to_thread=False, ) def retrieve_resource() -> Response[Resource]: return Response( Resource( id=1, name="my resource", ), cookies=[Cookie(key="Random-Cookie", value=str(randint(100, 1000)))], ) def after_request_handler(response: Response) -> Response: response.set_cookie(key="Random-Cookie", value=str(randint(1, 100))) return response router = Router( path="/router-path", route_handlers=[retrieve_resource], after_request=after_request_handler, response_cookies=[ Cookie( key="Random-Cookie", description="a random number in the range 1 - 100", documentation_only=True, ) ], ) app = Litestar(route_handlers=[router]) litestar-2.16.0/docs/examples/responses/response_headers_1.py000066400000000000000000000020221500564371300243410ustar00rootroot00000000000000from litestar import Controller, Litestar, MediaType, Router, get from litestar.datastructures import ResponseHeader class MyController(Controller): path = "/controller-path" response_headers = [ ResponseHeader(name="controller-level-header", value="controller header", description="controller level header") ] @get( path="/handler-path", response_headers=[ ResponseHeader(name="my-local-header", value="local header", description="local level header") ], media_type=MediaType.TEXT, sync_to_thread=False, ) def my_route_handler(self) -> str: return "hello world" router = Router( path="/router-path", route_handlers=[MyController], response_headers=[ ResponseHeader(name="router-level-header", value="router header", description="router level header") ], ) app = Litestar( route_handlers=[router], response_headers=[ResponseHeader(name="app-level-header", value="app header", description="app level header")], ) litestar-2.16.0/docs/examples/responses/response_headers_2.py000066400000000000000000000012761500564371300243540ustar00rootroot00000000000000from random import randint from pydantic import BaseModel from litestar import Litestar, Response, get from litestar.datastructures import ResponseHeader class Resource(BaseModel): id: int name: str @get( "/resources", response_headers=[ ResponseHeader( name="Random-Header", description="a random number in the range 1 - 100", documentation_only=True ) ], sync_to_thread=False, ) def retrieve_resource() -> Response[Resource]: return Response( Resource( id=1, name="my resource", ), headers={"Random-Header": str(randint(1, 100))}, ) app = Litestar(route_handlers=[retrieve_resource]) litestar-2.16.0/docs/examples/responses/response_headers_3.py000066400000000000000000000022661500564371300243550ustar00rootroot00000000000000from random import randint from pydantic import BaseModel from litestar import Litestar, Response, Router, get from litestar.datastructures import ResponseHeader class Resource(BaseModel): id: int name: str @get( "/resources", response_headers=[ ResponseHeader( name="Random-Header", description="a random number in the range 100 - 1000", documentation_only=True, ) ], sync_to_thread=False, ) def retrieve_resource() -> Response[Resource]: return Response( Resource( id=1, name="my resource", ), headers={"Random-Header": str(randint(100, 1000))}, ) def after_request_handler(response: Response) -> Response: response.headers.update({"Random-Header": str(randint(1, 100))}) return response router = Router( path="/router-path", route_handlers=[retrieve_resource], after_request=after_request_handler, response_headers=[ ResponseHeader( name="Random-Header", description="a random number in the range 1 - 100", documentation_only=True, ) ], ) app = Litestar(route_handlers=[router]) litestar-2.16.0/docs/examples/responses/response_headers_4.py000066400000000000000000000022341500564371300243510ustar00rootroot00000000000000from random import randint from pydantic import BaseModel from litestar import Litestar, Response, Router, get from litestar.datastructures import ResponseHeader class Resource(BaseModel): id: int name: str @get( "/resources", response_headers=[ ResponseHeader( name="Random-Header", description="a random number in the range 100 - 1000", documentation_only=True, ) ], sync_to_thread=False, ) def retrieve_resource() -> Response[Resource]: return Response( Resource( id=1, name="my resource", ), headers={"Random-Header": str(randint(100, 1000))}, ) def after_request_handler(response: Response) -> Response: response.headers.update({"Random-Header": str(randint(1, 100))}) return response router = Router( path="/router-path", route_handlers=[retrieve_resource], after_request=after_request_handler, response_headers=[ ResponseHeader( name="Random-Header", description="a random number in the range 1 - 100", documentation_only=True ) ], ) app = Litestar(route_handlers=[router]) litestar-2.16.0/docs/examples/responses/returning_responses.py000066400000000000000000000010061500564371300247070ustar00rootroot00000000000000from pydantic import BaseModel from litestar import Litestar, Response, get from litestar.datastructures import Cookie class Resource(BaseModel): id: int name: str @get("/resources", sync_to_thread=False) def retrieve_resource() -> Response[Resource]: return Response( Resource( id=1, name="my resource", ), headers={"MY-HEADER": "xyz"}, cookies=[Cookie(key="my-cookie", value="abc")], ) app = Litestar(route_handlers=[retrieve_resource]) litestar-2.16.0/docs/examples/responses/sse_responses.py000066400000000000000000000021401500564371300234640ustar00rootroot00000000000000from asyncio import sleep from typing import AsyncGenerator from litestar import Litestar, get from litestar.response import ServerSentEvent, ServerSentEventMessage from litestar.types import SSEData async def my_generator() -> AsyncGenerator[SSEData, None]: count = 0 while count < 10: await sleep(0.01) count += 1 # In the generator you can yield integers, strings, bytes, dictionaries, or ServerSentEventMessage objects # dicts can have the following keys: data, event, id, retry, comment # here we yield an integer yield count # here a string yield str(count) # here bytes yield str(count).encode("utf-8") # here a dictionary yield {"data": 2 * count, "event": "event2", "retry": 10} # here a ServerSentEventMessage object yield ServerSentEventMessage(event="something-with-comment", retry=1000, comment="some comment") @get(path="/count", sync_to_thread=False) def sse_handler() -> ServerSentEvent: return ServerSentEvent(my_generator()) app = Litestar(route_handlers=[sse_handler]) litestar-2.16.0/docs/examples/responses/streaming_responses.py000066400000000000000000000007661500564371300246770ustar00rootroot00000000000000from asyncio import sleep from datetime import datetime from typing import AsyncGenerator from litestar import Litestar, get from litestar.response import Stream from litestar.serialization import encode_json async def my_generator() -> AsyncGenerator[bytes, None]: while True: await sleep(0.01) yield encode_json({"current_time": datetime.now()}) @get(path="/time") def stream_time() -> Stream: return Stream(my_generator()) app = Litestar(route_handlers=[stream_time]) litestar-2.16.0/docs/examples/routing/000077500000000000000000000000001500564371300176705ustar00rootroot00000000000000litestar-2.16.0/docs/examples/routing/__init__.py000066400000000000000000000000001500564371300217670ustar00rootroot00000000000000litestar-2.16.0/docs/examples/routing/mount_custom_app.py000066400000000000000000000013151500564371300236360ustar00rootroot00000000000000import json from typing import TYPE_CHECKING from litestar import Litestar, asgi from litestar.response.base import ASGIResponse if TYPE_CHECKING: from litestar.types import Receive, Scope, Send @asgi("/some/sub-path", is_mount=True, copy_scope=True) async def my_asgi_app(scope: "Scope", receive: "Receive", send: "Send") -> None: """ Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ body = json.dumps({"forwarded_path": scope["path"]}) response = ASGIResponse(body=body.encode("utf-8")) await response(scope, receive, send) app = Litestar(route_handlers=[my_asgi_app]) litestar-2.16.0/docs/examples/routing/mounting_starlette_app.py000066400000000000000000000013101500564371300250240ustar00rootroot00000000000000from typing import TYPE_CHECKING from starlette.applications import Starlette from starlette.responses import JSONResponse from starlette.routing import Route from litestar import Litestar, asgi if TYPE_CHECKING: from starlette.requests import Request async def index(request: "Request") -> JSONResponse: """A generic starlette handler.""" return JSONResponse({"forwarded_path": request.url.path}) starlette_app = asgi(path="/some/sub-path", is_mount=True, copy_scope=True)( Starlette( routes=[ Route("/", index), Route("/abc/", index), Route("/123/another/sub-path/", index), ], ) ) app = Litestar(route_handlers=[starlette_app]) litestar-2.16.0/docs/examples/security/000077500000000000000000000000001500564371300200505ustar00rootroot00000000000000litestar-2.16.0/docs/examples/security/__init__.py000066400000000000000000000000001500564371300221470ustar00rootroot00000000000000litestar-2.16.0/docs/examples/security/guards.py000066400000000000000000000030561500564371300217130ustar00rootroot00000000000000from enum import Enum from os import environ from pydantic import UUID4, BaseModel from litestar import Controller, Litestar, Router, get, post from litestar.connection import ASGIConnection from litestar.exceptions import NotAuthorizedException from litestar.handlers.base import BaseRouteHandler class UserRole(str, Enum): CONSUMER = "consumer" ADMIN = "admin" class User(BaseModel): id: UUID4 role: UserRole @property def is_admin(self) -> bool: """Determines whether the user is an admin user""" return self.role == UserRole.ADMIN def admin_user_guard(connection: ASGIConnection, _: BaseRouteHandler) -> None: if not connection.user.is_admin: raise NotAuthorizedException() @post(path="/user", guards=[admin_user_guard]) def create_user(data: User) -> User: ... def my_guard(connection: ASGIConnection, handler: BaseRouteHandler) -> None: ... # controller class UserController(Controller): path = "/user" guards = [my_guard] # router admin_router = Router(path="admin", route_handlers=[UserController], guards=[my_guard]) # app app = Litestar(route_handlers=[admin_router], guards=[my_guard]) def secret_token_guard(connection: ASGIConnection, route_handler: BaseRouteHandler) -> None: if ( route_handler.opt.get("secret") and not connection.headers.get("Secret-Header", "") == route_handler.opt["secret"] ): raise NotAuthorizedException() @get(path="/secret", guards=[secret_token_guard], opt={"secret": environ.get("SECRET")}) def secret_endpoint() -> None: ... litestar-2.16.0/docs/examples/security/jwt/000077500000000000000000000000001500564371300206545ustar00rootroot00000000000000litestar-2.16.0/docs/examples/security/jwt/__init__.py000066400000000000000000000000001500564371300227530ustar00rootroot00000000000000litestar-2.16.0/docs/examples/security/jwt/custom_decode_payload.py000066400000000000000000000014701500564371300255560ustar00rootroot00000000000000import dataclasses from typing import Any, List, Optional, Sequence, Union from litestar.security.jwt.token import JWTDecodeOptions, Token @dataclasses.dataclass class CustomToken(Token): @classmethod def decode_payload( cls, encoded_token: str, secret: str, algorithms: List[str], issuer: Optional[List[str]] = None, audience: Union[str, Sequence[str], None] = None, options: Optional[JWTDecodeOptions] = None, ) -> Any: payload = super().decode_payload( encoded_token=encoded_token, secret=secret, algorithms=algorithms, issuer=issuer, audience=audience, options=options, ) payload["sub"] = payload["sub"].split("@", maxsplit=1)[1] return payload litestar-2.16.0/docs/examples/security/jwt/custom_token_cls.py000066400000000000000000000015051500564371300246020ustar00rootroot00000000000000import dataclasses import secrets from typing import Any, Dict from litestar import Litestar, Request, get from litestar.connection import ASGIConnection from litestar.security.jwt import JWTAuth, Token @dataclasses.dataclass class CustomToken(Token): token_flag: bool = False @dataclasses.dataclass class User: id: str async def retrieve_user_handler(token: CustomToken, connection: ASGIConnection) -> User: return User(id=token.sub) TOKEN_SECRET = secrets.token_hex() jwt_auth = JWTAuth[User]( token_secret=TOKEN_SECRET, retrieve_user_handler=retrieve_user_handler, token_cls=CustomToken, ) @get("/") def handler(request: Request[User, CustomToken, Any]) -> Dict[str, Any]: return {"id": request.user.id, "token_flag": request.auth.token_flag} app = Litestar(middleware=[jwt_auth.middleware]) litestar-2.16.0/docs/examples/security/jwt/using_jwt_auth.py000066400000000000000000000053651500564371300242710ustar00rootroot00000000000000from os import environ from typing import Any, Dict, Optional from uuid import UUID from pydantic import BaseModel, EmailStr from litestar import Litestar, Request, Response, get, post from litestar.connection import ASGIConnection from litestar.openapi.config import OpenAPIConfig from litestar.security.jwt import JWTAuth, Token # Let's assume we have a User model that is a pydantic model. # This though is not required - we need some sort of user class - # but it can be any arbitrary value, e.g. an SQLAlchemy model, a representation of a MongoDB etc. class User(BaseModel): id: UUID name: str email: EmailStr MOCK_DB: Dict[str, User] = {} # JWTAuth requires a retrieve handler callable that receives the JWT token model and the ASGI connection # and returns the 'User' instance correlating to it. # # Notes: # - 'User' can be any arbitrary value you decide upon. # - The callable can be either sync or async - both will work. async def retrieve_user_handler(token: Token, connection: "ASGIConnection[Any, Any, Any, Any]") -> Optional[User]: # logic here to retrieve the user instance return MOCK_DB.get(token.sub) jwt_auth = JWTAuth[User]( retrieve_user_handler=retrieve_user_handler, token_secret=environ.get("JWT_SECRET", "abcd123"), # we are specifying which endpoints should be excluded from authentication. In this case the login endpoint # and our openAPI docs. exclude=["/login", "/schema"], ) # Given an instance of 'JWTAuth' we can create a login handler function: @post("/login") async def login_handler(data: User) -> Response[User]: MOCK_DB[str(data.id)] = data # you can do whatever you want to update the response instance here # e.g. response.set_cookie(...) return jwt_auth.login(identifier=str(data.id), token_extras={"email": data.email}, response_body=data) # We also have some other routes, for example: @get("/some-path", sync_to_thread=False) def some_route_handler(request: "Request[User, Token, Any]") -> Any: # request.user is set to the instance of user returned by the middleware assert isinstance(request.user, User) # request.auth is the instance of 'litestar_jwt.Token' created from the data encoded in the auth header assert isinstance(request.auth, Token) # do stuff ... # We create our OpenAPIConfig as usual - the JWT security scheme will be injected into it. openapi_config = OpenAPIConfig( title="My API", version="1.0.0", ) # We initialize the app instance and pass the jwt_auth 'on_app_init' handler to the constructor. # The hook handler will inject the JWT middleware and openapi configuration into the app. app = Litestar( route_handlers=[login_handler, some_route_handler], on_app_init=[jwt_auth.on_app_init], openapi_config=openapi_config, ) litestar-2.16.0/docs/examples/security/jwt/using_jwt_cookie_auth.py000066400000000000000000000054641500564371300256220ustar00rootroot00000000000000from os import environ from typing import Any, Dict, Optional from uuid import UUID from pydantic import BaseModel, EmailStr from litestar import Litestar, Request, Response, get, post from litestar.connection import ASGIConnection from litestar.openapi.config import OpenAPIConfig from litestar.security.jwt import JWTCookieAuth, Token # Let's assume we have a User model that is a pydantic model. # This though is not required - we need some sort of user class - # but it can be any arbitrary value, e.g. an SQLAlchemy model, a representation of a MongoDB etc. class User(BaseModel): id: UUID name: str email: EmailStr MOCK_DB: Dict[str, User] = {} # JWTCookieAuth requires a retrieve handler callable that receives the JWT token model and the ASGI connection # and returns the 'User' instance correlating to it. # # Notes: # - 'User' can be any arbitrary value you decide upon. # - The callable can be either sync or async - both will work. async def retrieve_user_handler(token: "Token", connection: "ASGIConnection[Any, Any, Any, Any]") -> Optional[User]: # logic here to retrieve the user instance return MOCK_DB.get(token.sub) jwt_cookie_auth = JWTCookieAuth[User]( retrieve_user_handler=retrieve_user_handler, token_secret=environ.get("JWT_SECRET", "abcd123"), # we are specifying which endpoints should be excluded from authentication. In this case the login endpoint # and our openAPI docs. exclude=["/login", "/schema"], # Tip: We can optionally supply cookie options to the configuration. Here is an example of enabling the secure cookie option # secure=True, ) # Given an instance of 'JWTCookieAuth' we can create a login handler function: @post("/login") async def login_handler(data: "User") -> "Response[User]": MOCK_DB[str(data.id)] = data return jwt_cookie_auth.login(identifier=str(data.id), response_body=data) # We also have some other routes, for example: @get("/some-path", sync_to_thread=False) def some_route_handler(request: "Request[User, Token, Any]") -> Any: # request.user is set to the instance of user returned by the middleware assert isinstance(request.user, User) # request.auth is the instance of 'litestar_jwt.Token' created from the data encoded in the auth header assert isinstance(request.auth, Token) # do stuff ... # We create our OpenAPIConfig as usual - the JWT security scheme will be injected into it. openapi_config = OpenAPIConfig( title="My API", version="1.0.0", ) # We initialize the app instance and pass the jwt_cookie_auth 'on_app_init' handler to the constructor. # The hook handler will inject the JWT middleware and openapi configuration into the app. app = Litestar( route_handlers=[login_handler, some_route_handler], on_app_init=[jwt_cookie_auth.on_app_init], openapi_config=openapi_config, ) litestar-2.16.0/docs/examples/security/jwt/using_oauth2_password_bearer.py000066400000000000000000000065731500564371300271120ustar00rootroot00000000000000from os import environ from typing import Any, Dict, Optional from uuid import UUID from pydantic import BaseModel, EmailStr from litestar import Litestar, Request, Response, get, post from litestar.connection import ASGIConnection from litestar.openapi.config import OpenAPIConfig from litestar.security.jwt import OAuth2Login, OAuth2PasswordBearerAuth, Token # Let's assume we have a User model that is a pydantic model. # This though is not required - we need some sort of user class - # but it can be any arbitrary value, e.g. an SQLAlchemy model, a representation of a MongoDB etc. class User(BaseModel): id: UUID name: str email: EmailStr MOCK_DB: Dict[str, User] = {} # OAuth2PasswordBearerAuth requires a retrieve handler callable that receives the JWT token model and the ASGI connection # and returns the 'User' instance correlating to it. # # Notes: # - 'User' can be any arbitrary value you decide upon. # - The callable can be either sync or async - both will work. async def retrieve_user_handler(token: "Token", connection: "ASGIConnection[Any, Any, Any, Any]") -> Optional[User]: # logic here to retrieve the user instance return MOCK_DB.get(token.sub) oauth2_auth = OAuth2PasswordBearerAuth[User]( retrieve_user_handler=retrieve_user_handler, token_secret=environ.get("JWT_SECRET", "abcd123"), # we are specifying the URL for retrieving a JWT access token token_url="/login", # we are specifying which endpoints should be excluded from authentication. In this case the login endpoint # and our openAPI docs. exclude=["/login", "/schema"], ) # Given an instance of 'OAuth2PasswordBearerAuth' we can create a login handler function: @post("/login") async def login_handler(request: "Request[Any, Any, Any]", data: "User") -> "Response[OAuth2Login]": MOCK_DB[str(data.id)] = data # if we do not define a response body, the login process will return a standard OAuth2 login response. Note the `Response[OAuth2Login]` return type. # you can do whatever you want to update the response instance here # e.g. response.set_cookie(...) return oauth2_auth.login(identifier=str(data.id)) @post("/login_custom") async def login_custom_response_handler(data: "User") -> "Response[User]": MOCK_DB[str(data.id)] = data # you can do whatever you want to update the response instance here # e.g. response.set_cookie(...) return oauth2_auth.login(identifier=str(data.id), response_body=data) # We also have some other routes, for example: @get("/some-path", sync_to_thread=False) def some_route_handler(request: "Request[User, Token, Any]") -> Any: # request.user is set to the instance of user returned by the middleware assert isinstance(request.user, User) # request.auth is the instance of 'litestar_jwt.Token' created from the data encoded in the auth header assert isinstance(request.auth, Token) # do stuff ... # We create our OpenAPIConfig as usual - the JWT security scheme will be injected into it. openapi_config = OpenAPIConfig( title="My API", version="1.0.0", ) # We initialize the app instance and pass the oauth2_auth 'on_app_init' handler to the constructor. # The hook handler will inject the JWT middleware and openapi configuration into the app. app = Litestar( route_handlers=[login_handler, some_route_handler], on_app_init=[oauth2_auth.on_app_init], openapi_config=openapi_config, ) litestar-2.16.0/docs/examples/security/jwt/using_token_revocation.py000066400000000000000000000072601500564371300260110ustar00rootroot00000000000000from os import environ from typing import Any, Dict, Optional from uuid import UUID from pydantic import BaseModel, EmailStr from litestar import Litestar, Request, Response, get, post from litestar.connection import ASGIConnection from litestar.openapi.config import OpenAPIConfig from litestar.security.jwt import JWTAuth, Token # Let's assume we have a User model that is a pydantic model. # This though is not required - we need some sort of user class - # but it can be any arbitrary value, e.g. an SQLAlchemy model, a representation of a MongoDB etc. class User(BaseModel): id: UUID name: str email: EmailStr MOCK_DB: Dict[str, User] = {} BLOCKLIST: Dict[str, str] = {} # JWTAuth requires a retrieve handler callable that receives the JWT token model and the ASGI connection # and returns the 'User' instance correlating to it. # # Notes: # - 'User' can be any arbitrary value you decide upon. # - The callable can be either sync or async - both will work. async def retrieve_user_handler(token: Token, connection: "ASGIConnection[Any, Any, Any, Any]") -> Optional[User]: # logic here to retrieve the user instance return MOCK_DB.get(token.sub) # If you want to use JWTAuth with revoking tokens, you have to define a handler of revoked tokens # with your custom logic. async def revoked_token_handler(token: Token, connection: "ASGIConnection[Any, Any, Any, Any]") -> bool: jti = token.jti # Unique token identifier (JWT ID) if jti: # Check if the token is already revoked in the BLOCKLIST revoked = BLOCKLIST.get(jti) if revoked: return True return False jwt_auth = JWTAuth[User]( retrieve_user_handler=retrieve_user_handler, revoked_token_handler=revoked_token_handler, token_secret=environ.get("JWT_SECRET", "abcd123"), # we are specifying which endpoints should be excluded from authentication. In this case the login endpoint # and our openAPI docs. exclude=["/login", "/schema"], ) # Given an instance of 'JWTAuth' we can create a login handler function: @post("/login") async def login_handler(data: User) -> Response[User]: MOCK_DB[str(data.id)] = data # you can do whatever you want to update the response instance here # e.g. response.set_cookie(...) return jwt_auth.login(identifier=str(data.id), token_extras={"email": data.email}, response_body=data) # Also we can create a logout @post("/logout") async def logout_handler(request: Request["User", Token, Any]) -> Dict[str, str]: # Your custom logic here # For example jti = request.auth.jti if jti: BLOCKLIST[jti] = "revoked" return {"message": "Token has been revoked."} return {"message": "No valid token found."} # We also have some other routes, for example: @get("/some-path", sync_to_thread=False, middleware=[jwt_auth.middleware]) def some_route_handler(request: "Request[User, Token, Any]") -> Any: # request.user is set to the instance of user returned by the middleware assert isinstance(request.user, User) # request.auth is the instance of 'litestar.security.jwt.Token' created from the data encoded in the auth header assert isinstance(request.auth, Token) # do stuff ... # We create our OpenAPIConfig as usual - the JWT security scheme will be injected into it. openapi_config = OpenAPIConfig( title="My API", version="1.0.0", ) # We initialize the app instance and pass the jwt_auth 'on_app_init' handler to the constructor. # The hook handler will inject the JWT middleware and openapi configuration into the app. app = Litestar( route_handlers=[login_handler, logout_handler, some_route_handler], on_app_init=[jwt_auth.on_app_init], openapi_config=openapi_config, ) litestar-2.16.0/docs/examples/security/jwt/verify_issuer_audience.py000066400000000000000000000014061500564371300257620ustar00rootroot00000000000000import dataclasses import secrets from typing import Any, Dict from litestar import Litestar, Request, get from litestar.connection import ASGIConnection from litestar.security.jwt import JWTAuth, Token @dataclasses.dataclass class User: id: str async def retrieve_user_handler(token: Token, connection: ASGIConnection) -> User: return User(id=token.sub) jwt_auth = JWTAuth[User]( token_secret=secrets.token_hex(), retrieve_user_handler=retrieve_user_handler, accepted_audiences=["https://api.testserver.local"], accepted_issuers=["https://auth.testserver.local"], ) @get("/") def handler(request: Request[User, Token, Any]) -> Dict[str, Any]: return {"id": request.user.id} app = Litestar([handler], middleware=[jwt_auth.middleware]) litestar-2.16.0/docs/examples/security/using_abstract_authentication_middleware.py000066400000000000000000000057051500564371300307350ustar00rootroot00000000000000from dataclasses import dataclass from typing import Any import anyio from litestar import Litestar, MediaType, Request, Response, WebSocket, get, websocket from litestar.connection import ASGIConnection from litestar.datastructures import State from litestar.di import Provide from litestar.exceptions import NotAuthorizedException, NotFoundException from litestar.middleware import AbstractAuthenticationMiddleware, AuthenticationResult from litestar.middleware.base import DefineMiddleware API_KEY_HEADER = "X-API-KEY" TOKEN_USER_DATABASE = {"1": "user_authorized"} @dataclass class MyUser: name: str @dataclass class MyToken: api_key: str class CustomAuthenticationMiddleware(AbstractAuthenticationMiddleware): async def authenticate_request(self, connection: ASGIConnection) -> AuthenticationResult: """Given a request, parse the request api key stored in the header and retrieve the user correlating to the token from the DB""" # retrieve the auth header auth_header = connection.headers.get(API_KEY_HEADER) if not auth_header: raise NotAuthorizedException() # this would be a database call token = MyToken(api_key=auth_header) user = MyUser(name=TOKEN_USER_DATABASE.get(token.api_key)) if not user.name: raise NotAuthorizedException() return AuthenticationResult(user=user, auth=token) @get("/") def my_http_handler(request: Request[MyUser, MyToken, State]) -> None: user = request.user # correctly typed as MyUser auth = request.auth # correctly typed as MyToken assert isinstance(user, MyUser) assert isinstance(auth, MyToken) @websocket("/") async def my_ws_handler(socket: WebSocket[MyUser, MyToken, State]) -> None: user = socket.user # correctly typed as MyUser auth = socket.auth # correctly typed as MyToken assert isinstance(user, MyUser) assert isinstance(auth, MyToken) @get(path="/", exclude_from_auth=True) async def site_index() -> Response: """Site index""" exists = await anyio.Path("index.html").exists() if exists: async with await anyio.open_file(anyio.Path("index.html")) as file: content = await file.read() return Response(content=content, status_code=200, media_type=MediaType.HTML) raise NotFoundException("Site index was not found") async def my_dependency(request: Request[MyUser, MyToken, State]) -> Any: user = request.user # correctly typed as MyUser auth = request.auth # correctly typed as MyToken assert isinstance(user, MyUser) assert isinstance(auth, MyToken) # you can optionally exclude certain paths from authentication. # the following excludes all routes mounted at or under `/schema*` auth_mw = DefineMiddleware(CustomAuthenticationMiddleware, exclude="schema") app = Litestar( route_handlers=[site_index, my_http_handler, my_ws_handler], middleware=[auth_mw], dependencies={"some_dependency": Provide(my_dependency)}, ) litestar-2.16.0/docs/examples/security/using_session_auth.py000066400000000000000000000114471500564371300243420ustar00rootroot00000000000000from typing import Any, Dict, Literal, Optional from uuid import UUID, uuid4 from pydantic import BaseModel, EmailStr, SecretStr from litestar import Litestar, Request, get, post from litestar.connection import ASGIConnection from litestar.exceptions import NotAuthorizedException from litestar.middleware.session.server_side import ServerSideSessionBackend, ServerSideSessionConfig from litestar.openapi.config import OpenAPIConfig from litestar.security.session_auth import SessionAuth from litestar.stores.memory import MemoryStore # Let's assume we have a User model that is a pydantic model. # This though is not required - we need some sort of user class - # but it can be any arbitrary value, e.g. an SQLAlchemy model, # a representation of a MongoDB etc. class User(BaseModel): id: UUID name: str email: EmailStr # we also have pydantic types for two different # kinds of POST request bodies: one for creating # a user, e.g. "sign-up", and the other for logging # an existing user in. class UserCreatePayload(BaseModel): name: str email: EmailStr password: SecretStr class UserLoginPayload(BaseModel): email: EmailStr password: SecretStr MOCK_DB: Dict[str, User] = {} memory_store = MemoryStore() # The SessionAuth class requires a handler callable # that takes the session dictionary, and returns the # 'User' instance correlating to it. # # The session dictionary itself is a value the user decides # upon. So for example, it might be a simple dictionary # that holds a user id, for example: { "id": "abcd123" } # # Note: The callable can be either sync or async - both will work. async def retrieve_user_handler( session: Dict[str, Any], connection: "ASGIConnection[Any, Any, Any, Any]" ) -> Optional[User]: return MOCK_DB.get(user_id) if (user_id := session.get("user_id")) else None @post("/login") async def login(data: UserLoginPayload, request: "Request[Any, Any, Any]") -> User: # we received log-in data via post. # our login handler should retrieve from persistence (a db etc.) # the user data and verify that the login details # are correct. If we are using passwords, we should check that # the password hashes match etc. We will simply assume that we # have done all of that we now have a user value: user_id = await memory_store.get(data.email) if not user_id: raise NotAuthorizedException user_id = user_id.decode("utf-8") # once verified we can create a session. # to do this we simply need to call the Litestar # 'Request.set_session' method, which accepts either dictionaries # or pydantic models. In our case, we can simply record a # simple dictionary with the user ID value: request.set_session({"user_id": user_id}) # you can do whatever we want here. In this case, we will simply return the user data: return MOCK_DB[user_id] @post("/signup") async def signup(data: UserCreatePayload, request: Request[Any, Any, Any]) -> User: # this is similar to the login handler, except here we should # insert into persistence - after doing whatever extra # validation we might require. We will assume that this is done, # and we now have a user instance with an assigned ID value: user = User(name=data.name, email=data.email, id=uuid4()) await memory_store.set(data.email, str(user.id)) MOCK_DB[str(user.id)] = user # we are creating a session the same as we do in the # 'login_handler' above: request.set_session({"user_id": str(user.id)}) # and again, you can add whatever logic is required here, we # will simply return the user: return user # the endpoint below requires the user to be already authenticated # to be able to access it. @get("/user", sync_to_thread=False) def get_user(request: Request[User, Dict[Literal["user_id"], str], Any]) -> Any: # because this route requires authentication, we can access # `request.user`, which is the authenticated user returned # by the 'retrieve_user_handler' function we passed to SessionAuth. return request.user # We add the session security schema to the OpenAPI config. openapi_config = OpenAPIConfig( title="My API", version="1.0.0", ) session_auth = SessionAuth[User, ServerSideSessionBackend]( retrieve_user_handler=retrieve_user_handler, # we must pass a config for a session backend. # all session backends are supported session_backend_config=ServerSideSessionConfig(), # exclude any URLs that should not have authentication. # We exclude the documentation URLs, signup and login. exclude=["/login", "/signup", "/schema"], ) # We initialize the app instance, passing to it the 'session_auth.on_app_init' and the 'openapi_config'. app = Litestar( route_handlers=[login, signup, get_user], on_app_init=[session_auth.on_app_init], openapi_config=openapi_config, ) litestar-2.16.0/docs/examples/signature_namespace/000077500000000000000000000000001500564371300222165ustar00rootroot00000000000000litestar-2.16.0/docs/examples/signature_namespace/__init__.py000066400000000000000000000000001500564371300243150ustar00rootroot00000000000000litestar-2.16.0/docs/examples/signature_namespace/app.py000066400000000000000000000003121500564371300233440ustar00rootroot00000000000000from __future__ import annotations from litestar import Litestar from .controller import MyController from .domain import Model app = Litestar(route_handlers=[MyController], signature_types=[Model]) litestar-2.16.0/docs/examples/signature_namespace/controller.py000066400000000000000000000004451500564371300247560ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar import Controller, post if TYPE_CHECKING: from .domain import Model class MyController(Controller): @post(sync_to_thread=False) def post_handler(self, data: Model) -> Model: return data litestar-2.16.0/docs/examples/signature_namespace/domain.py000066400000000000000000000001661500564371300240420ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass @dataclass class Model: a: int b: str litestar-2.16.0/docs/examples/startup_and_shutdown.py000066400000000000000000000015701500564371300230350ustar00rootroot00000000000000import os from typing import cast from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine from litestar import Litestar DB_URI = os.environ.get("DATABASE_URI", "postgresql+asyncpg://postgres:mysecretpassword@pg.db:5432/db") def get_db_connection(app: Litestar) -> AsyncEngine: """Returns the db engine. If it doesn't exist, creates it and saves it in on the application state object """ if not getattr(app.state, "engine", None): app.state.engine = create_async_engine(DB_URI) return cast("AsyncEngine", app.state.engine) async def close_db_connection(app: Litestar) -> None: """Closes the db connection stored in the application State object.""" if getattr(app.state, "engine", None): await cast("AsyncEngine", app.state.engine).dispose() app = Litestar(on_startup=[get_db_connection], on_shutdown=[close_db_connection]) litestar-2.16.0/docs/examples/static_files/000077500000000000000000000000001500564371300206525ustar00rootroot00000000000000litestar-2.16.0/docs/examples/static_files/__init__.py000066400000000000000000000000001500564371300227510ustar00rootroot00000000000000litestar-2.16.0/docs/examples/static_files/custom_router.py000066400000000000000000000005411500564371300241360ustar00rootroot00000000000000from litestar import Litestar from litestar.router import Router from litestar.static_files import create_static_files_router class MyRouter(Router): pass app = Litestar( route_handlers=[ create_static_files_router( path="/static", directories=["assets"], router_class=MyRouter, ) ] ) litestar-2.16.0/docs/examples/static_files/file_system.py000066400000000000000000000005471500564371300235550ustar00rootroot00000000000000from fsspec.implementations.ftp import FTPFileSystem from litestar import Litestar from litestar.static_files import create_static_files_router app = Litestar( route_handlers=[ create_static_files_router( path="/static", directories=["assets"], file_system=FTPFileSystem(host="127.0.0.1"), ), ] ) litestar-2.16.0/docs/examples/static_files/full_example.py000066400000000000000000000007001500564371300236760ustar00rootroot00000000000000from pathlib import Path from litestar import Litestar from litestar.static_files import create_static_files_router ASSETS_DIR = Path("assets") def on_startup(): ASSETS_DIR.mkdir(exist_ok=True) ASSETS_DIR.joinpath("hello.txt").write_text("Hello, world!") app = Litestar( route_handlers=[ create_static_files_router(path="/static", directories=["assets"]), ], on_startup=[on_startup], ) # run: /static/hello.txt litestar-2.16.0/docs/examples/static_files/html_mode.py000066400000000000000000000011371500564371300231760ustar00rootroot00000000000000from pathlib import Path from litestar import Litestar from litestar.static_files import create_static_files_router HTML_DIR = Path("html") def on_startup() -> None: HTML_DIR.mkdir(exist_ok=True) HTML_DIR.joinpath("index.html").write_text("Hello, world!") HTML_DIR.joinpath("404.html").write_text("

Not found

") app = Litestar( route_handlers=[ create_static_files_router( path="/", directories=["html"], html_mode=True, ) ], on_startup=[on_startup], ) # run: / # run: /index.html # run: /something litestar-2.16.0/docs/examples/static_files/passing_options.py000066400000000000000000000005231500564371300244430ustar00rootroot00000000000000from litestar import Litestar from litestar.static_files import create_static_files_router app = Litestar( route_handlers=[ create_static_files_router( path="/", directories=["assets"], opt={"some": True}, include_in_schema=False, tags=["static"], ) ] ) litestar-2.16.0/docs/examples/static_files/route_reverse.py000066400000000000000000000004641500564371300241210ustar00rootroot00000000000000from litestar import Litestar from litestar.static_files import create_static_files_router app = Litestar( route_handlers=[ create_static_files_router(path="/static", directories=["assets"]), ] ) print(app.route_reverse(name="static", file_path="/some_file.txt")) # /static/some_file.txt litestar-2.16.0/docs/examples/static_files/send_as_attachment.py000066400000000000000000000004341500564371300250510ustar00rootroot00000000000000from litestar import Litestar from litestar.static_files import create_static_files_router app = Litestar( route_handlers=[ create_static_files_router( path="/static", directories=["assets"], send_as_attachment=True, ) ] ) litestar-2.16.0/docs/examples/static_files/upgrade_from_static_1.py000066400000000000000000000003201500564371300254600ustar00rootroot00000000000000from litestar import Litestar from litestar.static_files.config import StaticFilesConfig app = Litestar( static_files_config=[ StaticFilesConfig(directories=["assets"], path="/static"), ], ) litestar-2.16.0/docs/examples/static_files/upgrade_from_static_2.py000066400000000000000000000003261500564371300254670ustar00rootroot00000000000000from litestar import Litestar from litestar.static_files import create_static_files_router app = Litestar( route_handlers=[ create_static_files_router(directories=["assets"], path="/static"), ], ) litestar-2.16.0/docs/examples/stores/000077500000000000000000000000001500564371300175205ustar00rootroot00000000000000litestar-2.16.0/docs/examples/stores/__init__.py000066400000000000000000000000001500564371300216170ustar00rootroot00000000000000litestar-2.16.0/docs/examples/stores/configure_integrations_set_names.py000066400000000000000000000012301500564371300266730ustar00rootroot00000000000000from pathlib import Path from litestar import Litestar from litestar.config.response_cache import ResponseCacheConfig from litestar.middleware.rate_limit import RateLimitConfig from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.stores.file import FileStore from litestar.stores.redis import RedisStore app = Litestar( stores={"redis": RedisStore.with_client(), "file": FileStore(Path("data"))}, response_cache_config=ResponseCacheConfig(store="redis"), middleware=[ ServerSideSessionConfig(store="file").middleware, RateLimitConfig(rate_limit=("second", 10), store="redis").middleware, ], ) litestar-2.16.0/docs/examples/stores/delete_expired_after_response.py000066400000000000000000000007731500564371300261620ustar00rootroot00000000000000from datetime import datetime, timedelta from litestar import Litestar, Request from litestar.stores.memory import MemoryStore memory_store = MemoryStore() async def after_response(request: Request) -> None: now = datetime.utcnow() last_cleared = request.app.state.get("store_last_cleared", now) if datetime.utcnow() - last_cleared > timedelta(seconds=30): await memory_store.delete_expired() app.state["store_last_cleared"] = now app = Litestar(after_response=after_response) litestar-2.16.0/docs/examples/stores/delete_expired_on_startup.py000066400000000000000000000003731500564371300253350ustar00rootroot00000000000000from pathlib import Path from litestar import Litestar from litestar.stores.file import FileStore file_store = FileStore(Path("data")) async def on_startup() -> None: await file_store.delete_expired() app = Litestar(on_startup=[on_startup]) litestar-2.16.0/docs/examples/stores/expiry.py000066400000000000000000000005401500564371300214110ustar00rootroot00000000000000from asyncio import sleep from litestar.stores.memory import MemoryStore store = MemoryStore() async def main() -> None: await store.set("foo", b"bar", expires_in=1) value = await store.get("foo") print(value) await sleep(1) value = await store.get("foo") # this will return 'None', since the key has expired print(value) litestar-2.16.0/docs/examples/stores/expiry_renew_on_get.py000066400000000000000000000010421500564371300241420ustar00rootroot00000000000000from asyncio import sleep from litestar.stores.memory import MemoryStore store = MemoryStore() async def main() -> None: await store.set("foo", b"bar", expires_in=1) await sleep(0.5) await store.get("foo", renew_for=1) # this will reset the time to live to one second await sleep(1) # it has now been 1.5 seconds since the key was set with a life time of one second, # so it should have expired however, since it was renewed for one second, it is still available value = await store.get("foo") print(value) litestar-2.16.0/docs/examples/stores/get_set.py000066400000000000000000000004721500564371300215270ustar00rootroot00000000000000from litestar.stores.memory import MemoryStore store = MemoryStore() async def main() -> None: value = await store.get("key") print(value) # this will print 'None', as no store with this key has been defined yet await store.set("key", b"value") value = await store.get("key") print(value) litestar-2.16.0/docs/examples/stores/namespacing.py000066400000000000000000000005271500564371300223630ustar00rootroot00000000000000from litestar import Litestar from litestar.stores.redis import RedisStore root_store = RedisStore.with_client() cache_store = root_store.with_namespace("cache") session_store = root_store.with_namespace("sessions") async def before_shutdown() -> None: await cache_store.delete_all() app = Litestar(before_shutdown=[before_shutdown]) litestar-2.16.0/docs/examples/stores/registry.py000066400000000000000000000006451500564371300217470ustar00rootroot00000000000000from litestar import Litestar from litestar.stores.memory import MemoryStore app = Litestar([], stores={"memory": MemoryStore()}) memory_store = app.stores.get("memory") # this is the previously defined store some_other_store = app.stores.get("something_else") # this will be a newly created instance assert app.stores.get("something_else") is some_other_store # but subsequent requests will return the same instance litestar-2.16.0/docs/examples/stores/registry_access_integration.py000066400000000000000000000003211500564371300256620ustar00rootroot00000000000000from litestar import Litestar from litestar.middleware.rate_limit import RateLimitConfig app = Litestar(middleware=[RateLimitConfig(("second", 1)).middleware]) rate_limit_store = app.stores.get("rate_limit") litestar-2.16.0/docs/examples/stores/registry_configure_integrations.py000066400000000000000000000006451500564371300265760ustar00rootroot00000000000000from pathlib import Path from litestar import Litestar from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.stores.file import FileStore from litestar.stores.redis import RedisStore app = Litestar( stores={ "sessions": RedisStore.with_client(), "response_cache": FileStore(Path("response-cache")), }, middleware=[ServerSideSessionConfig().middleware], ) litestar-2.16.0/docs/examples/stores/registry_default_factory.py000066400000000000000000000004631500564371300252000ustar00rootroot00000000000000from litestar import Litestar from litestar.stores.memory import MemoryStore from litestar.stores.registry import StoreRegistry memory_store = MemoryStore() def default_factory(name: str) -> MemoryStore: return memory_store app = Litestar([], stores=StoreRegistry(default_factory=default_factory)) litestar-2.16.0/docs/examples/stores/registry_default_factory_namespacing.py000066400000000000000000000012571500564371300275470ustar00rootroot00000000000000from litestar import Litestar, get from litestar.middleware.rate_limit import RateLimitConfig from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.stores.redis import RedisStore from litestar.stores.registry import StoreRegistry root_store = RedisStore.with_client() @get(cache=True, sync_to_thread=False) def cached_handler() -> str: # this will use app.stores.get("response_cache") return "Hello, world!" app = Litestar( [cached_handler], stores=StoreRegistry(default_factory=root_store.with_namespace), middleware=[ RateLimitConfig(("second", 1)).middleware, ServerSideSessionConfig().middleware, ], ) litestar-2.16.0/docs/examples/templating/000077500000000000000000000000001500564371300203455ustar00rootroot00000000000000litestar-2.16.0/docs/examples/templating/__init__.py000066400000000000000000000000001500564371300224440ustar00rootroot00000000000000litestar-2.16.0/docs/examples/templating/engine_instance_jinja.py000066400000000000000000000003001500564371300252140ustar00rootroot00000000000000from litestar.contrib.jinja import JinjaTemplateEngine from litestar.template.config import TemplateConfig template_config = TemplateConfig(engine=JinjaTemplateEngine, directory="templates") litestar-2.16.0/docs/examples/templating/engine_instance_mako.py000066400000000000000000000002751500564371300250630ustar00rootroot00000000000000from litestar.contrib.mako import MakoTemplateEngine from litestar.template.config import TemplateConfig template_config = TemplateConfig(engine=MakoTemplateEngine, directory="templates") litestar-2.16.0/docs/examples/templating/engine_instance_minijinja.py000066400000000000000000000003141500564371300260760ustar00rootroot00000000000000from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.template.config import TemplateConfig template_config = TemplateConfig(engine=MiniJinjaTemplateEngine, directory="templates") litestar-2.16.0/docs/examples/templating/returning_templates_jinja.py000066400000000000000000000013701500564371300261660ustar00rootroot00000000000000from pathlib import Path from litestar import Litestar, get from litestar.contrib.jinja import JinjaTemplateEngine from litestar.response import Template from litestar.template.config import TemplateConfig @get(path="/{template_type: str}", sync_to_thread=False) def index(name: str, template_type: str) -> Template: if template_type == "file": return Template(template_name="hello.html.jinja2", context={"name": name}) elif template_type == "string": return Template(template_str="Hello Jinja using strings", context={"name": name}) app = Litestar( route_handlers=[index], template_config=TemplateConfig( directory=Path(__file__).parent / "templates", engine=JinjaTemplateEngine, ), ) litestar-2.16.0/docs/examples/templating/returning_templates_mako.py000066400000000000000000000014261500564371300260240ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from litestar import Litestar, get from litestar.contrib.mako import MakoTemplateEngine from litestar.response import Template from litestar.template.config import TemplateConfig @get(path="/{template_type: str}", sync_to_thread=False) def index(name: str, template_type: str) -> Template: if template_type == "file": return Template(template_name="hello.html.mako", context={"name": name}) elif template_type == "string": return Template(template_str="Hello Mako using strings", context={"name": name}) app = Litestar( route_handlers=[index], template_config=TemplateConfig( directory=Path(__file__).parent / "templates", engine=MakoTemplateEngine, ), ) litestar-2.16.0/docs/examples/templating/returning_templates_minijinja.py000066400000000000000000000014571500564371300270510ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from litestar import Litestar, get from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.response import Template from litestar.template.config import TemplateConfig @get(path="/{template_type: str}", sync_to_thread=False) def index(name: str, template_type: str) -> Template: if template_type == "file": return Template(template_name="hello.html.minijinja", context={"name": name}) elif template_type == "string": return Template(template_str="Hello Minijinja using strings", context={"name": name}) app = Litestar( route_handlers=[index], template_config=TemplateConfig( directory=Path(__file__).parent / "templates", engine=MiniJinjaTemplateEngine, ), ) litestar-2.16.0/docs/examples/templating/template_engine_jinja.py000066400000000000000000000005011500564371300252260ustar00rootroot00000000000000from pathlib import Path from litestar import Litestar from litestar.contrib.jinja import JinjaTemplateEngine from litestar.template.config import TemplateConfig app = Litestar( route_handlers=[], template_config=TemplateConfig( directory=Path("templates"), engine=JinjaTemplateEngine, ), ) litestar-2.16.0/docs/examples/templating/template_engine_mako.py000066400000000000000000000004761500564371300250750ustar00rootroot00000000000000from pathlib import Path from litestar import Litestar from litestar.contrib.mako import MakoTemplateEngine from litestar.template.config import TemplateConfig app = Litestar( route_handlers=[], template_config=TemplateConfig( directory=Path("templates"), engine=MakoTemplateEngine, ), ) litestar-2.16.0/docs/examples/templating/template_engine_minijinja.py000066400000000000000000000005151500564371300261100ustar00rootroot00000000000000from pathlib import Path from litestar import Litestar from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.template.config import TemplateConfig app = Litestar( route_handlers=[], template_config=TemplateConfig( directory=Path("templates"), engine=MiniJinjaTemplateEngine, ), ) litestar-2.16.0/docs/examples/templating/template_functions_jinja.py000066400000000000000000000016101500564371300257730ustar00rootroot00000000000000from pathlib import Path from typing import Any, Dict from litestar import Litestar, get from litestar.contrib.jinja import JinjaTemplateEngine from litestar.response import Template from litestar.template.config import TemplateConfig def my_template_function(ctx: Dict[str, Any]) -> str: return ctx.get("my_context_key", "nope") def register_template_callables(engine: JinjaTemplateEngine) -> None: engine.register_template_callable( key="check_context_key", template_callable=my_template_function, ) template_config = TemplateConfig( directory=Path(__file__).parent / "templates", engine=JinjaTemplateEngine, engine_callback=register_template_callables, ) @get("/", sync_to_thread=False) def index() -> Template: return Template(template_name="index.html.jinja2") app = Litestar( route_handlers=[index], template_config=template_config, ) litestar-2.16.0/docs/examples/templating/template_functions_mako.py000066400000000000000000000016021500564371300256300ustar00rootroot00000000000000from pathlib import Path from typing import Any, Dict from litestar import Litestar, get from litestar.contrib.mako import MakoTemplateEngine from litestar.response import Template from litestar.template.config import TemplateConfig def my_template_function(ctx: Dict[str, Any]) -> str: return ctx.get("my_context_key", "nope") def register_template_callables(engine: MakoTemplateEngine) -> None: engine.register_template_callable( key="check_context_key", template_callable=my_template_function, ) template_config = TemplateConfig( directory=Path(__file__).parent / "templates", engine=MakoTemplateEngine, engine_callback=register_template_callables, ) @get("/", sync_to_thread=False) def index() -> Template: return Template(template_name="index.html.mako") app = Litestar( route_handlers=[index], template_config=template_config, ) litestar-2.16.0/docs/examples/templating/template_functions_minijinja.py000066400000000000000000000015571500564371300266620ustar00rootroot00000000000000from pathlib import Path from litestar import Litestar, get from litestar.contrib.minijinja import MiniJinjaTemplateEngine, StateProtocol from litestar.response import Template from litestar.template.config import TemplateConfig def my_template_function(ctx: StateProtocol) -> str: return ctx.lookup("my_context_key") or "nope" def register_template_callables(engine: MiniJinjaTemplateEngine) -> None: engine.register_template_callable(key="check_context_key", template_callable=my_template_function) template_config = TemplateConfig( directory=Path(__file__).parent / "templates", engine=MiniJinjaTemplateEngine, engine_callback=register_template_callables, ) @get("/", sync_to_thread=False) def index() -> Template: return Template(template_name="index.html.minijinja") app = Litestar(route_handlers=[index], template_config=template_config) litestar-2.16.0/docs/examples/templating/templates/000077500000000000000000000000001500564371300223435ustar00rootroot00000000000000litestar-2.16.0/docs/examples/templating/templates/hello.html.jinja2000066400000000000000000000000421500564371300255040ustar00rootroot00000000000000Hello {{ name }} litestar-2.16.0/docs/examples/templating/templates/hello.html.mako000066400000000000000000000000411500564371300252550ustar00rootroot00000000000000Hello ${ name } litestar-2.16.0/docs/examples/templating/templates/hello.html.minijinja000066400000000000000000000000421500564371300262770ustar00rootroot00000000000000Hello {{ name }} litestar-2.16.0/docs/examples/templating/templates/index.html.jinja2000066400000000000000000000000761500564371300255170ustar00rootroot00000000000000check_context_key: {{ check_context_key() }} litestar-2.16.0/docs/examples/templating/templates/index.html.mako000066400000000000000000000000751500564371300252700ustar00rootroot00000000000000check_context_key: ${ check_context_key() } litestar-2.16.0/docs/examples/templating/templates/index.html.minijinja000066400000000000000000000000761500564371300263120ustar00rootroot00000000000000check_context_key: {{ check_context_key() }} litestar-2.16.0/docs/examples/testing/000077500000000000000000000000001500564371300176565ustar00rootroot00000000000000litestar-2.16.0/docs/examples/testing/__init__.py000066400000000000000000000000001500564371300217550ustar00rootroot00000000000000litestar-2.16.0/docs/examples/testing/subprocess_sse_app.py000066400000000000000000000010541500564371300241320ustar00rootroot00000000000000""" Assemble components into an app that shall be tested """ from typing import AsyncGenerator from litestar import Litestar, get from litestar.response import ServerSentEvent from litestar.types import SSEData async def generator(topic: str) -> AsyncGenerator[SSEData, None]: count = 0 while count < 2: yield topic count += 1 @get("/notify/{topic:str}") async def get_notified(topic: str) -> ServerSentEvent: return ServerSentEvent(generator(topic), event_type="Notifier") app = Litestar(route_handlers=[get_notified]) litestar-2.16.0/docs/examples/testing/test_get_session_data.py000066400000000000000000000011171500564371300246020ustar00rootroot00000000000000from litestar import Litestar, Request, post from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.testing import TestClient session_config = ServerSideSessionConfig() @post(path="/test", sync_to_thread=False) def set_session_data(request: Request) -> None: request.session["foo"] = "bar" app = Litestar(route_handlers=[set_session_data], middleware=[session_config.middleware], debug=True) with TestClient(app=app, session_config=session_config) as client: client.post("/test").json() assert client.get_session_data() == {"foo": "bar"} litestar-2.16.0/docs/examples/testing/test_get_session_data_async.py000066400000000000000000000012341500564371300257770ustar00rootroot00000000000000from litestar import Litestar, Request, post from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.testing import AsyncTestClient session_config = ServerSideSessionConfig() @post(path="/test", sync_to_thread=False) def set_session_data(request: Request) -> None: request.session["foo"] = "bar" app = Litestar(route_handlers=[set_session_data], middleware=[session_config.middleware], debug=True) async def test_set_session_data() -> None: async with AsyncTestClient(app=app, session_config=session_config) as client: await client.post("/test") assert await client.get_session_data() == {"foo": "bar"} litestar-2.16.0/docs/examples/testing/test_health_check_async.py000066400000000000000000000014141500564371300250660ustar00rootroot00000000000000from typing import AsyncIterator import pytest from litestar import Litestar, MediaType, get from litestar.status_codes import HTTP_200_OK from litestar.testing import AsyncTestClient @get(path="/health-check", media_type=MediaType.TEXT, sync_to_thread=False) def health_check() -> str: return "healthy" app = Litestar(route_handlers=[health_check], debug=True) @pytest.fixture(scope="function") async def test_client() -> AsyncIterator[AsyncTestClient[Litestar]]: async with AsyncTestClient(app=app) as client: yield client async def test_health_check_with_fixture(test_client: AsyncTestClient[Litestar]) -> None: response = await test_client.get("/health-check") assert response.status_code == HTTP_200_OK assert response.text == "healthy" litestar-2.16.0/docs/examples/testing/test_health_check_sync.py000066400000000000000000000013261500564371300247270ustar00rootroot00000000000000from typing import Iterator import pytest from litestar import Litestar, MediaType, get from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient @get(path="/health-check", media_type=MediaType.TEXT, sync_to_thread=False) def health_check() -> str: return "healthy" app = Litestar(route_handlers=[health_check], debug=True) @pytest.fixture(scope="function") def test_client() -> Iterator[TestClient[Litestar]]: with TestClient(app=app) as client: yield client def test_health_check_with_fixture(test_client: TestClient[Litestar]) -> None: response = test_client.get("/health-check") assert response.status_code == HTTP_200_OK assert response.text == "healthy" litestar-2.16.0/docs/examples/testing/test_set_session_data.py000066400000000000000000000012541500564371300246200ustar00rootroot00000000000000from typing import Any, Dict from litestar import Litestar, Request, get from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.testing import TestClient session_config = ServerSideSessionConfig() @get(path="/test", sync_to_thread=False) def get_session_data(request: Request) -> Dict[str, Any]: return request.session app = Litestar(route_handlers=[get_session_data], middleware=[session_config.middleware], debug=True) def test_get_session_data() -> None: with TestClient(app=app, session_config=session_config) as client: client.set_session_data({"foo": "bar"}) assert client.get("/test").json() == {"foo": "bar"} litestar-2.16.0/docs/examples/testing/test_set_session_data_async.py000066400000000000000000000013401500564371300260110ustar00rootroot00000000000000from typing import Any, Dict from litestar import Litestar, Request, get from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.testing import AsyncTestClient session_config = ServerSideSessionConfig() @get(path="/test", sync_to_thread=False) def get_session_data(request: Request) -> Dict[str, Any]: return request.session app = Litestar(route_handlers=[get_session_data], middleware=[session_config.middleware], debug=True) async def test_get_session_data() -> None: async with AsyncTestClient(app=app, session_config=session_config) as client: await client.set_session_data({"foo": "bar"}) res = await client.get("/test") assert res.json() == {"foo": "bar"} litestar-2.16.0/docs/examples/testing/test_subprocess_sse.py000066400000000000000000000020231500564371300243260ustar00rootroot00000000000000""" Test the app running in a subprocess """ import asyncio import pathlib import sys from typing import AsyncIterator import httpx import httpx_sse import pytest from litestar.testing import subprocess_async_client if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) ROOT = pathlib.Path(__file__).parent @pytest.fixture(name="async_client") async def fx_async_client() -> AsyncIterator[httpx.AsyncClient]: async with subprocess_async_client(workdir=ROOT, app="subprocess_sse_app:app") as client: yield client async def test_subprocess_async_client(async_client: httpx.AsyncClient) -> None: """Demonstrates functionality of the async client with an infinite SSE source that cannot be tested with the regular async test client. """ topic = "demo" async with httpx_sse.aconnect_sse(async_client, "GET", f"/notify/{topic}") as event_source: async for event in event_source.aiter_sse(): assert event.data == topic break litestar-2.16.0/docs/examples/testing/test_websocket.py000066400000000000000000000012361500564371300232570ustar00rootroot00000000000000from typing import Any from litestar import WebSocket, websocket from litestar.datastructures import State from litestar.testing import create_test_client def test_websocket() -> None: @websocket(path="/ws") async def websocket_handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() recv = await socket.receive_json() await socket.send_json({"message": recv}) await socket.close() with create_test_client(route_handlers=[websocket_handler]).websocket_connect("/ws") as ws: ws.send_json({"hello": "world"}) data = ws.receive_json() assert data == {"message": {"hello": "world"}} litestar-2.16.0/docs/examples/testing/test_with_portal.py000066400000000000000000000015471500564371300236320ustar00rootroot00000000000000from concurrent.futures import Future, wait import anyio from litestar.testing import create_test_client def test_with_portal() -> None: """This example shows how to manage asynchronous tasks using a portal. The test function itself is not async. Asynchronous functions are executed and awaited using the portal. """ async def get_float(value: float) -> float: await anyio.sleep(value) return value with create_test_client(route_handlers=[]) as test_client, test_client.portal() as portal: # start a background task with the portal future: Future[float] = portal.start_task_soon(get_float, 0.25) # do other work assert portal.call(get_float, 0.1) == 0.1 # wait for the background task to complete wait([future]) assert future.done() assert future.result() == 0.25 litestar-2.16.0/docs/examples/todo_app/000077500000000000000000000000001500564371300200065ustar00rootroot00000000000000litestar-2.16.0/docs/examples/todo_app/__init__.py000066400000000000000000000000001500564371300221050ustar00rootroot00000000000000litestar-2.16.0/docs/examples/todo_app/create/000077500000000000000000000000001500564371300212515ustar00rootroot00000000000000litestar-2.16.0/docs/examples/todo_app/create/__init__.py000066400000000000000000000000001500564371300233500ustar00rootroot00000000000000litestar-2.16.0/docs/examples/todo_app/create/dataclass.py000066400000000000000000000005131500564371300235610ustar00rootroot00000000000000from dataclasses import dataclass from typing import List from litestar import Litestar, post @dataclass class TodoItem: title: str done: bool TODO_LIST: List[TodoItem] = [] @post("/") async def add_item(data: TodoItem) -> List[TodoItem]: TODO_LIST.append(data) return TODO_LIST app = Litestar([add_item]) litestar-2.16.0/docs/examples/todo_app/create/dict.py000066400000000000000000000004531500564371300225500ustar00rootroot00000000000000from typing import Any, Dict, List, Union from litestar import Litestar, post TODO_LIST: List[Dict[str, Union[str, bool]]] = [] @post("/") async def add_item(data: Dict[str, Any]) -> List[Dict[str, Union[str, bool]]]: TODO_LIST.append(data) return TODO_LIST app = Litestar([add_item]) litestar-2.16.0/docs/examples/todo_app/full_app.py000066400000000000000000000022421500564371300221620ustar00rootroot00000000000000from dataclasses import dataclass from typing import List, Optional from litestar import Litestar, get, post, put from litestar.exceptions import NotFoundException @dataclass class TodoItem: title: str done: bool TODO_LIST: List[TodoItem] = [ TodoItem(title="Start writing TODO list", done=True), TodoItem(title="???", done=False), TodoItem(title="Profit", done=False), ] def get_todo_by_title(todo_name) -> TodoItem: for item in TODO_LIST: if item.title == todo_name: return item raise NotFoundException(detail=f"TODO {todo_name!r} not found") @get("/") async def get_list(done: Optional[bool] = None) -> List[TodoItem]: if done is None: return TODO_LIST return [item for item in TODO_LIST if item.done == done] @post("/") async def add_item(data: TodoItem) -> List[TodoItem]: TODO_LIST.append(data) return TODO_LIST @put("/{item_title:str}") async def update_item(item_title: str, data: TodoItem) -> List[TodoItem]: todo_item = get_todo_by_title(item_title) todo_item.title = data.title todo_item.done = data.done return TODO_LIST app = Litestar([get_list, add_item, update_item]) litestar-2.16.0/docs/examples/todo_app/get_list/000077500000000000000000000000001500564371300216205ustar00rootroot00000000000000litestar-2.16.0/docs/examples/todo_app/get_list/__init__.py000066400000000000000000000000001500564371300237170ustar00rootroot00000000000000litestar-2.16.0/docs/examples/todo_app/get_list/dataclass.py000066400000000000000000000006541500564371300241360ustar00rootroot00000000000000from dataclasses import dataclass from typing import List from litestar import Litestar, get @dataclass class TodoItem: title: str done: bool TODO_LIST: List[TodoItem] = [ TodoItem(title="Start writing TODO list", done=True), TodoItem(title="???", done=False), TodoItem(title="Profit", done=False), ] @get("/") async def get_list() -> List[TodoItem]: return TODO_LIST app = Litestar([get_list]) litestar-2.16.0/docs/examples/todo_app/get_list/dict.py000066400000000000000000000005731500564371300231220ustar00rootroot00000000000000from typing import Dict, List, Union from litestar import Litestar, get TODO_LIST: List[Dict[str, Union[str, bool]]] = [ {"title": "Start writing TODO list", "done": True}, {"title": "???", "done": False}, {"title": "Profit", "done": False}, ] @get("/") async def get_list() -> List[Dict[str, Union[str, bool]]]: return TODO_LIST app = Litestar([get_list]) litestar-2.16.0/docs/examples/todo_app/get_list/query_param.py000066400000000000000000000010461500564371300245200ustar00rootroot00000000000000from dataclasses import dataclass from typing import List from litestar import Litestar, get @dataclass class TodoItem: title: str done: bool TODO_LIST: List[TodoItem] = [ TodoItem(title="Start writing TODO list", done=True), TodoItem(title="???", done=False), TodoItem(title="Profit", done=False), ] @get("/") async def get_list(done: str) -> List[TodoItem]: if done == "1": return [item for item in TODO_LIST if item.done] return [item for item in TODO_LIST if not item.done] app = Litestar([get_list]) litestar-2.16.0/docs/examples/todo_app/get_list/query_param_default.py000066400000000000000000000010471500564371300262250ustar00rootroot00000000000000from dataclasses import dataclass from typing import List, Optional from litestar import Litestar, get @dataclass class TodoItem: title: str done: bool TODO_LIST: List[TodoItem] = [ TodoItem(title="Start writing TODO list", done=True), TodoItem(title="???", done=False), TodoItem(title="Profit", done=False), ] @get("/") async def get_list(done: Optional[bool] = None) -> List[TodoItem]: if done is None: return TODO_LIST return [item for item in TODO_LIST if item.done == done] app = Litestar([get_list]) litestar-2.16.0/docs/examples/todo_app/get_list/query_param_validate.py000066400000000000000000000007361500564371300263760ustar00rootroot00000000000000from dataclasses import dataclass from typing import List from litestar import Litestar, get @dataclass class TodoItem: title: str done: bool TODO_LIST: List[TodoItem] = [ TodoItem(title="Start writing TODO list", done=True), TodoItem(title="???", done=False), TodoItem(title="Profit", done=False), ] @get("/") async def get_list(done: bool) -> List[TodoItem]: return [item for item in TODO_LIST if item.done == done] app = Litestar([get_list]) litestar-2.16.0/docs/examples/todo_app/get_list/query_param_validate_manually.py000066400000000000000000000013011500564371300302650ustar00rootroot00000000000000from dataclasses import dataclass from typing import List from litestar import Litestar, get from litestar.exceptions import HTTPException @dataclass class TodoItem: title: str done: bool TODO_LIST: List[TodoItem] = [ TodoItem(title="Start writing TODO list", done=True), TodoItem(title="???", done=False), TodoItem(title="Profit", done=False), ] @get("/") async def get_list(done: str) -> List[TodoItem]: if done == "1": return [item for item in TODO_LIST if item.done] if done == "0": return [item for item in TODO_LIST if not item.done] raise HTTPException(f"Invalid query parameter value: {done!r}", status_code=400) app = Litestar([get_list]) litestar-2.16.0/docs/examples/todo_app/hello_world.py000066400000000000000000000002121500564371300226650ustar00rootroot00000000000000from litestar import Litestar, get @get("/") async def hello_world() -> str: return "Hello, world!" app = Litestar([hello_world]) litestar-2.16.0/docs/examples/todo_app/update.py000066400000000000000000000015141500564371300216430ustar00rootroot00000000000000from dataclasses import dataclass from typing import List from litestar import Litestar, put from litestar.exceptions import NotFoundException @dataclass class TodoItem: title: str done: bool TODO_LIST: List[TodoItem] = [ TodoItem(title="Start writing TODO list", done=True), TodoItem(title="???", done=False), TodoItem(title="Profit", done=False), ] def get_todo_by_title(todo_name) -> TodoItem: for item in TODO_LIST: if item.title == todo_name: return item raise NotFoundException(detail=f"TODO {todo_name!r} not found") @put("/{item_title:str}") async def update_item(item_title: str, data: TodoItem) -> List[TodoItem]: todo_item = get_todo_by_title(item_title) todo_item.title = data.title todo_item.done = data.done return TODO_LIST app = Litestar([update_item]) litestar-2.16.0/docs/examples/websockets/000077500000000000000000000000001500564371300203525ustar00rootroot00000000000000litestar-2.16.0/docs/examples/websockets/__init__.py000066400000000000000000000000001500564371300224510ustar00rootroot00000000000000litestar-2.16.0/docs/examples/websockets/custom_websocket.py000066400000000000000000000010331500564371300243010ustar00rootroot00000000000000from __future__ import annotations from litestar import Litestar, WebSocket, websocket_listener from litestar.types.asgi_types import WebSocketMode class CustomWebSocket(WebSocket): async def receive_data(self, mode: WebSocketMode) -> str | bytes: """Return fixed response for every websocket message.""" await super().receive_data(mode=mode) return "Fixed response" @websocket_listener("/") async def handler(data: str) -> str: return data app = Litestar([handler], websocket_class=CustomWebSocket) litestar-2.16.0/docs/examples/websockets/dependency_injection_simple.py000066400000000000000000000004641500564371300264610ustar00rootroot00000000000000from litestar import Litestar, websocket_listener from litestar.di import Provide def some_dependency() -> str: return "hello" @websocket_listener("/", dependencies={"some": Provide(some_dependency)}) async def handler(data: str, some: str) -> str: return data + some app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/dependency_injection_yield.py000066400000000000000000000011661500564371300262760ustar00rootroot00000000000000from typing import TypedDict from litestar import Litestar, websocket_listener from litestar.datastructures import State from litestar.di import Provide class Message(TypedDict): message: str client_count: int def socket_client_count(state: State) -> int: if not hasattr(state, "count"): state.count = 0 state.count += 1 yield state.count state.count -= 1 @websocket_listener("/", dependencies={"client_count": Provide(socket_client_count)}) async def handler(data: str, client_count: int) -> Message: return Message(message=data, client_count=client_count) app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/listener_class_based.py000066400000000000000000000006371500564371300251020ustar00rootroot00000000000000from litestar import Litestar, WebSocket from litestar.handlers import WebsocketListener class Handler(WebsocketListener): path = "/" def on_accept(self, socket: WebSocket) -> None: print("Connection accepted") def on_disconnect(self, socket: WebSocket) -> None: print("Connection closed") def on_receive(self, data: str) -> str: return data app = Litestar([Handler]) litestar-2.16.0/docs/examples/websockets/listener_class_based_async.py000066400000000000000000000006611500564371300262740ustar00rootroot00000000000000from litestar import Litestar, WebSocket from litestar.handlers import WebsocketListener class Handler(WebsocketListener): path = "/" async def on_accept(self, socket: WebSocket) -> None: print("Connection accepted") async def on_disconnect(self, socket: WebSocket) -> None: print("Connection closed") async def on_receive(self, data: str) -> str: return data app = Litestar([Handler]) litestar-2.16.0/docs/examples/websockets/mode_receive_binary.py000066400000000000000000000002651500564371300247210ustar00rootroot00000000000000from litestar import Litestar, websocket_listener @websocket_listener("/", receive_mode="binary") async def handler(data: str) -> str: return data app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/mode_receive_text.py000066400000000000000000000002631500564371300244170ustar00rootroot00000000000000from litestar import Litestar, websocket_listener @websocket_listener("/", receive_mode="text") async def handler(data: str) -> str: return data app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/mode_send_binary.py000066400000000000000000000002621500564371300242250ustar00rootroot00000000000000from litestar import Litestar, websocket_listener @websocket_listener("/", send_mode="binary") async def handler(data: str) -> str: return data app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/mode_send_text.py000066400000000000000000000002601500564371300237230ustar00rootroot00000000000000from litestar import Litestar, websocket_listener @websocket_listener("/", send_mode="text") async def handler(data: str) -> str: return data app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/receive_bytes.py000066400000000000000000000002601500564371300235520ustar00rootroot00000000000000from litestar import Litestar, websocket_listener @websocket_listener("/") async def handler(data: bytes) -> str: return data.decode("utf-8") app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/receive_json.py000066400000000000000000000003151500564371300233760ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, websocket_listener @websocket_listener("/") async def handler(data: Dict[str, str]) -> Dict[str, str]: return data app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/receive_str.py000066400000000000000000000002361500564371300232370ustar00rootroot00000000000000from litestar import Litestar, websocket_listener @websocket_listener("/") async def handler(data: str) -> str: return data app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/sending_bytes.py000066400000000000000000000002601500564371300235570ustar00rootroot00000000000000from litestar import Litestar, websocket_listener @websocket_listener("/") async def handler(data: str) -> bytes: return data.encode("utf-8") app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/sending_json_dataclass.py000066400000000000000000000005341500564371300254250ustar00rootroot00000000000000from dataclasses import dataclass from datetime import datetime from litestar import Litestar, websocket_listener @dataclass class Message: content: str timestamp: float @websocket_listener("/") async def handler(data: str) -> Message: return Message(content=data, timestamp=datetime.now().timestamp()) app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/sending_json_dict.py000066400000000000000000000003171500564371300244100ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, websocket_listener @websocket_listener("/") async def handler(data: str) -> Dict[str, str]: return {"message": data} app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/sending_str.py000066400000000000000000000002361500564371300232440ustar00rootroot00000000000000from litestar import Litestar, websocket_listener @websocket_listener("/") async def handler(data: str) -> str: return data app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/setting_custom_connection_headers.py000066400000000000000000000005071500564371300277070ustar00rootroot00000000000000from litestar import Litestar, WebSocket, websocket_listener async def accept_connection(socket: WebSocket) -> None: await socket.accept(headers={"Cookie": "custom-cookie"}) @websocket_listener("/", connection_accept_handler=accept_connection) def handler(data: str) -> str: return data app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/socket_access.py000066400000000000000000000003641500564371300235400ustar00rootroot00000000000000from litestar import Litestar, WebSocket, websocket_listener @websocket_listener("/") async def handler(data: str, socket: WebSocket) -> str: if data == "goodbye": await socket.close() return data app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/stream_and_receive_listener.py000066400000000000000000000016551500564371300264570ustar00rootroot00000000000000from contextlib import asynccontextmanager from typing import Any, AsyncGenerator import anyio from litestar import Litestar, WebSocket, websocket_listener from litestar.exceptions import WebSocketDisconnect from litestar.handlers import send_websocket_stream @asynccontextmanager async def listener_lifespan(socket: WebSocket) -> AsyncGenerator[None, Any]: is_closed = anyio.Event() async def handle_stream() -> AsyncGenerator[str, None]: while not is_closed.is_set(): await anyio.sleep(0.1) yield "ping" async with anyio.create_task_group() as tg: tg.start_soon(send_websocket_stream, socket, handle_stream()) try: yield except WebSocketDisconnect: pass finally: is_closed.set() @websocket_listener("/", connection_lifespan=listener_lifespan) def handler(data: str) -> str: return data app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/stream_and_receive_raw.py000066400000000000000000000016621500564371300254210ustar00rootroot00000000000000from typing import Any, AsyncGenerator import anyio from litestar import Litestar, WebSocket, websocket from litestar.exceptions import WebSocketDisconnect from litestar.handlers import send_websocket_stream @websocket("/") async def handler(socket: WebSocket) -> None: await socket.accept() should_stop = anyio.Event() async def handle_stream() -> AsyncGenerator[str, None]: while not should_stop.is_set(): await anyio.sleep(0.5) yield "ping" async def handle_receive() -> Any: await socket.send_json({"handle_receive": "start"}) async for event in socket.iter_json(): await socket.send_json(event) try: async with anyio.create_task_group() as tg: tg.start_soon(send_websocket_stream, socket, handle_stream()) tg.start_soon(handle_receive) except WebSocketDisconnect: should_stop.set() app = Litestar([handler]) litestar-2.16.0/docs/examples/websockets/stream_basic.py000066400000000000000000000004341500564371300233610ustar00rootroot00000000000000import asyncio import time from typing import AsyncGenerator from litestar import Litestar, websocket_stream @websocket_stream("/") async def ping() -> AsyncGenerator[float, None]: while True: yield time.time() await asyncio.sleep(0.5) app = Litestar([ping]) litestar-2.16.0/docs/examples/websockets/stream_di_hog.py000066400000000000000000000010371500564371300235310ustar00rootroot00000000000000import asyncio from typing import AsyncGenerator from app.lib import ping_external_resource from litestar import Litestar, websocket_stream RESOURCE_LOCK = asyncio.Lock() async def acquire_lock() -> AsyncGenerator[None, None]: async with RESOURCE_LOCK: yield @websocket_stream("/") async def ping(lock: asyncio.Lock) -> AsyncGenerator[float, None]: while True: alive = await ping_external_resource() yield alive await asyncio.sleep(1) app = Litestar([ping], dependencies={"lock": acquire_lock}) litestar-2.16.0/docs/examples/websockets/stream_di_hog_fix.py000066400000000000000000000006501500564371300243770ustar00rootroot00000000000000import asyncio from typing import AsyncGenerator from app.lib import ping_external_resource from litestar import Litestar, websocket_stream RESOURCE_LOCK = asyncio.Lock() @websocket_stream("/") async def ping() -> AsyncGenerator[float, None]: while True: async with RESOURCE_LOCK: alive = await ping_external_resource() yield alive await asyncio.sleep(1) app = Litestar([ping]) litestar-2.16.0/docs/examples/websockets/stream_socket_access.py000066400000000000000000000005511500564371300251110ustar00rootroot00000000000000import asyncio import time from typing import Any, AsyncGenerator from litestar import Litestar, WebSocket, websocket_stream @websocket_stream("/") async def ping(socket: WebSocket) -> AsyncGenerator[dict[str, Any], None]: while True: yield {"time": time.time(), "client": socket.client} await asyncio.sleep(0.5) app = Litestar([ping]) litestar-2.16.0/docs/examples/websockets/with_dto.py000066400000000000000000000005371500564371300225520ustar00rootroot00000000000000from sqlalchemy.orm import Mapped from litestar import Litestar, websocket_listener from litestar.plugins.sqlalchemy import SQLAlchemyDTO, base class User(base.UUIDBase): name: Mapped[str] UserDTO = SQLAlchemyDTO[User] @websocket_listener("/", dto=UserDTO) async def handler(data: User) -> User: return data app = Litestar([handler]) litestar-2.16.0/docs/images/000077500000000000000000000000001500564371300156305ustar00rootroot00000000000000litestar-2.16.0/docs/images/benchmarks/000077500000000000000000000000001500564371300177455ustar00rootroot00000000000000litestar-2.16.0/docs/images/benchmarks/rps_dependency-injection.svg000066400000000000000000000540651500564371300254620ustar00rootroot00000000000000fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1litestar 2.0.0litestar 2.0.0litestar 2.0.0sanic 23.6.0dependencies syncdependencies asyncdependencies mixed02k4k6k8k10kfastapi 0.101.1fastapi 0.101.1fastapi 0.101.1litestar 2.0.0litestar 2.0.0litestar 2.0.0sanic 23.6.0dependencies syncdependencies asyncdependencies mixedframeworkfastapi 0.101.1litestar 2.0.0sanic 23.6.0Requests per second (higher is better)RPSmode=syncmode=asyncstat=mean litestar-2.16.0/docs/images/benchmarks/rps_files.svg000066400000000000000000001307521500564371300224640ustar00rootroot00000000000000fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0file response 100 bytesfile response 1 kBfile response 10 kBfile response 100 kBfile response 500 kBfile response 1 MB050010001500200025003000fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0file response 100 bytesfile response 1 kBfile response 10 kBfile response 100 kBfile response 500 kBfile response 1 MBframeworkfastapi 0.101.1litestar 2.0.0quart 0.18.4sanic 23.6.0starlette 0.31.0Requests per second (higher is better)RPSmode=syncmode=asyncstat=mean litestar-2.16.0/docs/images/benchmarks/rps_json.svg000066400000000000000000001257101500564371300223310ustar00rootroot00000000000000fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0json 1 kBjson 10 kBjson 100 kBjson 500 kBjson 1 MB05k10k15kfastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0json 1 kBjson 10 kBjson 100 kBjson 500 kBjson 1 MBframeworkfastapi 0.101.1litestar 2.0.0quart 0.18.4sanic 23.6.0starlette 0.31.0Requests per second (higher is better)RPSmode=syncmode=asyncstat=mean litestar-2.16.0/docs/images/benchmarks/rps_params.svg000066400000000000000000001142041500564371300226370ustar00rootroot00000000000000fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0no paramspath paramsquery paramsmixed params05k10k15k20kfastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0no paramspath paramsquery paramsmixed paramsframeworkfastapi 0.101.1litestar 2.0.0quart 0.18.4sanic 23.6.0starlette 0.31.0Requests per second (higher is better)RPSmode=syncmode=asyncstat=mean litestar-2.16.0/docs/images/benchmarks/rps_plaintext.svg000066400000000000000000001434631500564371300233750ustar00rootroot00000000000000fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0plaintext 100 bytesplaintext 1 kBplaintext 10 kBplaintext 100 kBplaintext 500 kBplaintext 1 MB05k10k15k20kfastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4quart 0.18.4sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0sanic 23.6.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0starlette 0.31.0plaintext 100 bytesplaintext 1 kBplaintext 10 kBplaintext 100 kBplaintext 500 kBplaintext 1 MBframeworkfastapi 0.101.1litestar 2.0.0quart 0.18.4sanic 23.6.0starlette 0.31.0Requests per second (higher is better)RPSmode=syncmode=asyncstat=mean litestar-2.16.0/docs/images/benchmarks/rps_serialization.svg000066400000000000000000000705231500564371300242360ustar00rootroot00000000000000fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0serialize pydantic, 50 objectsserialize pydantic, 100 objectsserialize pydantic, 500 objectsserialize dataclasses, 50 objectsserialize dataclasses, 100 objectsserialize dataclasses, 500 objects02k4k6k8k10kfastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1fastapi 0.101.1litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0litestar 2.0.0serialize pydantic, 50 objectsserialize pydantic, 100 objectsserialize pydantic, 500 objectsserialize dataclasses, 50 objectsserialize dataclasses, 100 objectsserialize dataclasses, 500 objectsframeworkfastapi 0.101.1litestar 2.0.0Requests per second (higher is better)RPSmode=syncmode=asyncstat=mean litestar-2.16.0/docs/images/cli/000077500000000000000000000000001500564371300163775ustar00rootroot00000000000000litestar-2.16.0/docs/images/cli/litestar_info.png000066400000000000000000000333141500564371300217530ustar00rootroot00000000000000PNG  IHDR_wsBITOtEXtSoftwaregnome-screenshot> IDATxw|׹0lծH.ccbpK΍׉ӓ{s߼{旛87ccc6€) ծh{v? I ~==sv3sΞ9B̕nBhB(;0: B(mYOjEzpnt VQgORxO]| }eߜnzg_/1NƯr{߿3W'AW/*QOmyT_6{`˧6;w~WE@K;,@:r`;~r*M;~[ P=~ZTbϏng2*O4^hB19#ψ;vBξ^v]?{&+0 4?eĂoVSC'mx]t%W宇+ J%E)ǜ\R:cRQl;wX_ܚXn̡6&||7Gswxokq[vws ޴wWHL5zn0vm۞;h*[~(AɤuW+`ܒN󑯍[|Ն#G6'i}t |J~w3kmq]%_}lWWn/6s=u|~b>ҼmOKqύjJ&XWiź;<쎥Wڴkޅ;+$:YO'>.o[yo[ԻN|r4Cԙͭ_}? Y:t>UW2a^ym)w-,v~Sr9ֽ n(ײcnY`T~go')O|(V:뮛LiաQM J{<Xu˩ ɚg3R|=7Y]fFU1˔|`MV~t1ݼw :BpU^?HQ' \%^p$jk[ o6yD誆'>)Ns 5ŗ#>O+TnwG,F}g-So=9v r|uO>T@}&?8.$?3ġ̤=YvQOvGsL8Φ7 *`*~ W NS1w7Ge8g-tȰom>|Jh{OVA X$&"t,SSѨr+̖ %P)0'U  'N3m`\Pp|$~]L7ں䍋qM.8K_YaY hG:<4K=Cxc+|M@?іNY t 0ud3'$IL [)z$vn7*h8M1'1P&>g"N:cY*,uw;R&Q8==*enn9@.uwAi<:dxMEiIՍ_=߬;#G^1%:mgZ]7}sm,~.bp煎nIt^Ze=D:Z@zm/P1-_N@_l!x9MԚ:#)D)Dug}].w&^ZX\3sdG1Da*sEL8YTT,h/***rl&e쌱6EgQ3_K\/QT9bHIQQf`hPmʂlQ yrи2ʡV/ ]C/[(|FuycGlSn=xȍw( 'XX_əFa+yU`7>:A?x?YթXeemd+<W+~rsikGw>63TGGXyit!K=W˿wS+n:5'[;C?SLk.{+ n+_lΟ'~f$M̰HNl'#yO9Dv</R&Is׏.\XmQ&l,- __~AV$?%ჽ+h_Yat[@@g,WcMzq Yvt]@L!,J/a !H!t!B(;0: B(;0: B(;0: .Loga!YQ, j4"g˱~9DZYNõ/js> a9^qծܧ9Mmᡨj_8;r9)OЁ,Pvo?IIЛo.ɒ%PTlݿcW4fdlw}qC0N~ҬlMA]0: B(;0: B(; K1CpˉF߅W aHxT.81SB(;Y .kt`_;Gt#q0: ʮ jaDRۯ+AYι:gXVd}o؅dRczMegcO/U}Uc\PiOѣUM9_;/`yűU>͜}5pˣ_TUop]C8ǨG~禾- }&̽ycį@py4 !&yZ)~ <*в[=0I"SIL^t<؊pG?Dپ`@ ~XtQ9?Prx#7}x'Wh)_izW _ dogɱ}XKd28dž .?^g^f$S]^{&W25+^Y֓cZQf2fsQOAv|HCZzxj+_گꆺͯ}SLD ;yɘnmkDqxD4=cVa5g=Ljڒ!_3F5ɉ ZԾQPk}Ր )E|ruIHXoFnz :m$g;sO?}Ů %3?)@RaB[=Ws+{gn9Rk4BRcCt (2.~ăBXʜ}Iq}1CM'2''ɋU Xngmy7gY,,|6]}Hxl~ÿ`o z<P2#uS)q0̱ymF3ESgO"W/oKyu>qȐDDX^u;sVӣwԯ:*H|Y f?F^)O 7&MRcgRty4@Et lsyXi#e3-Ñh;) )#m(,1Ӄ9 :cqӢi\luFb=1lpq̱{a=ɀ6n" _R#_kot wN۱Μ%Wsl163U邊YRFce8xuR4CT96رϥ~Q;KFM#J>kVGU_,b_e^kn_yx%? -RcS}˜{OLg _ZnPaR%_hF$5O5mz:[zZkK J`M֝KX$T绩(dx[h?>®%Ezys.ZCwN"bPFSx;3TZMqxod§MOiL)F~iN@un(98Yy4fQwRm~i?k;J+b ұX_pI]]4riwV>v-Ξz|A5 gўmvBugo~~>fo^Vp\Ot hv/c͡οvSIr=/vCaB@HK>b| 69bD- ͙xg`BU>mu$ p>xk ^YnP`yw(fnl›HC76&G{ybV$/Qj|l;&:F+iYjL r$2 v&Q= @˭Mm1)kb#3R2r4ՔN1\gfo 7,n Ќq-tC*"ϑ~Ign)4]e Ot4qɚ'U5L2o^d"%K&Du `^e ϰDG[3A dFh (ߜ@ '#\ϣݕ=?EU~G"m-CO5&&t#a7ywRH}'SQx TH8WP_^Niy*d&2HT2 h:8T&2V~f< #UȔL筸#4v8Tqw$to'%Xy_,S _:!r=Pݱj< 'Tccqсjb~~dwx]}P+'j EkIpd>:|ƙ.i;aQ. hDЈI>ȑ. L )A: EXU_ų:B,_%8VdrA?s|Ϗ6꫈)UFa61o5Ϊ_7?bxmvp5y@ʄeb4*]tq^bgv͕\n.5VroK{4v1Qm㞲 ]}d!X(-5-,%QvGOPSjFy"T'a_%O_]Թ|V\ ffTDgXn} GXhX"W)x,Rq?U=9*){DJJ ]'Y\p?|B(;05 #g!0: .Y\z$5k4;LSo۸IYD^G6{3/*k>Txt3swtU5-^:Me@7+/&'5qQ$wF$kW%37Pk*>#otv7lQ7|5Z[^M^2{ae,yz ;w Q7nޑLDCq7}~dXBےіxġ9E7P'?־0:OaU 1sc?dp?[ߓ:Yvɰ| #d壒mM-Ho=o.VW. __ , W` V~#=,qu:S'vSHIoR0Tad|&ӃnyTP9yu++˄x@H$Ӏ,P^> 7ѳ|CKR#ڰ6$-qLuQ28s\*֬{8(n4jBLPZ7/XYvo)o3)g3vD7߻~h t~gO=E |> J|<8'@04BSIDATO3Sdg Ҥe'u w,g6s9YeUhPS Y\uV SgAaw00|OqN> 4`> ԡYt'?,,y)E)g]3P|<u@FPvBat@e,eQw`> P`!FPvu@FPv#qh%h1`t@o#5lIYGf*yso؜/!=WZy1†[vn*Sg~+uFŘfQgXC{\RN|g\x9Kh=m<ֳ:I>,%oR_ʾx'*s|| R:*%5/%т:P_j.^糘|kD¾v)O[^2CxqoFmyӷYyʔfbT9}=%RqۚKW$OMJ=^ N%g5'У_\Dɜ!19Q:ECq7}~dXB>tϷO#6[P`yVk='KƴInTcjiE2XtL:˙OzcǘxP&&9}+piHyS I2{10x?mn`! ) Z$m~_=8 x?m& `AQ/8țDG v7 1~7|&-sv »+"o8U'M_qT$ePe4Ƚ:G8 ÿ };'x~yQ9qieIb;a/#׌ ΪNx jw[SzTm&eHħ߱;@Kio (U0`^شv(JLJfTeߕYGz7pn> "I jlD)+d?ycRV 1+I%WMK5/YVkVsVJ":^Kf;(%NOh+܅)e?pED_D]G,~") [ ;@SJ4豕[ v,ZzxmCĨ鬿@0W qptwdXߥG_ uo) ?.ѿ,+a';:ݟ{hAb{߿Kz?9ne mIHFN!DyVXkj>S~K+[CVwѣtJ DDoX{=@U-ȔJ'jih?* *!u{B{m$1C"H&:ͧp]T1Ӗ\ >f T L?ʼnsU31@%~i;=Q H0[VKߘ1l)^IqX#܁[UVD# +KбmM ,?O$m+{ߞ*CsLT&4ni\M2URLbMPOW @o'W'KrH.,`U4*&n~Ӌs2b&`hG3K}6%d͡Iݾ|]RNcD*9=i(aX$yc LlVOK.] id3%I@43 Ѝ + rOY@h)v(s^BL ,e@"-%U tASoUztI|# zO (۷#$D)t._2 / ؂)y*r W׋kfJ1l|rowd"W{h\&^L֜6*L,̥qҐ ^߁<"Baȗ/}1;xnQ_ ]ťߦ?Ql$RaO#u't~GU-ᳶ4J[Q*e5SVo=qI#ͻWe/Ē7- g ) yKϹw|TP VK;vUAa BƔ*2sq?*jrge\ґ7|ku |@@gXtj+40T XaGt@rt5eg 2 }juHǵ7^Ȕz{\= 8^ 9Y\cp ~=!6zCD:)9% :B*N 9|JȅL8u>8j(UX9.kև2[toSoHUq8fz"Dg,nZ4<ʌ3^` z8^}djO;i#G5zH[xS]謠iH%<X3L g8L)qV`OAw %@_h5P~gd2dÉ ;Gy}⨑}4~%u)8Ċ剳Uڦ7ͥg#dWj&CԂRUH"&xE:$j8:{yZ5pd͓*S$PM ͨH;c&9i"{TxTG&&T3οlfŦHs\STkJMs}B?{2/sU3;PA3;!l_Q*^]׶kµ٘)j+|wf ţwkNz,0PΖGPV]? g#fa o/MU9fS#5e@жF3I@f!_6/>$O6h]K#@|u6⌃/>* v2?Cd73+Mό$ۺ6ڍ[Ã\UQ69}]SC D,\0}55;'рnK aj>x[xlrwTa6scvAsqB-_Mʹ~Y7|r1wVY-|%YGaUI__+.w]+fWCos}P_j^>"|2܏;0BggB(;gq Y zB(;0:PˠF$_q%8~q}> Ke"pzMVU ;3l(;|5OwV@@Tc ӿRemW?\t]|)0ֆE{=B4K@|7+/&'5q]S6 Hn窊fo>CC'Ot hv/c͡οvSIr*3iw=&9du> ;w Q7nޑL*ȬQe"/q# -~:nlxB[ƫ΃0lB?|s8W PCqt*ɺr  $0QO^EژgR/r<[λq%I$WoBM({ʳ@-ϥ:/*t|(HJ@9v#yr^Sjd`9ZRIR @ZC47Ou mKŖ{kjz.,xȳrreyPe> H)y-W )]#/_mK r/:hnNJmpb_ja?1 ^Z_=ЖW΋Aqx^+埆3f$w*^9Cwu1wsݸvPEPFt?PKs)6;ɯZhlqPi- ԨrljKZfuL 6SEI]z3*oZitOZx62V-ԓ3axOWdv }ThBl,*j휥4K :ni |s`DȕC Rol*?;G900*'Guvf˻qJR D$2 v&Es pcK #5_ᗣ) WiÑ\M376$`k |9"G5u\KXZɠV$~QFMen)8UC@-Y_t;롡-kt$)G^ U{d~ܬLJ^mkP6.arݸІh )X2%S( -Sx :ޚ'LCU]]-! H'@B=SO 5e$4 1 ~Ļ⹆daab]gL,G9Ю;)z11)c-_Ώɻj-GZ;0/<+2TH)ůލ;t]/`:Vh#LЪ 1r[g/r94TS('P9`/0*_yndӵB+;b6aY7:!r=PϽPSi:3'Tccq|~i,ВTaEp6 @#Ɔ4zf0]P>kB*hlua\rb[67RlaLYbO;)Q%lEK/W9 )A: EXU_ų:B,_%8VdrA?s|Ϗ6꫈)UFa61o5Ϊ_7?bxmvp5y@lOeb4*]tq^bgv͕\n.5VroK_./(O]{[:VZ]]9U;tJ׳Tg{bHƭkl f(k(k5@sKG\hlƞ97 xގ;˞v^G} @Jk *̒Ф+@w{aj&-%i5%9Grw3_\ *EhS DqٷYb=h{s W"EO]7]mPt@ (MЀ0ŵ #gY Y\4#7k 1C]|B(;0: B(;rM}1# 2eGUdY[| ¢`@*8V ٣m^-N?aW4yr{ş`XX>+*LywDe0: B(;0: B(7JIENDB`litestar-2.16.0/docs/images/cli/litestar_routes.png000066400000000000000000001273161500564371300223470ustar00rootroot00000000000000PNG  IHDR-sBITOtEXtSoftwaregnome-screenshot> IDATxg|\ՙs4nY%[ni1&,BIv!KP@0BQ6%KHrҸ4Ȗe3gs9E3 @7P'@ KrH !"(@Tm*~Τ ĘWso3f_ ]ۏNtU}΋I|ʛKeg<Ͻ0#of)'g[O9;z7NOƣ>ׁv!r9㰉ܺprnNq4{?5qssui,V8⇞Yݺ_;y(8UϸB9U_.[o!ߴbix^n8 GN25eaњ o9ZL| ŪVFm;~϶U_޿ p`?Wk ](]λbfc;)R2MmYwTQlan,>n}/fa]Or;5n~.n|2q/{4y&d(*xя)k]zm*43g\w>C,W87=c63&cwݿtb~7fwa jRB&2L߼G,~75>RS,zr?]FBV@iw[?)6;7¿E͛Ƈ9~{W mi!Z'+v_ w?J{Zhݿײʂ?Wӯ1L9XT ,#e`u`?qKoBrY.@19%@_uyyGOSFgĢVmΨLV(в鋑F}vED,+sM=fKjZ ruc;Ι26+u(֊vIwN5"5u0q3{ @} fH*C߁ծd-|vݎ?R%gkAlyI*[T}5S ?ovU%nrI6 .}*|8iCVPbzj  |W,#e@Qێ7θxء-XeItO30DZtMtQ0BW6 QKtI:#IԳ``I]oX%`X&|y0dw `~Zw_B \^>BD2\`Ǐ](m_wϛӾ^|o]뻿~E5G,Zt߽UM VOT#ۛ޶&ӟehŢү:Uf|oUJw}:RѨj[߯RM}gJg/>I.~r⻷|E R>{˿]~~V7 UB!HoΟN#Yq<kb0qi<2r-{BAAm/7p[)Gׅ[Ҁ];Dg}+2ynp*ۉV8whG9&]^SO-rE[3o>NWp!J^#+_ !6#2&}]"uH]% rPԤ?_Gcun9(0t 0!g H !"(@!rH !"(@!!:l|~+!In0!r(}Bc4ً~Ľq*ǕpXGV<ز|=J|P뱫QHl|v/ol{?뛿[_P) |"s5-?){+zM㤇e_w771(kſwa+D_ʉc*?>=q$8z[C8$s4EO1oEŪ&Z+U֪"6.z^ ڰgcƟa!fb+Cs~͒nj!i1;천w#OwgGQ%7aN`UVіYd•!u<j#9|19Wv.p .;{kiDq}qV , `bn[}x1Eᔣ$>vZVY @ϸoUJq(Ӽ8žk>O^a8RΠ( }ߍⵚ \D~p/ZU;G4M"c' TbXhg} &.+n\AOsray :PvHi:}F+{/6~tq!Sw;%r`;rG9ϼB)3^>ՖN<"T8.9AŌ^Z{{Cx.kWux'tfIzmm  KՁg`n:15cڏvՓq7+ q""!"(@!rH !"(@0ʡKz;B@B!rH !"(@!yOh5Lj33_׍y$m{W+/*Ou‰xtsα":ڳ  a_l髩ҼL.R,M @9򬻒*W[> ŷL[E_q;[ "uvۈM2*T.~8FH*Zڔז(1CWSW/}K3㘰ML̤14'žn?a/ՋiJb>R !BN>O̵]>gͻJ=YT4EMo2[Surُ)O4rXqR>\9-e.nБf 3}ѕzϤDEqi;)JΟ`7qmՖ;\͊gO~PՎ4e{=&(Crry.=_}壏8Cl/1zn&{R(*)}NgkikG8MOߕY|)j\,}__4 WСTuCT*ZTDC(*9[1䢆?gw==Q.1c/&&n{ڳ}N*.϶ )/K~ :m}[w #s뱸'I/M,ϞȤng$ y\6tV@b[:kjp1"KJ0tP{כ/~|&BC#wzLLvZd|dRt3K]LLF:zZhlqf9LZ;NhSImmJfPIcdb#R0%LQ贄ʇNIP,46 'F٣/FG[0t;8iS"'#&c-tCrwJ+w|XDAg5rohN~oluLZUʒ,@,YF*HLS9-52&4~.7S#h]vTm 5 @EEG>=e ]bV]&*9(>12U{tu(kiF1#{j_Z@ҜhB+rja,WުԿlNɀzłos\UsD,~&:%:G2 N!z]+NK;:pXJoQʀD ͨE>dZ1zEMzOw(KW@ <6gͲ]+ cA]CXA1WrUnl VpɉȐD4cEg͠M12Bc܂ 2)S7u!:. jwP)9nw;6U bң22)ndԇtp %cOh7EW&Im >l0gf q¸ɶ@ϫkO'y}*zMj湪W|E/i8Y/l|r{sߡ@k.\=g? Y +{[?vkjd!ڡҩSUuqBy09sZou+)Tm.Os]==n10aSǿvU &ts1"'U:EGrhЎ};?iC=Dz^lPBa[9y?L6 gxx+:9slQZcٶBG9!Qck+#m :(3}Q2*{ⳬ){Z +̞i ґTiT{#c1&.ta>ZNJk‚ ٷ7o?"CӳKKL.=ׅoXgr(s<`_3 &XإO$<.%"76E{#+U/Neh^Q-=_v G-LCibOm?ێ9/"Պ- ?2dP~n{zʽ\Wjꛥ¾]pr]Bu+ДmQX KM:'RF#u1_C7|-}ʽTQ,?|]߬(ʉ>j'}mI| S,p|7)_M_&+wBW_r'Miw>lvU!IVKYdijA5Q(GʅErHHZl}iJ~huD2f KmmƲlV/3>}+Ղ)N i > TT+S s*tA8B Ga@oW[ZΚfu_ Acq贄svߦ[)֟:{tVm{S:̮K3p0~hK&Y`Q$#?瓦H7E-SFߣ{biT:N5_Aŗhf.ebAT´;P~tj? P{+ݘzi`PԊ"!R5~[19\ǂ­NӁ1rMGv5}Ä ϼk1cGceq (;3Ϻן f;Ѡ~t_;:6~nAPS3t UV<3[,1{YVtRIl[x)0>f;(N@‘ ' Cr\Y(ûYs0NNz@}ˏnz> 8G^;o⭶e6>>mQ)[2kcZ)+t(,ɡb)hF#9d ʘ(`Qt:e̠b eOzp*61Is&PȆ=~՗L8Ō&LW-[P0~t`ܚY])l۬ ~wI㄄bF''Fگ:ͧezUu08Ė$$9f϶:va|0n=G@??<Bٚ~kqa~' Jp$r%/΁.ywݟnO x]}fbDcmt$ XŽR4!:pP'·-\?0~n7p2^~.s J(#$4'﬎i`n׭$Ne, ۍ*9ڕG.yw<[5ԙ56P[\6 BQNh BUuF ȡ@!rH !"(@!rH !"a8..6h}?~`V8D O@VAx Xtp;.>} IDATX䈏f?@\;u&&jݧ#Py ᔣ.A}5[FO: 86P,(B@`cs7$P[_?MvA{Pt\[,E=»yh[e-*?U)EL~\@{pDVaKh0` p! 0%3nݸE%* %?+?>+ }9ⵚ \D~p/ZU;D )Z!䡛JG1,/*D, \g'vj &@<$ clVU hm> TՃzwD?ofE'F⶚%ژl.k;at>&>+z6{M>#h<4&0@I {C\O6 bVUb 灎ۏ 9xB앿@ŅL麈:c ٚY_wڇ;碌[C^lAIHh)nPbƃmSPsP`9rG'7}g~_˚bAOq1^=ڇaX-B}쒇*{Y+@]3׮Qk$&|=BE`3#0]-a`:p[[,W`kLع]d͵q""!"(@!rH !"(@0ʡKz;"y;4pC!rH !"(@P|eʧ?:tsα":ڳ  a_l髩ҼL.H,&ᯜaoi#1mswD;zeUʩ \qT)-}Qc;r 0$ʡQ#XFpy#"yN}/8A5vC_S7,[c0%m|zR+N==Cb@r'TsRk2,uS;@:>Œu/R(.;rSyg?6'o۪-?w+yΛϞ hzVMQ$8yz(dgdGA3*X:=`mDɽM_Q߅NLMx\/ɭc{XЯ~Tr'O4W%[P+9[\mAT煨4ǒݺ9ZF~NIsg5ge,G'qM $uݚK?x݋J!ZE_{+RI0>'.+qc:͋ڽ?k'c z&(XHUO 5]L˻jy˪Y:GQ`mo)kŞN[%B\7ɩhpJ)@Tr):hri&N<ZP 2zޕvGz@_T~vO Пoo oؔNS*],LCD/(0|˽Vâ9]3͆1jU2*Kԛ#˨_)i 0ǀ RfW HcEnkLijwadKd3LC9X5TV('Amݧ4[5Yi4=_/c iN49n0uyv`oUZU6}d@Ko7aU.ݯ3dZ_ PWWQiE\:I>!s٘h`g*>KE6gZrYe_ֆ=iv.^i&qc[R{/մ.!@*ɧf6工5ߊMGh[}c3ԩ~z- ?§zFRB⪺J?5u9M<%S`ەK@F5!rH ~kB$>@!rH&570•c FӅiKg͒<إgg3juQǿcIz+眳sLl{UN@kѶ+-zߎcf*ㆠs>}~dϱ7P~ؖiN^ ab}/ފu*)jTX-QNH沣y]IxP2 a-ߏri</ɯ.)'> ߰Yw,YJEKF?>V SQh2BSuhں6N',h_}{CL!RNp_5= ғ]!~]hqUԗSa6~5ǥā㘰M`sqYYN>򎿲m\Q.xQ:+yE+Z|rُ)O46bz5eb&=9I(/uׅb|2M-g1X Zߵ%!G \SwxOOjUM}T0ZطΗTKnE2= q \ar蟠YMB)#ߺ!wX>>P(i>.}oVrnVa'gzI_[`M S,p|7BPOs=[{Bv];+/ôKդmO{oԉTWj5q)r4a>냞Gpz|˒$Nok֝9 ݽs \Ϫ'I/M,ϞȤw9'첋sЭ^;$Z/]f15;jQ Ww9䌑> 7nپ =?=hRo O ;N''ʔ1KXZUU?~pɕjDZ'4pM k'f;h=Rr?]eP1'rŴ)LtJZg^81MdbժW%#[XZԿ*U;YG`mzPGj::-\ݷ)!ֆG gNŞ{&}p#>eޔR o-RMX)((e{wOS,JI ?/qŗhf.ebAT´"_g=#P_AB2ľsA)VNߧ=1+Pj+ECBkbRs\+[7Sd8xj yc2#яczKWuF7Iyֽ,17>؉'_k \THU(.Ke{]TVml6mi|p5tJcl7:'B.PG HCφ!M,]IxgItrS=iI e@[ChCEtg}z> 8G^;o⭶e6>>mQ)[2kcZ)+t(?-ɡb)hFWE1C(AY/nY&S1t ٰgߢقIǣq܄)(U1[Qwpi7~E(:kmʠA*76Q+Ddd[PM \U3+7şm5n#T} XhPsUl_ϲuز4v]U `t4[?OOPF>wy#+ytt tS8 j-F4yj1B-V~H'ţ a(G^G=/zOSF{sߡ@k.+>͢4v]r|H}t ;o\id:en]$P|GIhrOBY:ד#yܮ[WIXmUr+]xνj3e[SʪZg sCppl~T!,ڇ&+F!;D9Qk߹-w/5a@-B$@C DQ D9B$@C DQ P us <6h}?~`V8!OT4V@ ᦦ3͒9贘HpJXvY{C C(ݒ0'ONWS*\sFh,o}:A\5ؘ|JM΍\w t .VH[ޟ0Uuld4Qvtnĕ.H_ b{б9!n]u{3TP 40$SVH.Ap'>W[5Zn}$v;X :X%Lazց}J@qqнU pcg;*v|DO=cE@-\6ݎtp;!*U~PTx$^P} CUN,D:!DCُ2flp@B7 [>ÂX䈏f?@\w>`bn[}xh15G86_ Q+l?=  q0o;es7$P[_?MvA[NY(:~Agyi?s;8>vZVY @ϸoUJdQ(Ӽ8žk>Ʒa8B1lcC&a JubgDܻqɋJ1DUKW~ yw__3hA'A >XW(RK+k5+$X9_*wt_@4MAPCo!T:aVQ!bI:=?S1 4!cõJ@kiЦƏ л؋_>C6/:ą9jn7;,hcBbr(':Pv\|= & HCjn4J gTjŧ1SP^ g* 1@@u g*H^e#h@ ,>FBϰ(Mybҏ..dNEtqlM ,pnUÝsQƭAc/pR$$ m 7r(e1JA쀶)]Ƌs(`ގ9@y X? @m>`>Bnq=*BZa >QfMߙײbPSfܽ|Wa`-![ 'C%Ubb9P1=x=Pq󹋿r~@!rHV*'j<7n&Ml߀=˔O*tF8&n $iۃگ ɣ4%޳.GK_mϺ˃y%tsα":ڳ  a_l髩ҼL.:Nb? >Fn Wr1ʑgݕ4Py^9oFip`WP 1ih+.16}gAnW&X IEK71&1DasqYYN>򎿲oXXKY* _Hqj+d7x?KOwuŁm @`khzKjg%pSٙ?fR󸔸~tێi3(D/W}2nK5'EV-sZR7#$O]>##Zg+I+7wXlsr&q\GYlg_e@X.^(dg1 bcc,yq0ET|E*cflE'O4W%[PK kr]%uuk(2okv=7sH4ӵK^iFH_Kdv=:DB^=3凚+*! E}\;ſ)٧~__B QʳWtpKJe2S]G!Khہ<ۂ$fG Qi%c[u';s;ڵ"2x *$j$YOH(5  ͳ;>1 2ފT}W]_YR81 E拟5٣(1lnRUPOn;h0Ug%g2qBc6wab*uD2f K˖;ϬgG⳿lZf+ -FdHq3dgUJ G*E7ӱ⇡؉}*:gǪ$Bhqr`b%$ mmktoZ~~'VMr6f ǬTM+ן&&kjTqьOh2zEm;G~pe[CeEPQQ1hZ?9dGУǎL*E>=eF 4~][5Yi4=_/c iN49n0uyv`oUZU6}d#[WÐ5/-J2%o{#t1 qrmEҚe[Su>Zg!>x䌶7v-\c"F$ȱЦF 8{?*Y탔pCkR6@ a罨Ic쪒v}1ehT/vyO4[Z!Z9i}c:$FトơNrrJ>+6tN9mW.aAH !"@!rH !"PO#\q蛐`O,ELuQǿ6>l:9ga meq"vm94VhG[IT A}F}^P4!cYo6(mw-Ӝ&Ć3^U96SԨLgl[|swᣜ\-iŵÆ)o|?ʥL_Ծ|&Lʞ,k' Fʞ~ guZCp4*td+-m՞/X%||L)&Gɸ ]Oֱih;ퟰ}B 1ۏH9O C B_K}Neh^Q-=_vn#|v +=5#j.o$[#oO]>i&Ԗo,YYڒ#sA Fy3:TuU%Kxll`!&B.L&LBr/aH`X&$`!`J0U޵XV[wrm wBT>z[֩S .}vA1ongBr,7, WؙHRIQ?pp~fv(XhӞ~+В|?ݹ]y|»ɠMݷ۬bʣw],KOvv\b&V? CqX~q+*IJ1eم'}{v#{HS=ll{:Wp.r#%9oofW `ГF,\57J8^{҈(2`uVR|iOc"7szf)oF@O@lOߵ'1nG2&-ڿ- Ʋ<.56F,?-v(15[ zd=%(r _M J4jQ ͡3™TQS#v8@47̡^Z)IJEq=@;gX/-T ޷drC){F!^kVbsuݷbǏgL^6] 0/GX:<Kv)>#bf&ٓ&ڥ 've*:<vyi3=%%k8*>*yvfXov Ons(`gx'իsH<{j${Ңpgw!xg3^U J] rY[Gݘ؟+~T>1Kmt+0 &xy&ۓ6)i>{p_61Osr1VǦy-sA8 :?-Lş;~S!ء2E /y#xۨC9ؙyL~aæy9+JBAoϨRCK(:c82'(O:!00ʀUV: 3CU\ShOʷqӮ dTGe1jNV=bz#J႘ 5& 9̸aScR=iuo`ݱ9ZrONhVC #wBGݐܹW8?|@żs9J[xןv>瞴s gߦ?I/QoҟSlMCcڠZ44A x*GC#Јr44A x*GC#ЈQTb$%~\#%i>{W8-?t¬$yḪ6>;³Z^3lXS:^Ɏ&`x`ڦUa^?S˿m[[}*t(mYoƞ?gp; raΆϘ/nyO e^b/E8-y:BȜ?]tD].C(ÿS!\j͛ >V+vy`ٷ,K?>?AF}<`YM2 sg`X+ w\t$-&ٳ m36` ᩡ(!I:ߣN"uɜc"e9΄#lmX@`p Pg_%: zޢ]R!7/2:Hޘ_e}UWG_R ^ycwmrTػPD@@O~tbݙCzM3 Uvߔub^"H#0d[ѻ־NN¸@V`ڰv;s+IDK99a$ ,bѮ>Jt@t#=;h q/BnZ#{P< IA)ݏ i&qz܄$J쫴Yc5~Jm_hVZ+_}}'X;hܡrX {pzŵN1XHj*?|/uw# u!Zj )fPMu#z c4m6ILOH~(H c Oӌ bLw@%O2҄^z@\zr+_SJ&.„re{ hdz衮460Pd /}%B3޴,X~E[ w%IJډ+˫d8(L=\@}n1^#Nj"CǶTաL_}0i7[<c ^9:g{WZǨ^ 0YH1MoRw\_gy3ÖJSYD8Ok߆LAQzu-qG|WC%0.n`w([_K~WS~ ² c &$sp6[H8ޢn=-`YJbNի"5Dڳ0d#`0ۂ K:jR>+`Ax-~l#`*bl#4t,iMC:l6[-֕TF볒VwL)0 za@i?d̀> If6$zV$ .KIi%U !Grl22#;Y<)iH_Ax.]YI )H[N!x]O.z3 )`Ȏz)Ek81TdA_lӬ%)|O2*gk ލO۪$Nc!0pڝQ'ٖ$ ])@t@A j;f{l߹+k~[6b {7|Sn6XרGFX4NvgLøgȬ o0)ק}54>hcFK˲__g#7~-8쭞]EWl{պ޹ya^Tַ- .7g77~e8`B3o7̾0}*g_r,,˝ -bPGo$9u%Vs~ۆʄ*[&Sqmm];O"TIYƐ/A& I-zHS: Qtw.q7J] lP:B;1d SQ'o4sC3CHi ǀ+W=0&`m3XtN( b?>po|pۣ|'C^ ۼy2m|tc/l6t6+ȊHNjd_"֧ÃpT=,U0|H wDͼ5uX'xy<1Qo8a/&zWeMH6t/|F7I&ߢ YUU*`ΛJi<: GSŸ0 ҹ3[ :{`A Ir vUv! %6H dS 0c SAAV 2,*-S(t}O{hsJÒo.~0fUO}Pȴx~o-UvCfgI&9&v @Jl3qC#?'ȁ|~B4 9;+J~,cSǩv$ [.+}*%y~E=/R)8} uR˧c2=%'9 ޵ c)-UHc-;'%y7̕(,szINI}HVΗz j 1/ 9jRhO:~#,qG#JLWHcNҒ%?RY(tlT]ҿgܢRkS#v8@47!;tJ@H6\J RJ%ݮ_H#`S|XV$M%~&.ekd0KQOW;|0D֦(P~=;o.?0tr Pt|ji>1ik"G%OnPU4 {KP(A| @]Xʩ'C-‡>|.e?ްxj|!k"lh^psR_\6\,0R n.ПN:Lc$h5+戮OZx=  o1KmC!޻31 AH۬NI\sSci[w^iqHE/ÿt?X?%WtMRoNeF9(A.mSrCjmc7Y$!_;ƵRtYscΈiyHR.ccS 34QR 3Ki󑡫D׭|6pLr.[!NS@R{)"0rn|6?p5%/bx$,%qN2ie)/~l"]4'yA úCv9ohqbAEu-\JƂ%54osEǺ7sVYC\תEP)*$,iS~U@xHz^Rz>STaÊ4-\ C.0$`zƛ9wwߏ}Dt|Y:2|&~ "z%R(' pWP%h?Jt6cWiNV=bz#Jz.~h_p|dkz%b;|QZ270T%bυ G1ݰ.Ⱥƌ{#wʋ%R?4/c<>qc>qz&ԍ/nb zrgÓ( 3'Kqt] )/5Fn_7B]!9]NHLj$b¤\0̂%Tz6\_J!"Z $7ߧ;Oxwne{7n2hY>3Ox}{Ř{~@Wv3 LŃ)p.K}Đ ҷ!W~(^/wOB}BX6F,Qond/9U}P4~ ϶} ">\BZafvţ|Fpεꪬ~͙5e $_nյ֔)g=MZr ;K#e+fW +aHxh&E^ #Q2c3>iLfX,7  8FcIZ[2ey\|k$-so @:]%ϹMa%o ^T*G~uTڍIΉ"WxL"G2V?gl{&R`X7${829'~`&f|Iu._Y"t=/}!hGA'rb*b&[ 7ʮHD{S񌍕zϨC9ؙyL~az6-11 ƱT;4Y#5aO_]I`;؛j1ԘTq][u7Xw7sNȝQw7$wOmi;yEgo1cvc&w O';ǫ6L3d9x3cxƗ=,Qgxƕi=,ٚƴA;hhăV9UF^v>_ayرE[E_{ J@۩;vV)dgn?NGD3ۦCB'B2/1Z`] IZx"}ewlTj!I{dN~P :.߿Tթ.lMg<[kkʟ #TPB>t^rp&9^ P0H^J ^ ;HF.zJ{ Y6~eH P$Q`dΌG1dXgBvb R@,y ^v|08fDҳ篒Z| Rv=o.U) .>A&EF7v 2Gu%⅏G1vv !H%{hE_Ntd_oO lGw/֝9;4N4PeMQ'uAj)B49Ih$K9 Ha> i9$~Ot oPJ~>),F.ZC$ADQz0RJгvY"uo.;7 QS/@Ll"g_MXKX$\MϾJ:?F>QO eC IB mC &뎢8w)^)\^iq`D,$5]JaRrѺӃXp-]ՔvP[3(wM X`=16$' $?$Rc1P§iF1&;'AbBuVӔd@}=# )pEk:저, FOv(~it' YI A NP>n8 U:U>2҄^3etɥ.Wmb5`r+L8*X:./ 4Rl2s |=Py r(`}y˗joZA,"r̭Z$G+˫d8(L=\@}n1^#NJBn$R ^t%}lKU _an3B}my8.@rt0π(Q.Fa9$cޤg-!f,*p־ $+,Z㒏*oBL}H1ùtbdY^]aqW:tʊw? , 0vzbrI2g󰅄9z-#VX Ȫ$X*"XC= C6>b-ڠܰ橈`.Áb( 3hcä %,{!Ye۩=r :]$K?r[clfKtzu$сUd]$%B L3APx"Y3Oz ɯ'|9IKDRRZIbiUC\AwN%O O }WKEV5B Җc^8%>K C 2>)R` w;HNKnXfNccLiЛ TdA_lӬ%)|O2*gk ލO۪$Nc!0pڝ8s`2PίԢEtb#7~kzȄr(;hSg\$AGd/| 51n(B|e+"/Fn> $rZp[=-_v79iQp~MqZ;7/\wCԞeaf4lƯ,o){OY>K^e3k~e ?8ٹĆ|sN(7J]W?LsawmCeB-Z])J8Үܝ'Jt*T,WSQc^ė RTRl=$TayQxM`[(^PvE%.ia6(G7Qϡ!$ c P_Tj`,'aI }7t\Q>nj}/m^z/u_ΝyRM}A LB[ۇ|a"|.D4u!|B&~Hr iyۇ},0s3 ³`_~}:`ǿ l|OmzY>3Ox}[iR>*grZ%\Ybsj{ub*Ovv\"fw t?<_$_74џ!oOCB,'w`Kн/X~q+*q Ouȕ4r=Xq;fRKk3uUY/357kINnp1pQXWH-gCJ8`d0 ƾՈ6T#3?ixj.`0q͊gm&G7vվ@%y~WtW{A-ELWfӱOҲzG ^%4QߨK!fe jՐUL.r@CIthȟPVIA% QSuc^6] 0!PMi>orD]qr7 gmN,s={&- KsrfyfhoN.IdGȠ,:~'+IX^@=m/Z/C+)QOrl=(sFulC I:B%ؿ+ tppٿ.^0w+R:#B/w*Yء8Noyڿcl1eR[3F4z~24h'`uN⚛Kۺ+(M쌴]42Kg1%WtMRoNeF9(A.mSrCjmc7Y$!_;ƵRtYsc HR.ccS 34QR 3Ki󑡫D׭|6pLr.[!NS@R{)"0rn|6?p5%/bx$,V'Ȗ/Hha gs/~l"]4'yA úC}v9ohqbAEu-\JƂ%54osEǺ7sVYC\תEP)*$,iS~U@xHz^Rz>STaÊ4-\ C.0$`v9wwߏ}Dt|Y:2|&~ "z%R(' pWP%h?J|tcWiNV=bz#J#&^sv`o@kT 'G֯Y".)~.sq8K;oZ{O<C_R.\pú [사iر'1rV)Xra e9MdU~[À5Qn4}q3lSs?bW6f@:M|#7|Ԙ3MS'246Np|( Υ- ӢlUN<(~O.1$y73.' !71.cU(9O&Pk4-cy4&omj'-N?Rzx i]0/q黫R{ÀGY2;ЕthoIwKTi^nHdo>H⛜_]WHXy }Mq읛+ދ݅OyMX!_OZG[{Ӎ+UFs^X6+9-ѽgpts8ݹ;j)ɱSFNH3#WdcpGے\lpM븗w&n?a2e e RlC(:À!'Q͂\xoB5U?Bt3oM7S '-L__ ԯ1Sש1Y޳4 |rrr 1:ܼACK?hc9r?g+v|Vо+d| Ǣ}I}IQ,5QY𘾄Oφ 6C0ZDk[Dt\ ߚW-FiLfX,7  8FcIZ[2ey\|k$MK Mmr?ν^7t{_SG瑽s=ݕ(2B{m>W [+j0w5Ot>ʑ_gmvcRsT CF !^2H㑇Ĉ5OL2I q d&rO.'8M@\ܡEձn{^d B &vNF Ǿ2ie)^YBq1R-]φeWtԽj1GAœ6V+`g3i c6fjǓ=kw+3 +mFW넜*:WY, YrVr{Lt^W2)J+빰s џ"_=Pyis|k^5QQq+烉_TՅϜT f\ϫ^qfy&3ū6L7dU:ū6L;d144 ڡEC#Јr44A x*GC#Јr44A xEp0/FRg52-gZ>,bZn_ Ɵ?@]ɝGȔ`I ix+ ^t^rp&9^ P0H^J ^ ;HF.zJ{ Y6~eH P$Q`dΌG#e&t"\i<xXV"/zO"e9΄#lm_u}6t~IxQaH-RcCї#==?,]ugM3j=7(6T}ڨ3Nļ!H-E"G*a6ɶw}6q 1'~)'a;v V.s jsZIX'Ek_]}Ƚ$?JFJ zv.72A"_ݴ eG8jy )екWh'0@2ck\Dr!=9#[E/@L뫶) &뎢w)^)2^Zg5 IMEGWRrѺӃXp-]ՔvP[3(wM X`=16$' $?$Rc1P§iF1&;'P(BM$Y@o~}H"XG{[@I"땇Pm4+$1>gUݘU:U>2҄^bTKoY]4zżfJ),P!VR%FMfn:AcAEvl/!/D(aڛV˯Se-]IRl yp>_~ʥVZr9FX1BǶTաL_}o&o?!vp뛇Rch<ޣ:FMȇB搌yhz:˻6UȪ$yZ6d {hy~PB`9\UYWd- <"ut+98 ~[AXATF^0$yBulE+D@YĜWEkgaG,`C<7,y_*#9\ў^@Y_@%mˡ[clfKtz?T+'  g%"s")R`A >~ɚ}דlH~>)m:D{W;Ajw@$mIM$hG#OVX7tnQImn"^C*y_c3dF|;PZI6@}r< ڽU5]j݋&zx>yA1&Hب.y$7spC_([o{xjDsR ]);N?В/v }FSXRik5 a`..mL¸eB _=E gVڕ@)S-R9 _ME!{_:HRKM[P1E:t5?|1BsWD=,U0|H ,'aI }7[ʹ+D~L y`^~CZ{AO5tH5eȲG:wPM"-pTLa>z/_ΝyRM}A LB[ۇ}>0^H>n;}:>!ed?3Ir iyۇ},0s3 ³`_~}:] ~ҷg7FG 1} C mf 3; A:c~/߯)H-uUWeNsl|PL/-+)Iuu-Rǔ)g=MZr ;K#e+fWI aH82u.?_hHcNҒ%?RY(NFժ(^|-*3FpBi%:oeCv ET< l +9XE.4N gI'C-‡>|.eLѺ.6PN+.pN~wuنš%bPFj?m`WI#il0F;>s4nY$7lp%)F6$!&$!MKdi )A! b[ ے%˲F˭gόAdߏ\=;s|3)ޖw8Ef Ueڬu'G*q=}L%F˜P㫛sPVYY@_P$9t@ vqJ_25OXJ嘤Cv%痋HsjGGr3{p[wNE e;=XojӪWc0oU%#jDeT Hlzdbw|eMhɂN`ZO{csUC6SQ2Ɔ .N$gaz |}yҞl*+咙CKjuʀb:IWӇLa ߣH6S)O\d-e9Sip<5Up lrˎAKS0yB +swy%[YODpaK2g)ػm}Oӥlr1_EĻMk~nT}`Uz4eq#b|^6}d#6^Ϳhp/#ݦ* >_zOj29uؾow͚%J?y~ sNvՐE\t oe&=@}/Wܸ%j\Ki1^YZyfHfݣE "ɯ;~󣺖[F_\g|b5#!Si65ᣳi{Hs12gtm(R9S#;&STj` Imx1BeNٰyb`pa RT0|k`9`dA*c` IBe`gzˆ$ |`6|te>>l5đy8DFy+qe=֏WYkV)mV5 fwV@5L߳#HSrC\\իmkm[j?#tmZV,'¶+l!MfW-M:7jߖ5֏9 Ѵ#K6F wMv]&"-gV8{iShٚuō }l/a}_hcQ{|.+o-|HT#<>e4a5HέϢ=seǛҽltuds4IߓfozBE 6>.uPpmMæ(3ωh@z_K~n]Pz;KK?11WzPir_ aBY:z:6:6Z~~^_|0c6+jk3+lDL_#T/wgH/)z_u miIcB14\]>j|֥TϒaO4O/>|mVIн <3O\e.%/M9b~OK~Đ4"T-7a+0?X,k?DzI#u ~9( X:R22$O[*_E}jҕu{{!K,Mo-'mEy@=Z͹dUW6nk%_~/pPNv ՋX| HKq R E~Z=ZrNI}ʹgVQ3yk9Wmn;tmmDW4C.1엋ZYj-sZf~㞕cnLuN"Or'JT!IDžթ.~"it%3TX6:+<DD$эI :4&6'aݞ&i|C'#EL{g]%#rQ6:ȓ6L~XQdaKGLt6k/adؚ^K,BbF[K-wLF1[1!#|U y$y*$ȍ-u0fI7rsV VD=+Go*n͒rr$O9#mSXTnh`m2~9Onp *\]K*c FZt[Ih~ bOBߜsm|p/NE9W}vgķv7=! jZoYǜ+zOB{'tˍo{ Q;9d ~Q02'1zFSQc/)5Z30l0N-`dA*c` F9`dA*c` F2}!.~]>$K=={ٗG 7.Ǻ΁4漢V7^^T uc]Q i>?eN𾂩mK= I_pUؿq[,mum⧻9B~?=NGt-g~[ߣ>kl'ު8wnyo7_}l#s<'Q2G$z8x\Kr!Ђ]ǡ`$Bq:8p&>ErA};0H_H gA]:c:= [9(*I:W'ZBbwA׸]Ari6IU5NI5ψ2 ⳰$3zWc9 &..╤ȩ{v%ɹ2mcCeɯP5 -/'-44߯uo\Q;>,Ms66^aw^'Isf@j؛,&J4mnD҅ i/> j[i9⛈!܋JoN?AUt/P 7W=/h81Q &/&%pIOn "s6~D@2i;|%_fe8 jB\ ڌk$$sHЛgRX^H:$/=qtx;An}@n*2n  A?mC=@IZBC@ C@աF>@@>Jskņo|$,mDdhF`b (%P[h_;({Y#㗓 Pt 4xHZ>П!/8㓬lElښ,Ę2fpm~sASJ&%dždc az|gK"-4$ǹ1ƩgbCrԟ໴y%Ğ`SD\D~-g&۫5Ƕga.G,lE#o$^QO"/t?E&1%O`clȥ@+4Fs蛦t.H='KxF$&a餾Y+ϐƿ?߿j4ziOo{-M kTRגbpCI >6¤P nc/7`l30H#s RT02 10H$>nʷC|1T02 10H#s.iW󖳆#1faٚ/Qç"o>)Ȃ6go6 lg7̺G ZP9<@c~[.c̙p4/½&g 薞9=\dQmg~&sj۴;G҄_#twSB7wpo9,R2[0+ ee Ohzhc5C좟Xg=}mK:S/S15ݵO~_5SeBnnYGr&,gUw5sXAwK= EYi-k0c:QC?xs5 2>mmK^.uA!Hі¥CJ4rXh>F\w;, Q;?CzGr5-`ڳ۱{`XΜ﹦2Hm޽?`q%\{.%IDAT];G[?/\4C` LP!jHpDcZ92 SsX_q ׋PʲW>`* +;~4X&~ef?)B˜(Q9yO~E+L7ΝeT_wO6vBn 4O/>|%mlUy'_]k Z Йa܉va85O如Wԕ+|,ܗ_镯[@XcKp ſy6߰yEDpW|g92dCpש} .vdd!ٵ7sV aTB@L ap$(@8]_K_ @vUc:~\ M8 zfz^q .ӉӖ(lMmZUjs _5 *pdf\6!_YozoMFzZ4hhhݺ_ ?+fOcg'Νۨ*}z2n͛#V2T7|9pp)JG+Rݱz |}~޳Ҍ]4OsMeE"\2shIMoVPLG;p^)48pS{twBHW nqC}T&yg!Smy]DxӪJ.ilQ|}O32V`*f,Cjgcw~d摨urT1ILU URlΩ|w8ߚ*Ff8Vk(7*4Y(#+Vx'ޜj *УKFՈa%'9◶[sbb>C<41BSWh嫯bd/ZOՎNzKlܢoYVզNj]dm &{Z>)b^dd=͖8X=5ي=]))0?fZoYǜ+zOB{ONXݓA_\g|b5E{rC[>DK?0w"=b~l2_3Sh4Ф|IU =Lɺp CƔ)e9tHۆ8 DRGwLv% c R!IC 02&4\H̉:;/]U .#њA*c` o > 10H#s R#9s̾1_oٰV{ܾ~4D:i:V _e(~•_z`N,|oS0{>w>gyGzު^m^{oR)#enŷz>5gi_?9<_g{n 6ӿv`nlҹV{쮱~.IF wMv]&"-gV8{iShٚuō }l/a}_hcQ{|.+o-|HT#<>e4a5HέϢ=seǛҽltuds4IKg0+b/\(GA~qۇlko^}9 ([tф~#U Yv>)QfI Eqp+T+ lv a' L&V};G|wU3 $no$1!. .Q5njRgI{v#7"E9\ 9=0oW*j!cä\0o* rtizv" )F(oDӄnxp݃-uR7F~Kvę'Z?mKR\_妑8p˲ 7xm? m߆-0_TEamRiK_+8(O-1Pntu9$}cb1&ķF\I1m@:kMz^sɪ.>y҇Ήo<~jnzk%?-ݯKoVy7` r_K@t/JωMA[ V-[F-o9'Nf3(gBݼ絜[6Jo:y17oϞ{ }k@pRSF,T-Q%'`HWMS0$-l|5^ Kϯ_${ȋh\<\£TCŇB7763g-z_mFaEҷj-hfL9C;;?d/q& M-:|k~kLKge1#tMk;f{VKOtI1)92UYPG IDATxw|.Yenp/ai \ ]H.RHBOpfmʒޥUɲdeɲx㡝|[`Њ++c||}M+rÉH!`5z>Zgɯw-^d.~V浗gx<Lk>L8ܼѫr2.;m-6}M\uuTk_ȱ)غ{UGsb} 3->.jȫ*#/[k8uT]/Ǝ"͎sEg܌~`n p[`&/,Lӆøuav[l\Xޖ~/X`1:H;<?ů=} n شm;?_;ۅlwて/z"jmyS&o>Q惽/~dlKT5FcԌ[yIٲxɟzF㍓m`61ǟtŅWZOMoݜs` $4M.^¯~{\, u7s/x ZuVDDY L?v(F6wὛS+';f ! 2K 3I4zRML6[״p #g}\vӂ'kOaaQ?HIW,av-s9UUn6nn@RB2ȭp_I!؁sV/e.szS?!ݭ=5dđуr2,{l "+J' hF`c 1!XQO?.VԳkI8lUferp&U޵k+EgnopϽ6 Φ٥t|NJZڕED7ͻG7{F瑅mOoV~gU{*;1 {W7clc r7s 39z sv]%Xf˟+:}ď~&'duTV$^Ho)2ɛV5{҄9{Q&M||d#ʏG,?Vj;w~gczxg_9 _7iV1kxw~8us?&?3i1?%.}~>p?뗹!`3-hg]9k"{~2?IS.|/ϏqvxS% w[ wcVEU(h铪*='/73g.v+c7mjN[DDzU_AA~߆HtpR .a%R$:ЀZ"wA3yBKRjXV˸CUF:nZ}7suX-1q"I> L[iY4d#cEjrwA8%7Py!mEͳmgQN^6noD&D}Bf~鹸E؂uI]EW>qL,f7,'^U9Wjmr|rOՓ9n('c:8x$̂\!ȵIuueJ*+(.8+ |s}g?Oq3k^)hNxҦw=uR2iZQ&a G3)G9tbg-aZ$$\feѕ-6x5=K!:_VhnP[ix]?&A!AyZ!ZECB;>Ys%yz2"fݟɺ=>MvN6fpho&_=( rmMݳ}>*,4VK/_Y-7e)v }+r?P_Dzp>@j\?Gq MC]-|Onun |ȌIq69_9_g7$%30Ez'fׁ<<8gxW&Dbm+Xevsr22g/<=MI媥$i.Iv-Ų6m,0|0 ӛmoe}nYTQ#p,w{%Ƭ Llo+VՓUUDI8|g:3 seݟz饗^z]A95m*O +|f}R_|ث}$/"" [< ݧP)94N>zy6Z#;Ø[5ϰRї#/Xb`_[8z<v?ȁ5}.ܚ'Ҳ!Zŏq ᔒW2Ù``LY,U'TV1 :m25X,c lq{,`K7rv>*\J;&vz.>2[ן?NfæR֋$ 7ǿ1|wO=yr7W VΡj ORlw?xMDDzaݍcgHUӘYӦ'Gf]}ZٹiWKEDX>dB|';>BEL^:kNQXȼG7RjC顃 #}[|r4qՏs4~߇-8{ CŢ2=ɷ|y; Ke񕉁MLf K*RK<>^fO'U~I2*qo7̛ELs8mg` g쌹Zn/~>L^Oy (O s&\[ n*uM,XβSB (jAtiTOf`np}gs9}:ßȁ}},\{L෿u54WW9=ΟKiq]}7N8 ""ruYl~D􉡤E¢ 5:*?:rJy,HRWm+b~DEDDDDDDqӉ\Gjni8tKq^hH8t@tYDDDDDD=-""""""{v΃5˘9gj\v6DDDDDug(pg{;WNo{r"""""""}u8^% 3;yaDaq=WP]S[Y.;p>jۗ[f{:lK/v{zt-YEiy.@"B8tu|5m1idv-yAxh(45]DDDDD+j5s&flh0 ;@BC}""""""׃>pn+h5ZWS[K`? q1xbqϛNȋ_ijw~7WK-}yr5I^Oͪ[do8 ~DEZqU""""""8_ CC j˝T8++aNeUulWDDDDDzs]D`o+-NKrgegJUu WcۇW8)wV^x"h6Mp*l6[oeSDDDDDO3fΙgv&z7-==8Zo8_ߪv6DDDDD몫vKY[U*PmF-=cA^sԙ΂Uqݶ8t΂U7_BogADD`"""""""P,"""""""""""""W;{;""CF T_g佈t۽rGyyELy%*ݎ/u4\==f>tM2Cn6_CxnS>O̤|{E ۞~]5sf|uטYwݘMtO)w$ǓOM]W >Y,xVg}_MYRS=_z2Ӌ= KoeXel{%g||  *{8pEm9ec^p-}em4A{V=@M]vͫu|ieO/=9w߷[XsCiV^ߌn[e8~L;C7h,?c;ֳl]wwL vŷ8ZрC\&874`S??jjqt7"%s?pt/Y |<8:Jc.:cȤ1pϼpjG|xa7-aŜش3h2`|OL|WE.'v\J,-j2p#ܒb剉ͯbOŬq)qUsjf6ϥAY0c$yj(Lc4_'5|s' "!2l߸c%0GR#<0-QgʻEōeQo|Fna_cN'ܜY/Vm'~9fUq~1#`K_ݣN,X2kGD) @"1Çl#r:_S$~, {-޺M5 :a@dvw ll/r8rw7;nceޤ$.!Fp&eo/q/uϐ6>'Z 8մB{ l* >Zi6Deu5nꁳoX`LX1f>VAVQҥh|Qƚ||x9\'le˹G;g89v#(h~_[HlF}FVv'/KqpLqmiLs6Nl-LϑwWlSif>W%-KɱSWN`LOꪷ؛XrFQ>v*NeRRpt(j~$u:Cc{ߙ;kS]Dc\Ku=$O_R]JÿrFEܺ|9_| mH59!u5 L' &)),nCMԇn0uMN&7eXa#m2o)hBڻ3 7%X,U8>+DTB,xܤY6Xonw]}?_OΪsNhb=MwdjYc<)dXj{Ǚ/)K:>LG򗕅<9{|ypěwr)oΩCK.k?ׇ//Ň?=*BCQ]xOTDXgU47jbrg!kC0n²<(MMl?Sek#y`RV}vy}GYظbzI_o8ϟˆH}l`q(hʂBӬMEA!u-@/q2mzd@ iy1Sb|C[L3Oi&T`8|aedəit0!O(fϚp}m7,`QYR\D!7):wc{?xgRW>+01m" IY$hn*& d@A^]"bXNم=̓upv85kIW~Ś7D;gJc%C\nKv `mOˢ*(^xn!Lιu}XVF^~re{YoZDqn98z9Vd`ޅ}otaxs^\^7o'eO^?__fqP[WGU  9jN:~L[@ӇgK񻖛zÎO0cԟ7gUȕqd1>w<7M.U*Bgnsw~v oY-ZYMڸk\\ԇu00a% 0a. Q U^ʳNgXmM:օSG+,o;}J,/h8WZj<՝3ioNgc66S#)a_T$F2zL>o0`UЙBN1ey-8cgo0>~6B7(Ϝ1aU^XMn~ Ld1lOMY|WHeW<έ)>O=Nz@ƶ-}+OtXw5$l|wf=U޵~a{.r6Us7v*)؎ 0"IL5=dwy*Favg0s<0(/r54`qs *&|t Hu6>9ꩮ.쾽eeQЪᢈ qX\gԜbY|a-̨ȑƉ}fD}U慱M$gVn&d7wU%,5]kH!fę.*kO@bhEnkjDMb">ٟ1X:-ݟV}5}h]ڰi%}B?!f*9̘fqtEzQI PZV٪/޳W{;YZϞL'FTxQ~:{;۾0#I/1cv2u3/ϣtj޾xjeyre츴 p:+/yhqTNg՝~9x^jr @s eו³<`N= Z~<0 IJ,sxglK8, &tR68?vâ<g/cXWvA _| pmV)6k;DZJ&G`'ք{ NTZ@-u9>T;nN};㢥0opex+M{_5{[~nUofgeEl~lno?Hg) k=xJSTZxຶ-?.4Vږ+M5;)%a8(9y?`:on0=Takr6\se #»>\qYw,;Wi-?Buj3s7rd\V<6ϾBiiWđizAV9筭874`K`]YC޾1n "7xv>f/!\U%d)oA|NIf˜ٌB/Wsj-~qʗyԦ._^4tv<]rϋxrelg?^ٵ󽢲ںzlDbS[WOqinx:1sμNK~92"⒒ l ňNj۹ lPz?{e9iUr?~2,r֑f+y71VĖc]gnqT-/^Oya8b,r<ɀŏ#8.,#=YN@p/?yҫ 2ΆU7_Ol2j0n/bc9zDz,ڼWxz}n{Zyů5YyHXheIhӳ+wVR^yՁ|Mobu4X釚P'Xw0沶q=\i 7? x8:H= z`3dxsrψ LK7D% '+78vg\.rs{8W"}Ϋ>KpZ(.f:,c:\3hxUuu \٥xN*ˆ0 .YEW:((Hƕ>pܩp\Y`QZ\ 6{֟"|^x;.iGhbٷ0e\Y.DDD{{w^Dg??ӛxhN!xMkphl诖rwplg5B/Lnbq|2  l`Z(Dw6'm C|ɡԖ ,o`>8x,U#՜e^+ìq)$G窦im]zƔ\oژBs;}5=ry^qN#*Ń'zӁ JbծxNͮ|O~:ҽqCk1r8 -_d 3G`l7&2khI"0<yBA\^y{xO\n 8a. u*gt""&""""_Wu3 """"""r-S,""""""s/{;"r~ """"'YDDDDDD EDDDDDD:+ 6,/P쉈DmqvE}an`7gd- 3*D6ߋggf{i K֤bƇY[LY34EDDDDDjs]mLw`%\|ʙ0p'E5QazwCS/""""""WC &)~ {O,kgM 3R/:} Ur(ɚ VX $X4T9zʏO٨nJ)_-wgPcPZԌj;6Aq_a4NTVϬAe~j劜EDDDDD>8x׳<,i{fέ M'*;GUpOzx=N ^L7;6` s6#l3M 58yܟ&uakgz%8=eN&'#* tyl=#;f׷u+*lt>8kGMsaw2ʓxkXWHc] NeΘ:6o6VX:\^m'e4e5 LDDDDDwQ]!Ce^}瓴E 8{yF8O'ePZ`1^^ȪR7&dI5. z^5O8zXc/ ֤-h~Qџ0b9T02`P^m")F l֌k4_""""ҡ>,"}A`fH},ټ(W<'AԸ9LtmO9̎u仮<]Vy~;77.{?_]Ȥ1-^#"""r)p8t)1kyt-1|F }[0  kKO6MÃ粧V|HO΃\սQ[f.k+) i$y53t0se0|<55{ɮh|!O}FE 4 ٵj%۲,Fe "ok gk} ,ec}h(?ǾNFc縕͏ݎO|țsƊNzpzIeG/a$}1<m~d2Bgs?ƭs݈̻z U~I̹n"ʹK>;Ƀyy$fUZ=z w+=˫m'>L\}I;+C'uoNf)i #>ܣ"""rM`"c 0Ȍبl?FIxjr!adCc#4J"ux3v#2پ N7x2ٲ47͹}H>6zZXa $`2ūQY:{m $:.PO]y EDD䚢g1VM55#gۡD@}-z{*ʩ0 0EƆ$ ) \h9boB_<>A8nW=2tVVnȇ9o竷*8þֱ9ɵР.""" Ey 8㼁#cRf0iUURO4M %Ī{B'jh}ekqyLZhNH&k^ YhzuDZ$w:+Y7Ptl#@'w#pGDDDM-"=}kzg%.ݗ&/y6<.Ǵy#w s{j'T%1}V 603c`|W(!Fx|lao ڲ|S9 >wnǪ6+gWx<=ϱqJM5[eRY]NtJ;h-.ǧ1餮BȏnlȝZe G-aIT vԕfVk\Cs"rEv6>n dyR݋H_"r]Ka``9y<n=Nu/"""}ju ̂ˈa+M]ۛqvƮ{U[:"""""]"""""""P,"""""""""""""P,"""""""""""""P,""""""=YDrvT|dogADDDQH,"rvvDDDDzE@phYDWc#C]iぺjt@H8t@H8t@H8t@H8t@H콝Y^rԙ6DEGiݲ-eQ]]CQa![gϯnGEGOeU .[g '*:nI-"""""".ߏ \.U55u[P,""""""4M/h>i^PVH8Hgcز-)ؼc.л~Ηlߋ vbcmg?߿m0>>37'OK9CENT8 9ԝ70z"MBDzt2!-ړ7k"o{l>ZU|zDypuz(OQg 7e,#\؟3i-c*8Þ kzBf%O}9eo(f=e}v6}ƈ;-wh3\70u>' &>ԁ"lj,Y V<ƴl30}X,AZru'(ǢQGcl^E )\F3ol?<%h`MK;: w1'6䃽4t9Ͻ͇w= _s$ ;Î",aڄĄrfc=G-6B]0]nF[i 1uqQagbO9Y|6}L(CG@j^ys'}!YD?{wy{{zѾE $60f /11&qdnfI*ܺzܙVsߪN%3IĎ7l'6`0$!!VwKjs?$@VTQϳuOsN,ư=3i:'\KghWo`T/{7KNyazwe D3yzNÅ&-}5N87t6L߳'&g6O?of36R, w&܋>W<6|t9R)X k_ HdֱwqNzuk^浂*;>bdݚx]z5U~b~>'Zn}Jr Xk IDAT榻#ba; 'O~[vxΞgd_z+~O0~t;I3!봑I?q= ^dźQ8$ZBֶ3 KYDDDRmqʘŬ #isܜ)^<>;[8P@ScŻrНłyzn[Ȃ1U^|Mپ⻝^.`Xmh&_u%츏4z]@a,=g9]a0abNL\E8~FM UvĩLk@Q6/!48ey8zL;>[^jkݴfojnbӟ6o{OF(4=GScg*i(;M5$-W\Vo-OR(xp wc>w9E;vSoye?\-"""gy)[Y\Řd2&L2lN״|jfapj]˕ 3{rb߳sCf2&nN?Τ@Ξʕ >.(;687VmA|>cD]S>׻tu=a>4tԻ ۈ%#:o-#΀oE Yfbe)Wʨt'2*{>/%?8V'o9m>c."""KYDFE}aR[BWļe3!U`͓a`uz67Z^DV[yk9 -]m0;a>bO>SUum۾`۶/rWu10rg^7qel!y'1~s}j_}Clgɺ)[>+I65`?DDDDRmG|`/V 4?TƍKL#+Z[:}^ʬN: :&$R~32(2wQ6Ɛ٣uH"3[>_xw^.\8 5F²fylLo-@yS/?-WStp 36m,uIȊұ"Z:{Cj6QaCYDUv__/R`,$95+ײ4SWzqAf<Eּ+U^RabF4Ys ']g 5:^7̔$Ҳ1e .ȹJ͋;8#&E5I7>;W70-6lmܱMUr;GvΉ0!#;W]-E!e'>wOL_s/P裩P894UGq. nL"-k@Q6/!48ey8zLS$c)ڶ.|Mԕ>&ΞB1 \bIR[^37DmKmm?li9Ė}oESϲ8'd>찈3vܮ:476kvngQx00WuDgaLN|X]NMZܞޥ7EZ@ڃ\ t}r1hH!'&= iz*H09i[n{xŠr**)PJMK]Sffs9Z XTzӘQ|W9w3%xDNAu4^ !5 gQf!~V3GsG c"^[ҭ.B^K2ݷz(ee{gpÖaaֱwB(Br_R6.IyLzd5]c7x+|\X uwn@- ]kDDDhv`B|?Rpeunh_4Ɛ٣W¯{/!W份vrjꍙ2srJ{OQayv?9'`h|xyj/̛HfJiY2kh5wǸxbcptER.4[lV?9cINH$=o&f,ケiðQܘ!]dHJI'95Fh\Dh_DFKW ;>5o'5r.}rd*@׶ ^BW+W(>;lR}|E//.%poorYsIsluSq>>ՄqZ.fɕ}\QdI\i"""t-h}xb }2hoi&.1ymZdֆx&' y//#ūX>gi5ٿ %A Y|E?}B|]DDD]b&],ol.K"n?*8|Mne,"""ңݸ&߀- zZBxC{q6%SIptrN>yf q~Df7~Q_KoYVyNN9g xn~IZiL_&c*/9Xq}z٦a+{xǒ`z)=fgY>3Kzwۈ'񕬜_d'5.W鈨};/ ?-zvm5""""(8ȈYjIII|ч縸X֯ ǦM)seo}Jǫ}gj\-\y;)/덠1̜lPe1r?į4t:1hWo`T/{7KNyazweQz$~Yvm9)֭y Z<̇ukw9Om`t{=~brfm6niP~8"2bm_#..>?g_cE0_{ݞ001gR*L9q0,svͪ#8@7fl!ώl5XGtg`ބGټZS8ӘBn Qi!ηf2q|Ϙ]mrM&JqgO!J""""2TqG}~klݺk6 8[;r&7KǙAr}~1%w3<S5`=-5 5}樭FjjZYAQϊ?=nv[XFGcЎCNL2{ﭧ#n_DDDDJYDFT7Fakoβ\6_dfA,0+Uݼv8T],b?=}rq?YǮa{#h?i)RHi6ݳEd]ϫVb箝a\Io>213vWll!8cc;Wl2n\ܷ{$z (w]Ĵf*{F$"""r5"rOwy˖!f#*lrLqrd< y鱱\,fk;J9~+ֲ4?\ O]%Н%<@SxaD2SHǔ+X g/|1.9([DDDDngorq[(BZ TsuR;_wwԽe/+bMZ8їP#-cł3Jc aڤ^^?\JbJDDDdPV@{K3qɣV^DDDDdgY- CYDDDDDD$ g0EDDDDDDPp CYDDDDDD$ g0EDDDDDDPp CYDDDDDD$ g0EDDDDDDPp CYDDDDDD$ g0EDDDDDDPp CYDQZS~L^ ;xS~rL2:"""2|)||{N,Fgx7pYюIceŝ Kul{7՘xȃHYD(f! IYVw+aK]jNM- LSA.-"""2Eb[p23سCY,MkgvK46'߾b>ϳu5|pKTZ̼|fN6(2mW[:V47nߥk'wӼ~=ƻﲿތ?= g?ɬcּk-tUw|NYɺ5xXܧ6a_Mn?19y|6z{K7O-ն"3G'凷ˑJgXj_&n/7pFTAҜe>oK5+~D0^3yX>72r^YWQlh EL}3?^_/k"P~/nYNWLӳM[)E̜DToGlQT9`fvk˂3gQ{ hjxVX0oBڣl_BMթCiL!7p͇˨4Pq[38>HtRm+G]4+g}L=$c(b13q}45Tqb2=ַ.[ 6's\&ڱˈM$Jmy V|:ʋP'Km`EDD䡦gy~EYƢ tvq&`tPzgAAL ŝLy$@ǔu *-nOrL2lNieyvF%>+fsXrǥefVg(>DVEkm-M}΃ZɊ7Z-^D<󿦰JJ.Rҳ>|G4`"""QpJkozβ\6_dfA,0+UeO&} Q0n0;wR&9&5/[ih"h+䕟o]Ӡ8ro#//>%e)ǤGVK8=v^PFDDDT[D~+ 0)6qm9w%0x>i{A#qpoH02(rs &L lAŒ4ܸHFZQCYu$b997v7xu\su̞EDD@DbD%MNٙI7L7/5q!/=6}n:R`,$95+ײ4SW"\}:K8x¼d$5)WvA^;c\r?Y@fR"OlܧJ`lYY,&I'9!i䧚4zg ņ_DDDjZ-"[[:{ CHb.cQ}Bܤj0]W?cEIk}?w;bjo佶eX&ZiB!|ǴI=(Ş[OYW ;>5o'5r.}rq]39Q}‡Eݍ=w!8V7g?SMXθqQ;׎IJrJfGhϲ<am'""""u,"b-9H """""""a(8,"""]n7 IDAT"""H """""""a(8,""""""H """""""a8F""C5phwADDDDFҲQiWYDHUWkG """"rT[DDDDDD$ g0EDDDDDD5""""""2bO*x#ؓ;,";KHLϏVF`ϡjyG'po7_@gI}ah='~*Kx_ -l˙߿a'l 㘻pǐg'ĵ9WZ~w:w]&ZX{,|/vTgW?Z~)"vaavw8y Hb1qwYcpwۏ3K ЗAlSGeYNxb',`ż|dgoPt=,"#|m13Yi^u|;5ɏ3y3-fsíoE8);i}7 GL ;,FAڛL~6|6b&ej[Kۗ0u6b?yK1ka>1u} pcj;DDDn6y(ңAYD>46ziIl('knv{ϭaaw0U<}[AnhnhuKZpJ:8/[Δ@aUMmM׿ɪӨmOkr&f6+d7>>GfAIx+w|>{ʞ3'm/1j])Bbak1Cֲ,[ȃHYDr@lv_KӋ?i#|L&:;|+gm8sON%?O=ݗhA~UǬ%;0k uݬ-1֭J|<˼xnA}fݱL$w,|٦O6>VN^ylx׎юKt~d)ϲ+ck$uE*ބ}DDDvqO\S_3ʦieb͒ڢ;n m`If 3҉ilySu'9xy.#} %Á`D8p}n׋m ɢ`=P۽'8xh<4 z^xܷH ),_3-&o`K6;V"""!$,"aj;tVrge>79cAyG>co,`I~͇:qa+FBti Rx00pF9{jrv)3?hNg\Op=K$ۧIGBH(qڹw/S^[˯9xF?Q)㘹p lsϲh6QhsExխ=SCs0X>Ÿ#55(L`#K W흷{G$o:ir xgpiζ~Es*7DR7lD$9~*M!0yG@[#X(2Rbٓ6 ldevcv6n9˃CQpafs1G -`WX ȵcaiZånb8o>;>Ԣ}m1 ׸w6ڤ,+E*gm@pw=q^{[:ǷS>Єj[7/neFCTt]uݞ f}}'"9'3koi&.1yiZ;\_n9mrpe5,""""""#.o *"""""""q/8,""""""HYDHycsG """"5,";>;"""""""a(8,""""""H """""""a(8,""""""H """""""a(8,""""""H """""""a(84,bcb1)˂ջܳvED䡗iZWVwo"""2 Rʤ{֪jC/.6:WBϢ{ڪ< ,""0W """""""aglsLjM2فUuxhNDDDFfEk,3 /QZS~L^#S|QqIf3Fo1?sAIN*QdL*`Bj,Q6,MW(#t L&/3(L%7(:Y3 ߃9~Z]xkbu rs0\00Qp݆{۳'B>O_|FٍOT1xyϺVr7Y_@-+bHc,56W'v3n,lmKfZznŬj߇˶*ASBsǑJV/`Lbtw'ǜ3YTEk7?"SC7sƧ.)@eOB|vG'~Ky}q-6 X7R䵓;i^Xw_9 ݲ>b>ߧHt/}pl{3_7oFʜl@Ĵ#Ӎ&˼w~{F;9rO,hTiK ηyڍH%Oɸ3?ԘڸrߋB3?h}tΏsL{y&־OM3l0FVv'凷ˑJgXj_F }EK]m`PwTVn!c, ZRg `uneYXE:w;$ajB='K<gSH|r!ӲUsrg.h=.Iƒ@;J*\$Մ#5Zko)v i'>!E6 S{n ;))[C^ `D2a8[:\(Ѕ񓙘@l& z;nFt &';)Iӵ*.ߘ]_#q44H'jrmAltf<9Ue0q#33`[H49Ĺ{|s%kv3iU`6>[M8)F*M&Xد\)Ǔ9\f} kc{1 ;f;vTI|pfkn}Ʋ"|Y!ACw*ڻD[zǦ>_?ehz<ə)kSV )$⹞fg1W10jދqhl\BG΁ܴk+yg E0_{{zrb9/^'}: X^L;glf$qܗά9ci.Iu`jf ꦭ͏31;`fapOPieyvFegdƦ>װtvq&ipjrbT`LZܞ3- oop7xJ_貑;.;!.}2 (t(ɤp:3sੲ僞Kw8[!^8p _&": sVSQUIɅRjZΞʕ >.(;68x tf vPo}7=7qF%]+_b쌰np"v;f[e4%3- W{r:i/…nOm;pg%\>ndE5R^o]Te37= NjշFo]觡IiWl[Jށҍ}3L ҷ[K#T' ؒI3\ _;wdc빹p O&=hZ|Zo7n;  Y`csIbbϿ94- g 'v} hV@h);+ bO͔6ng0F޾L\ L73OIT}ӉNNIO^v2qN,mᏛ3(Gg'/ ͎ .Q8{~lg_[vqng|z Q SxJz}q[;wbcWcwl}Zk)τ錋htQUUK?Nr~={>K4Wr~>ZšC!77㵵:tk8>[ gyXݭB$pUxQ$O%ohbZ!B s?6TƍK㚛B}0P09; kKqO$s;~VY M2G<}4r{@\q ~/o"jB:Fyרq'j.qQW,>WV4ܸxb#-(#H E7Ttm`~r_7}tiwq\g=3 r&A0Kb)&hKdIlNZowCݺUU{ږe+ۦ(1g9`s<3D `TJtCSJ^U|Le1p]?sN|}kҺ =b)IT_k Zrx;4q/{:.Ť4cvdfl# Ddt]4ڛ1ݴic '-=ʘ=/[O &wK ,ctR&399kfMz//>Zۣp6SK\z! ߜg)͎݌ 1>a` '1Ҥa,6PlX>F$eۂc{kb`` Ca,mϲ~-4ݜ0_n'&MT1z0uFڷa' `xD3f- 8썷q򼮧!1#6ϞFۂK'N3:rNBC|1/=lہ3M߱ز:~'/"FȬL+Ww^w-!]Ԗrzkư#&*'0cvQWD䔹,Nw\K^hMRrԴLN{*rCi>%-\iDrJ~VptXqlj8bT6`b89]McqIg%Δ{F||BmsGCmj!Fj2sf`1t˸ډVMqSٳvJˍzu;eW aJ 2ᢷڲb: ]7ZHΚ$_貗q|##SiaǴmvJZF%x]7*Bf2?Z{n>:r[5_ڍrJ tR|"܌9k3MͶrlnSWgw)Ʋr"ru68)!,|, {F|"ΕQz⠿9CPD-`$t8>>x\6?|߿yy z-❷xrm \~v.;= 7NMFisSp\ͥӜpwmbh^Ѹxg {.+"2!Q_ M]c YYri*Uٵ$Fv2!{{ĬP>5 >c>.Y`& >P`%Ĩd痧G-lǞmJHl4~uwVz˩j Eȸ})7Mrh\(*EdB=/`IdO#-Ʊd_/O婸/|EB m'I%կ}wDžOܾ.ʛEǜv)| dvcC S#6Ѷ˽qyh77fc%"""{=U[O@6))c-[y`&tvuRUS{OU,""^; 4U[DDDDDD"""""""p@*EDDDDDDXDDD䡣 dnk dx?s6iMx颱86LL$f AjxXWQx;׽A/)gä{*Ed b?eqZfdZ,.=WivYd`2(ao`} )f C}pTq^vs`=ⱌj]a ?eq>Nd~R>TivyZ4yn{֡-+א3;N͑W1Ǒ=9?gMG\ckm`!lJșMJd 6g'͵9v.4ڶäI. {YD&0 as6~3r>Ͼ |x~ɧ?#Fy~Fzm=enVo/,YHm:>>L_BxBb~if 6z淇kpxikiv|v+ ֭ošq8}=ξ/udP~]h+<(Kg;Hyy9QŴX 9U4'^1Esr>z ᠦqo7y~Pp}fǡz뚮alXLֱXq [w xÛOėIAb)Z+9K8;)y=yY6ξby` Tmyp@GaxV'$nw;o߻sF49`@:$&Y_ϭGqx4_ԡ|쩎${F>DDD&O\b"6t!"1-͝$D`?k9QG܊yrBIdUlZś+jWd2l^Csz϶˛~Ftsb'64-8fҨZN)0}9/,1٬Y>HB'2pfL'm{y-FmJDDDHB':.Z;,h<\O^R|Z wWjS>h)%XSMZ$7p1/(`ёQA{7w +>\ǻ9\3ph94/^ q:Bm8))iH}O3 !$,I&5UtuvXr~SImCW[53TT0DDDђR3nKSm=,erW ^ve5U[DDDDDDӶ' MnaT8xaU߄EDDDFSEDDDDDDG9xc/)5cj+ь gL{)%˘QZATr"YJj/_R0Pӿ-+א3;N͑WgXOYl ,ˈ!vޕ!QLX OeаC>̭ɚ%nӰ,K{dUo0V̌#Dљo&gEjh@άxMOeev|.a9 RYćhꉽ㗻JpǦ`ajyi3dK]ˏ6X\w/W{Jqb!vwy!,EGoľlyക<>O؞ƴ~ʶmjo9r2G2vS}i&;g5Kl ֳpHrg[ LwSIs҈dVCC;[EtIesRD#l?\BcczJ*/yql-4,ΊWT8દISG+A$'Q} sY5%)I>ԟ+ikiv|v+ ֭ošg,$a6~yN\yV"fou~._]k86g o_1#+P]·2n剔rv|9Z|I~t-3}Hf+k\n'uzz\gl$|g4pwV߃\66n[=oLlL^^Z˙⋲Vhc]Xp/4g?v_#m8+AS'tY;82&bҦWv Ǎ͜KmU3fT?ÅMɩoFU{j1M`|C5@f.!x3*ux]%:m[639}.7Z[Csͯ''k! L'k_щ/)+ia3sـ*c$X^"0a2]8''e) &qrJ:]>b>sϬdSЊ )`:k;?=Msx깁ˏXTžm;GₕJ3?)u*r͘ڈ_-=#{?拆^cff {eۥVLl/6[88sva "1ډH,g""J eEa46 z)/ČO&KbJ6jK+$%m#26JJdHv >pAM#1,<>CW1v]GOY~?,[ƥ?k-f|8 fwg Y\ٻSvZݽmRdQ?xJGgr`rw|ZZv'[=dɢpnc2ꛚ+BZF{y;U2dAL̹ifzƜ^#J gW^m8qdٳIr+kjs{SԘ>Z 9WG۩vbo1 ɱkzh'8r[E\ mV:k\,]/9O?o{Wb>0 |HHYʂ&lo}~wVp?1]rךi%.pk{1w/p֖.A]]{l4U[D&W eDN0J*͔w09m>Ԗ޼jA[$&Y_? 3?eƷ4r<4:]LGk,cFjALe,F-eEFz؈W.{5(RSGXƅ\X8 \8_A,;\l5IK3k 3n;4>:MIqsL>5m8M0"ti2mLr55 '!$@!?KżiL#]nMb!6/r*.啸37){y%7 G돇i+Sc+3[()i53;I_iф1c&FrL? c;eϝ9YΤ8֬bvuN9b IK̋[x5#n<9 a!D$2u*6-PFVu %2!ySAKs' )$v٤y '-d\_t?O*Z¸S7"vqQD=E4\̥ f{#0R~,\1A֖NcI $7ܴۘ]NҖєHBBc|FqBEߘ{ȯ%J"˟Ks]:(/9 _g/eިw{ M^^I)*h*3bo:ј8_:BMW'XYhlf9½]\1;m'%ؘwPyi_ɪEOnc7}tPqZͪ[Qv;c>.N'wK/t)F-ow;9wi酜6o`$\ßZ)imȷ^<8Vrɉھb;ظ9~0 IDAThM9͵ZpQq7,WϨsTc][m|aXxӎ=Gg!dL)&`]=.ԝ5l|GF)LJyy AYyK߾[IYJJkoM|qԟ?7g*`7>eϪYXlg1>nʾ٘ݝ4Ld9GKY6 zi+ʩ[uOc2-'pt4Qu0xQ7MsSa^fpwcUOIm}Kkz׼yݓƽwik>>ԫnb m+<)W~G{1=,,,wH}0㛐H~&""_b^_OۻsY]q ߏy?.Dr,13XJaE#PRI;x,3;Kuw1\kzps1Vx$?F﫸w̝b܈6+貆2/o4BʜGw3y GaiT8Ȅb &uDkZ}oiMV] l6ݻq(8}4R[8~w'{XHytb.Ch~&"طCSE!4U[F-"""ӽjxYDDDDDD"""""""`""29~ """ gy!""""Ci*EDDDDDD{{l I䢕>1ZFBl?W;{8㹊M*E…QRV6Kc޴.}x2# ]1'k73+@ggo12CA$/aՂ puةO]lXDpjnA,x,nQlL%E=|*.+–L ^\5$լ^A|*Γtnۃc ,ˈ!viZ7Û\_9I 줹:>΅A <>zlnh5~;cyyZ&q_䫯2M,+xU>d>}zjn$`]4P=hgnōZsn4__Gyi-u((eg80KRZ)xK%.<WfMwl^~Oy=`bGo~4&,V_y˩:'fur#TҸ2+ݗ')ZwNWYdr:gVT)Gupq%""2p bȂQU]C@@+,0 9BwH0B5'ԛ`x(_id(˟/xI_uq~>&}7f3Y(=b־B[m>HWos_OWM_;wugr<%]2X m_pClϳr8. Y fLb+Է:6Z;~0Ya꿒I/g'p>e=뇿હH"ݔ6JnVWG10 tz~@coӛ*""rp חYӧ?KυKTVU-#"՜pV{WL#yKx4QNW (z+)v,Yf(KD1zΕk &Hyy9QŴmMԺ'+-,q߱hk" ncm= 0okz 39U4'^1Esr>ޏp֔u߯ zi)9';nե]9Uɼ-=Q;x]Gye>0+1^x"j p+̹gy,I^7xӛj,L"""GL(]]]\ǜYlٸ@:wn-Y"7oܺe`8 :q ooK1>f00\8T?ʺcm{f`.;g>O LM!u^YaOf$ʣEm.'=-4w/].&&ښOT4: ( 8:&C'vTg<3ƒX;ݦYD&3ϓ805̖ܬJ×~fgCuw&& Ł%H Tӛer٫u, 5%aW]u949+p鬪t3.okq,"#=tm{y7#KsUN_1fouu[Ĕc &[;ܭyYY2zGDDd,~#"r9N?Ass˘Kff@9/S[Wwb5;D+ySyNfXBR3}zP]=Honv"D&3o~*!ЙɋҴhB㘱zKj9yt W4wBbhغqDq+S !"& ViQ>ܴ=ʃo&?`x ٬Y>HB'2pvwɡNΥ{f֗6DEM/="|vqp\8 /Ǖ+"2!8ε67=\V5د]&g!sSRVs\NqZ2\ǛerPqZͪ[Qv;@7?d[V>}Vh)ws:(;-9ӥ<ﴯdբxm}tXSJޱ1\ :oZ&?oÁƚ#$І ə&7x?£caM gGy')jO/%Gs=IzX+=^[DD&cU1% L5}55tvw0X.]z#ꊳLUU$ayp8(SEDDDDQ,"Bss˸e]z8*EDDDDDDƮ%m~A'xǨm:^{.N2G5MN?ЃwO:`$/%s78Xwˤ-[̼"l8;T_Sȸ?l_jkn~cI^_ljyϥg@<#?ټWOޕ\EDD5"PYɁڇݰX^dx/\yKäĹY$/>_"xjqoOy vfm䅅J&Φ1P0²xMur(wy|67 NE+}cεl/vpt]co{sT8Cх  l E3Ƽi=\(eF8bɼO4sn gV<&'b nǛe6u%O&%2t;ME0crf'o^[}9RWQ,Wzu8_ZfSϟşؾ>Y<CP>ӴDV- !WduL"լ^A|*Γ]}YGFJ柰%GW ~&W<7l =67v P_<-vWp&*2@v >XF =]$`]4P=hnčZsn4__Gyi-u((eg80KRZ)xK%.<?fMwx~%^1O/axDDDg ""<_'hn~ ~i (rjnW[̹:fgh:s*KH 0_b`^ԀOo͚I$4x)I whoNWmbiZ4qXQGO5۬Hdh( ̛Jt]ȉ:V<͓BDL"SbӢ}pxW:hi$ ! 0˛~'v.5\GN&&*oᆪ|Oꪮ΁ !@H!Pd#ٖ4#ϳGsfyͽ5{3y3IʒV5Q Mlcuyt):tU]:C,zTG;8uj[SLn"L w˄FES?Kg1q8,gii"g7uspd}· ߿=JˁMl3N>jI&]]3r7r`ۼ ؿUL.`νf$\WחJa"ď j-aY?Ur|>nYNR G`oV9̫0uٹ.Q^Ko19q%;7˨gիko`_gA:J^Fe:$*װ껙Mm}""ry0fϙT"Wh V~8uuϾ3uZRUqDDDDb7kY8%qʇpk+JQUEDDDDz,"ֶ~,""""2PZLDDDDDD$g EDDDDDD2Pp@YDDDDDD$g EDDDDDD2Pp@YDDDDDD$g EDDDDDD2Pp@YDDDDDD$g EDDDDDD2Pp@YDDDDDD$g EDDDDDD2Pp@YDDDDDD$g EDDDDDD2Pp@YDDDDDD$g EDDDDDD2Pp@YDDDDDD$g EDDDDDD2Pp@YDDDDDD$g ED)GKVDDD2 9qoLbt{%]  Ұx+GG00i>0M3EDDDE ,u>hOec]DO+&xѧ5ߣB7CDDDDST@?믿֧l%B!(lygVגiY 2|932$I87|̇rs7c,enhW֊})cu͝v&$oK1qN 'f+~sȂ:FLSﱈWiH IDAT2>=pbTnjo>MPiMJ;ߺ """","T4_c W|kw39܄o[3[IMzElL)w-+yeku.`%f/MNS]L9e?7<z9vM_݋c<NbS+([&y/U0=zwȀLDDDDGYDιgv{--Kbyg_{)zV^^({,7ͥ?(޶?׌'mqTǑߖṈ~uy1,k;pK><,$д-rR۫;MYD΋h4ҥKyGudY˜~E:%@spR6~YW5QJ;^Rɡῧq7 _JElyEښRrLMM%Ynn-%K x,"EVE *921j!00Z>3n}zI¾Gs[Cե,"\Vŋ{p0랛./ blaKU0vL.k{Q6ԧf0j`}8vp{\th#@^ O1ln!=aCu6(/;xʪq>!"""r%7""4l).)?%E\8)nfv/8a)ˎ a(=|[#) _\XTQKfͿS)ɡl Ӧ"`D8|(Hĩ2pS {3.{cə|"l(U6>GǤ}vOgoik 8 |^b k]-͸حcĶ}q<3~@`Qb[;Eg3scHT?MgpO<8}koŝftDCe4_cO0\u)QC57hn,o`0Y!-gyx;k>ֶEX<c0g`!zO1'Ir /;\϶h,߽f^JވpTwvu|vDW)LwTRF&h_>kY31{i[垑طW㦊<,5nu|n[Zhz zYUE*'_]'чֈH?QCy sO;FQ3_okn|W?v u}亞e}Z7vl ג<C|Sv_(w"~kg0Ncc7d,M=HH}Ezgqg|9?ѲPhfFWIܡԆ|ϓhT' D?)(͹w;G h|m:.^%2_^M}ubwQ6֓UXoz=5XH;qO%ao]$~lh nM/]VK.G%g-c{mlNm_s–_J'‡e71(5&F0sK iLBMLoݹK2~ jg[q§7t$K xV~)ŎF>?ioH$pfwCYDDDog ;M#DC_H!єXp~H]#pj4uih-flñ[;˜eb&bY}qj7uHO'Ͻ&#. l~¡]"D}?}U7wmKutˡ.u0SYmNgtsrfAq8>g<9ܻ~fu?Dʺ<+ˢEh Y^9g72pm 0k7ϰL{ Bm ۻ!N63z[}Y^UCl;78k4 \fԧAQ0zk+5r3X6o<}b`h]dgv[/>mC}؛8J"^4ϐk>HxSziKZVŋ=_2p\:R˚ޯo67Аư^veQ^'vHS 0ch#"A㉝8vp{\t~ly96>+3XK}jF uEDDDDrf;.}:D,ÙrQ3)`adhNdz *,WQfᓫcᎺ GrV3 >F!_8\CDw!t=kM>~xga`+dذM~dO#o<*l4rF;/?\h ͆G~A ɴ1f=|[#) _\XTQz/^6n1f"f."'I1g:"A$wTFf`)6>~ Ragr rr((ô\ @ #ΎByO4CZ79S$"v7—cˡ#m !Jb%"B&:F~p]#x.0L}=ߤpȎ?G45d v۾G)|Gp_m<]wj!Q[cmxo(|;mP'vT rUG\cXӀ/'g9듞^Gh! ;,Q|=^c5U][ w;0{C^5di`}؋Ȼa&Gw0rfN:܀q &Vx|xfwy6vD \Z;NBmzml`in<–wb{)~#sWq/߹ as]$g^myMuլ}}kζ0WZ>znON4o:v Na"ď j-aVoޣ^|ݲ"An_>GDDDDz>H#_%q3B|?Z +F]5W?({xU֟ј|ԟP`ŏ~ɱQ7`4fgVу%L|i7nOrZw?G!;}b9*_!y_yڛHz2CzZ+? !ScwA.;g?3=W}rkOh?J`SCTvb;cEEbQpx4о49{q8FNv+v_в03W/Kv칓pe-g$𜃅ϱ6x>Jۋ]5h|9dyۭ#|/_uv``{6}+Nmnl:eNfl:{gԶ^CDDDDz͘=gMaW݅ne9Syυn%+j%+;SNDDDD.>p"o6@㘶bÁ?KaQ }*>>@ bzbI&S$I▅il6la4[b q'H_\, 48<},)НM*"z\V}|gW IDOS2MHNJ 4)"""""bH*4&Hc}f#Td2ͧ>,Ϯ-8t:i {=%LL%gyq:\DDDDd]ӳ{>+ѠrP$ɲ t:)戈 8{^~ZÑ^eYQAjױaF" GDcq,"""""0'6"Kycm+ɥOyDX݆}Y>>.mGbhyoiٲ,v rQp+gڔLIP[kY≠:(aEkyj- & VdӬj[r!ga@ ɳBV|=?0@]%/ N"vB`3+*pa͚JfϾ̬`ŪI$x=.<U%""""W{dXI9LQ~5U3&10kc+4DzNd)L| ̮#(9"ebT}΁㙘M'}YY$ΰj[ii5V]^i%_$LRp˟Y7lgǺ*&bs8/gMtΆ'L"r.^n*{7f[U'>9fN䌹Ii~6ذf=S'ubzj>M`ƨ|v*s /eW$ۉonz>\wЩ7r#(;IEٿu-1]%W9G'ٻt3njq̙P̦:\cֲ1ʲړWe؛}N围GfyU:Cq |{8!8ꫧmzORx<3.""""r0Ԏ]pjV Ɲ%6|Ënbkױ%G6QDy|.V5\ cprdaaok 0b̿3M۫8~k P񇼼+*sjY;8(y7_:Ŏ56_>e&t^Ƕw ]4m?*|ɏ{GMdT?6&hs)X%LBᲥ c,Jk:W>_oaugb?J hUm݁iczUUuLnK\lY SΑ BqJYcXRaV\ecO %4̶4i e'8͘ScOk% wZ^=Sr8#V>Nsڃlq3}=Mh8 4noӥ3nP#hAG1y郛汭|4n͵_52[90;ӌ8Ɛ=fjvsPvy~ Ol޲>(O,F/eWDnƔs;GvMPLۓ8|nlS-IDATD"=u[nl.t{l!ˀ%n8=b<0i*\" uYX,po*EM^M`\s8=]f \ 鴉v|ݷʵk3kHx"A AQq6_lVX0,1|jbl8f, Ǎq34.P iZ<ʬf3 Yq`$A6w$V/͗pB )rRsraNd|`7[ھЊ""""rel<;0ꛦr a`3͵,+A77j,l<{>]è~7mpcG1LW>bOd.Æm{3iwnR]!6oorsz-ۏEGYšG䳭|b!|~OUr[/װRYUK)fvm)'h]ܙA> ` Ǻt@o9Eh_oMR ΑhG,A/{ ?Yr:hnG[DDDD䒕n㕷LRƌJ8m0ԱzG#uE]ғxlrF[ 8˓^>c_\waj']6>rFXC[?g_IEkKQa)$nƊ;wR{:_۴7b3m^X[s)|V{?ՀBIQ!T/f0 3})، %qOaL&i fx}Rw1v[v xnZ>y|2zUWu D1r~\NgʸNr~u 3Mr{9]vLF9(v=ӡ>DϮ-8Cs]C#.@Ǎnq͆nqrکkhH\t[d<\v1rxho |4p(ep8yD}U}|gW^UL<7,N A:"6iljDz^\L3z<9:t2&&c ,e$e GǰmƃtϮYp4y{q1lvlgߨW+ወfcej ð+?{I}<П])8AŶ,sO}|nf  EDDDDDD2Pp@YDDDDDD$g EDDDDDD2Pp@YDDDDDD$@H IDATxwx׽j B!zތセ؎q_ܛ$Mq/q;`LwP,BB|^ϳϣ={339s?%K~pb(40,.stro^7S@5GGGO\v }*+òn+HW3MӴϟ쬜f|9''gPj~S:re]fna~`5#G!''g8r!ѵ5]Fn6;_y%K<~,r\.~UURP"""""""W0N5RNz.GwHDDDDDDJc:;`"""""""2 փEDDDDDDZ`y,sEDDDDDDZxg72įx?dȽw7DqLQ^_;SɯrA,ncz/DƸ\1pKy7^@""-\>ŏya`ڜcȤ|)qvzKX_?9?_600LN@B#{3xTn~͎9S̚ej1R8CY efAZ}6e cЄ!J"v_ղϏm.|>ΫOge^ye+4[>Whtmz.+=rai⪩47A-Ik8AnvQ]QLvE1)ٴq_cݑSv2?}., {egs&ۏ1Ɓm8|wq&X, q9ټ9I-{,_fc(tVx<~?0s͙׬g-'?Pܡ EDÿ_<'unلGN敆ᵃ?ԢxG*e>qL!oúP] '[»{Xɰ_O wyw!R 1bڍw5ğ;^g᝭)am}3uŇx@>̿^zE6F~r~{ؚG,OYMa6n-%kỼ054n.=Or[m{]8]aɷxo5߾Cj[ɛio}+hW~ 䩻Ƴ+2*LzfyɟFjOsf1Ǟ_>{{nD|!m;tdz˨EyфiLHb&,3.W Uk^{zzuVDDY L~vNwK'#/q rϗ;gos2.l]ӆKPx",~ʟǙ$ofS/a~ G&Rq;>'r5ɬy+-ɝ'O?͛;O\Vӄ'};kO`awC<6?>+a,7'MX-O([QqQdokM.?v5^nmHBL_S*uo/bUg*u+:]6O^' ?"[{H?rBG"F^#_ɰ~W|1)dWIH {4&d3g zs=6[yi;Fŝ عt3a,p8s/wͼ3,/Pqe^;t3w.+ xOUaLxdne;Du;m$~ vlLww`A\_f[藟w/Gk[7S=O 1XHb ?_R_|kBM\.6[TPVSGOedCscOԣa Ȼ8OqKکš-̸<gUGQ~ EDzʊrfN~5}JfՑ{ׯ*]fzE-""Ϫ-%7o 2iIa0 e`qnI1vX``@5TUUa;e{J˜095,i\"5,7XOf!L V|rGEvr:ugXzQtr܀?'c!'23kϕ<{ti)d7qaX816b9i95dងmd my7}~Z UMa^>|s.> )*²:y7{-0M۶mu.e[Y{-w]I2},ibj[*3ydUS?[xo0-9Cr^zוj4ϛ5S&ٗ;ozo^{:\)l[8 ,ݳNðA,+#ѱ0NRh]\Upg|y;*_&K cy;;oqjX?B 8NVPq46ӗl$"K]XfS'lɖ8Wr?$xߏʢJ*73)|M67n.P'm0gϚ10Loa4f*7]? a|'~o<8Lxp揙ߦHa|ʿ Mdyx\@s}ۯŢǾEm6&}q͗n4Gp55;Nxώ呛L~P v2C0y)vXIj ?g-w0ڞ""r1M?ojvOseE3^usgSTKG~ߍ鿂\^V9kZT $VkVeE?~zZ/N"""CCEDD.UEE<84x^ )1 w'Q6{# X'g($H_s=Ip ITEduxz/bDDD:qAKiO'&""""""rYDDDDDDuYDDDDDD zB8AH49HX EDDDDDDZ"""""""mg2\hU.H"""""""mP,"""""""""""""mP,""""""{wrn/CIDDDDDD/ޱed5.3 بv;nR*""""""=Eg{m{rlWb^z%nO)˲(*)@`""""""p8_aۡ!?۷wJ~qaԿ7M."""""Wm|͘>ӧYWbKp`!U8z}&ߏ(1K^ (>scՕԷ|ڌN "O#>6jjJt '>;+}V}OM]:Rh/R;_︒Ӎǃ=ndΐhl~%r=忉~NBC(.4wr[OȽWA\d2a-+=nUZ3v6|` ۫)j_F8 gN=pueT7ٳ0(0?rs9u5OWm]qz3@6Jpzں:l6JKEU5n1MgS,">~c9/P{A !s?y59̆k9,KeJ229z)Oxo]q 8~)ʡg魯?,m_g;2\pQ΢߂!O.:[ 3n3F'dUÉX'jLBOe޴a 橦4/͟zrKyiߟ Z3eF2bb:vۇ2_y}_HSvFܷF0*rrup"}ǧ_5Kb1{t5IrA P"{'AE[=B $[1R+n(hW%80А`jjkq|>FEPr񡸤N]mv1b0MIx nOfw"ʠC֢sCo7Ծǟ%WGoz|R4Ma6x 'G Vt JZ붺 lXo 6#:8@{5YVc 5 4go?vw%'Y T9>ۻm~ g3gBx* 8֜Hn[+'IC |0?e˱<2SO4$hm?{EamGۢp#< 5]q7M9.(ߐj{sL_&VCٺ~c[?'3?}ǻii\^гp80M񳠠@++teރf:cB srSS/w.dD2ᇘmRmQ*Qsjm942ޏ/c^ |9DX`Ui>0ڏ#fWZ}Jʶyx?ޟ{) $V, @eEmd!vߗw66AQ:̝ !N"=Q (?K95:Q$ׇywsB}2 mčύwމlL%6h$^SO_" B)HN>\XnN|7>?#/Hk6j?>, MKsW+<1i.$tD▾3b0;T@_<ky,pͺˋ&-ߐ ysL_rq?=~1(xi+?->\XWԸ,ʪO??"z6~V^^ѩAs}/7` ? ] {<}][AQV g˩/\I?ƽ6i?<"*a=LoS㏳ѣ;w"C aU~I*yT6YEE{S[,ɫwW&c+y&"K|hnֿɫ3ii0wf:&-_\/JWspp0.W4nwPZTFhlnR>KbͿ,$ :g<f(Ç=&O:&,b’GXNͱ5ŌF#r:xr:GOƁ#abMH_'t6^d67i3$UIaIvoZǎԊ >DDzJ䇦0s[-Xؿ( F("}xSCk"1NaaY)%=~!l+,n^ufdoIn;gh6f^C_>v a/#4t]D LbG 'o (Fgk[!75ڱ $D'Y2oob+qf ™]ET>m4~ uҹD[s#O!;د/S櫬Jk!E\nsmJ$/yسu#f?S'3x{>0s_l67/|.|tx Yf~~͆j8u: ^6k;߯r?.ZV2k - 3ϝMrtM j x;Uɉ')$XuefdIAeg|v0~vlgТkG/ܗ>|*]d}61^Yi<䧥I΄P ZjksF0a!mzX-_ 0.s;|+.>{oǰ䉑x a)3f;s12S0;X*G_=x&87icao1/=6B3)ì5^XA/?%&3nH져i+srܟqK${c~_r#uFNg':9ʏX|ݽ|oz-Eܾ#gt\dnxW+f1g<j)Is0É={7qd4n$F^㨶ەӘ>z stgCŌѳ ( Ч| Z ֮8ȢYw5TVsz.L\I6g<&EUior80/͹ik9XT?ϴ\,K;wob}FS++Ō+co R ?|4W\CYL19]Tח|kV;fϢ,ߓQcY4%Ik.?6n[B>D@9̨Qlqd&e#H VKuOZȻtzOa7JH!ooi%%׸[5,?`;|Nvsa^줋.2ֽs3NUQHƁ5mGĩDƎɨY;ں]6ܧ|xG-E@{Ӆ.h,w!]VCޥ;K+FxX(Z q8vA[zQPXxQA3mC};~&?$j[ey/9^GdL@ GVn1Zs->I­'n*`Kϲ*](M ƨ9[cK׼<҅*J |ߞ*/~K^WO0d`R32"]q=q!ɡƻ$NzGGs..զY䯫zӒ9W ?-f_V_]p`!b&! z>ҲNYW<_;ZV^uQA31pi21U.j9ͮEҋAsf99}GY.'<`0aCf&> M \9Y{LZbyҳn EzVVJgq؜d@E"h>U?3fQUVVҥZ,sZx0X\GI'S/gM޽dMKo,7g"TrċupP W.`[pfꕔRRz񎎫+lg5'NBVrQW_M 2>*HQ9PǾudg4:#M7(9}gHc L_TTrWy`3F''ٽqS)ڴ0U w ]sY^ܙ&O gW la#~^}s ҃t=ҜqՕxHWi9?gΨ\zWaY0}x>Ewbl9:f !/ OS,""""""[n>Ӆk0̈A*s9iS;Jry/>T[Djt)pi}_ "@.HϤg6(piC<3v;1Sԧ?@<7*#w.|A!6;cb A5ʷ|y掾Z|/""""""\ M'1z9ClIa־m}:1l>Fn._,p'2m&䋈Уc3vŇNyy]L+)}J3pgl;/OD^jؾf%/mȠ09oh*2+)_Rϳ>j.ĠV!hFyVc$7&4k*rlzT>bWƿ*l#pnͮx3?΀E_c x_Tv {xY~F𽣰ǏvP $.!f_y$fߪy3*{c]7IWquL&Lb[7s8(hzT l]SEuaVhґ'n|Uui+K,;؁oAa@a2M' Fvn$} +ѿ5k8iur /̣j..""""""rzT ʊd0~ň%|< V7:K ̝{0sٱ <wNf/q'1,&'kbX5 4շ0;a8t8ࢷx3~d%pgoݜ"V>Mϸwae^;BrG"~m}̯8EzI uA|׷`7Ϲ}\8c)i#9nl?Q# &ѫ'/TW$ㆅXVybrO/fBfXsYk DDDDDD;炃۩74=;"ߟAz'}3J1W>5n\fJ>>R7IoY7%, gPFGzW< ɪŨaXO_,7.@^""""""rzT P}3'6.3 &q,˟:6 ]mG׭eWOǏsu~^6/ͼLe Oo,‚|W^[ϳk)-+d-l;IDDDDDD:4tfzj|Ro|Pey/#TvSDL b86gdߟww!:"IAN75u6"XyuwD8ZN.Hc{EYDDDDczԬ""- Hb޽'%Ŵ>=#|dWhDDDDM=nr0i 2sŇY> [[sG8f?{)|9u9K/~̓ Z_ `GO: =]^DDD6 EK Ză`缽;G0Y 1l/k!Gf㓆@ތ7?⸧ 'S?@)+^@BK "ȉY[Lu|8ɔ(_s,iky,ƞW׽2nȶYi{ Spb"a>xem=_^DDD,"]ƌL")#[H޾ySlcʭwrϢJ*ϏWxj 8F,}Lax2ٲn?5F2l*7i{vR89^1D;+=ailp27x*ظ$w{3Ry6Z#ARqcQwJx.GݻZ{uh--ȘB| <5dd*p+zEXUTћ@?Z@kK)i2SZBH5M\76l^F5֞[.+ PYYɹN^;Cg0v đ^ 3ujաw'>̚p3_)W)v[ņ2uP,"]ȓ̩k2,Y-VE9>Bd̠ +;'t*+tFT/`M1m:ljhOH7&뼟R PL2gcBsmIʈ l=‚ \~T** $,աۗʴaϽl&V;uh^W 5-,"""W8H*? /1Sgp[.!n6d˦w?glX[>ہy<9l|o7^ϣ 0Љ\+I\|F5%N(|=:ʨ'5-Oݺ/טR>8\$|}drET>ۮC;mo8z1dBF`MMQ{>m+1}֜1PD.IeY A!]+22ԽyYEDDPm*$/gT?N.NCE``ނa:9 EDDPmjtjAH8AH8AHgJ"H'(.Hg6YD*RYVEA!gJۏ]ҏ\ EDDjt-"""""""""""""mP,"""""""""""""mP,"""""""""""""mP,"""""""""""""mP,"""""""""""""mwwDD2wADDDZ\natʺ|oA_^|tuDDDhr0"6m4ڗ}'m̤ܯp<'7Qٍ%+Խ.2N)"""Ҍ.H @& 椬m咞Ep(Kgô9/"8Hs crho9ΤQ ufOh4IlY=2^عs6ndu =<w1)rlx :/l-j~jO˒i1\7;i.s 6ā4cWَ ZY<ʔl6G2up4jU( Ƀ רa6\ˁ-c-c37BNl1څGcѮ:\̈d N?eNI|W98w~|Gd*6}t^ c/T㝽OO/DGEw--lepCh.>OgțKG7Ƶϯ'ވNfZk8IϳnWvʍc[tuAܼez-:ð'm`+y 뾻k*Qk+83׀l;XDMř#K 3pmGKRvR:)rH}ZTUicފ9?oܥ2E͞NL6PTǁ EܿC5x޸Db, bsO.W_}' q9A[oxPXSp9yhYDF_K1JVj^&WZ1k֛L챉-1=nfQYĊrSI{8}GZu=[R|,)#ƀ>-{̤Տ3.;jbfe%ffV;zq&WY4jiX+dcЂ1>wUç>[/on ɧ>UjFH ~1L=@߽-iɎZ.]ks-1D[-U=7XFQhRWw{9<&m~ؠe"""SpgSru/@"ehveA;^?G2q'0;tTVSYZ&67_DzzX{bϰKE5v{]>=RC^vrMl&QutO.V\&Rm.p1|}%f]-$&Mqm#Znm$Ғ{,%j#nG#_Lo 5r'h<2&U㗩HJ#5[>_|~.]<5V;XXK(*`YgBEMMQKyaTRHJ"oJ/ ۑHdՋH;>;W7232dm>}ӧO@5Que#k{DT^}ʓZET&2gx/xrH0ُcm8)7pZ L_RkAU|{: Rm3AkIjڱ/%+#MjJ8W^R/>gzS([Xo狤㿁F/DtU^rV.~< M\X?/?dyI?-u\=qKnCLVVsr+^fV[9uzm?)8,""""""84q:GNVAYDDDDDDF\KK+ё8#ё]-A_G%""#bfGjjjHOK#&&aHeL˅aH,""L_E%"""YܼysT6lvF&8ݎ XٙGa`6lmg%wlChDDDDDDDBPp AYDDDDDD$gEDDDDDDBPp AYDDDDDD$} I9`ӟcfW|ޔfEDdEDDdEƀ툈uȘq|Ӵ65 ""2";ǻ """ a=]CwIh"VqspCj=,3wqZ bē&VesEDD䞄Gn"&௼FuUpW*Q_[](;_̊c|yv L[րl;XDMř#K 3pmGKRvR:)Hx/`S]t'}L#gPXڛ*؄[Mi9J:⥪MSk6{禷g{f+/,#~{ȬTڎį~6\uONc# %:la;gvUok_gu8ȨOy*Yj1c3Q=өIӵmrrk^j%UK{Z!3< 2"IYܫMSF } cy/ql!"r.Stǂ&^99dTw>VV@řT=l |W[3HS9f틈<̺i{9PSUI0X5 gUf~&3GCk c*_]ˤv\K2{ݷz(eefpaaVw8C(w^|de;m {+q|{@~|֮\ȴeNMqcܾAZv?@Y1k_DDDnw v7G "2&ܵ4ʒF2j92վ'4R y/ebzk ,&gzuĴf \(4T^TN:Β7 fM`﵊ye$\C5m yߌEDD`g񉉷f#HȠJ?3qa5{v$=='`Ddt"x'jxz#/,JjBIiY-Z]/| DfN!+>)7[Z>k ĒEDD$dof@fЌ"4 k_'4q1.M|jt(@-׼_FB{u5(<5lr> 6?Abofut`\!.AMOL=`D%̓%9\52mܹ˘ϲf jmL\U~#өwim"""2(q [kn|CPobǭ<5Dc]6d'.&BpƜM91%:i.U:H7lbՄ񏜨5Ǥ_;#,""rץ=o* imj*j8!"p?D[덓|wm_w0c:V$&ʁ|znLyl0!Iw+'};/ N f {6jH( "2jV^M\\}9**M^u{hFdn[yg]u%\hJ!W0wnJK"hD6s|v~|̝[ZN'Nlew)\ /lڄ1՟NO1^_4n՜Fˏ;>"VqsgcE(_z-3<5sb ͝FXW9L7K((i]_^݂32(ٻC%USgi,^8yl;XDMř#K 3pmGKRvR:sERz󤏩yCi_DDDDJ3"2jZZZٴU6mz;v~zfr5zqE`R|u&'¶hL!P1%JCϞSIy8[cڪ)D&Wn >W-=kp Z02Ȉ'?gqzʈ1a틈P)8ȨxAfB_Vr͉REsUyO&=뷺aaVw8A?I #Rh֏Y[ "2UٽgBs(1!r6Y͗-0p̺Zj 41]sF"YY1tq`zk ,&gzupomtN8ȘhiieC FX,idwzڧ.G'xʡYn-V\٩'f0kzHqq̵_Vd<N%5!,d!59h"#plfEe\?XvcVyS<^ȥ,Mtsb0&NWc _KVF4Քp8T3Pup 5/gD]5 5a}lXk?~0>}Ȁ';&obǭxZ-""""""H """""""!(8,""""""H """""""!(8,""""""H """""""!(8,""""""H """""""!(8,""""""˜g@w4^?Y5!DG9}|g~$Fg5x뷇pY#юI]ymMHuokVx/cyߴ.{8^`+= q8n5fc5Pqvwk/ yyn<$GOvrN|)8CFdn[yg]u%\hJ!W0wnJKXD6s|v~|̝[ZN'Nlew)\ /lڄ1՟NO1^_4n՜Fˏ;>"VqsC"""p=SDb,[ң|Utxs`Ԍm)(s7wa]%r0,v͊c|yv \ˏdvR_WMvx`U7gp.9-SK#\lJe"9<(];8^WOuAv15?8c(`13qJj|Vpj!Jcg0+?3w#=Ⱥ lq*.ګvԅ ׎ bXbMTVnl筦 AX-^4_DDDjq8[[빝e,.A[g\v H+KYlr"(l&oSo4=]y;SIy8[cڪ)D&Wn >W-=kp Z02Ȉ'?gqzʈ1ad1 OϪULtX휏9|8epuY4UUQ駥'qiSC߭Ae;wя:OpM?Z b::g.E '|| 0 r|O._/hil *6~ʋ{}/EOn?7<EDDDd`"""""""!(8,""""""H """""""!(8,""""""H """""""!(8 Kƥ]gy Uܨ.21sRmEDDDDDDBPp A8Ȩ4-g^/:=>gyؙ u(:ž#I5D]*|J0gI|a5h:g~S!/:Gu{ [ ~퉜u~ł%=(;zn^رd-wEgS99g~e=vY/񓵰ןr}57,̎6]e|}obCl7;X>N+=Gy?Qk}- Tj.O_zO5E9}:]-rbV.fBz*%ïPDDDƜ*MUe-׾3)]Os."ǗLk1n}r""5Πv߄~>߻^/Ggɤۋ(y̆Zn+KT8ټ JxESqv\-\DDDA,"9@ 6;vZ+3K9͚m7|fNuR} z8u& Av%.tO=>|l%ǽfZfx&r{}a S3-#PA`QsXMI^F?W񸉈f+-ZI$۸آǪ^e\~MZr Z+9 eÜ=>̓^kxσE˅w\_mוaV YSWxMq4|wB:L| """}yBr_EsG"vBKm8u55qL퐯YuGٸ:vQp< _]c%] KexaM|O5>&zN^ylkx7OїWhG> Fgz5Xz-o'"""wӌ};L&F|mw( y/KznzGh3}HN(5lL@]bu T٬{b.|&H vW~}$r5^noxNk0\܌PMyQpSWHJ5hHuhp+nԃ,"[aj;s.`7־ Rt4eo?lRW߆#3[ շdhH]+/g#V=Nغ'])#p8hce]w\P>MZEDDgC '5lxcT Kbeskߕ[wͪ;|6 /uk`Sb q`3$3 s`vt3\]owXbHIM%gڨw7{gpY7}2 7lEđ:i _s޾vW/R"7 l)v`5n97˃,"2̆B>C#.&X RͲqY*B{Y8ȨE78(zPo֓fEDDDDDDBPp AYDDDDDD$],")3ǻ """" ,"~phH """""""!(8,""""""H """""""!(8,""""""H """""""!(8,""""""H """""""!8ƻ"""t0!=ȈH c{#"""eY1kWYDDziEiy#+"""# !>T߸9fj<"#v*4<,VEDgf1^)8kEDD?ef87X4^?Y5a^ ØgGlg݁u81¢0⒱[2e7f|'x gO>w-"".8N?;#1 op1W?PokVx/cAC3+/ ke}ȃ_m 컟wzhkӞx|t;?E8o]}`=-Lj|4 -s.ڇT1~d +\kd踍,#ʛ`<Ǻ97.UOj&*Whr$1kV̛LRES{pY[4^1{vq6e3҉qqw=wcަaCyWu}G}pG1UZÄx'M'oڳݜpeiNѮ}<^,@s{ؾ=M#!f4&/րeb~E6OLs 2*lK1q?k޹K~Nj]c~[tlryޞn%Ӈ}m$-#J*?E}Wp`5AlaqLeƔN]#H k\jG$1-/i(kuLP8 Udw1 üg> 9.I1N8?Eff_t+=\n|::9&W>BAq1K?YAYpj?VBֿuՕ|p'{f6ۻxd.|6mx]tC01۷g^䅼{3(+>kd.Y{Ddx6[xH(;NL+ (nןc%6w!WW}ݑ/}Ʈ_ϩ{=F=gK~,wNQ1sAh5ff v2'M >:8=6: -wƐf>봑Fshw$%@oRy+G}Ecu5͉cP}T AJs\jRUi$bShn@\^YtϓZQqs4,:0>}{ň"!!֚~i K˥@>ouJM\Gz8?}Yq;uX>#)5!ݖx4yiۄ7[$I ۫ixhL&HMp5i.:גI#rY.[L˴:.:D^Ta3L`֌\]0"HjkٳRGq03-Á^KwL/:mq$ƴPWNjͤ$J8WTO;NB1-Ә3%xB7#lu3i3v+@ 1 +h'e5fZ=0 Y yDDzĘV_F oz-^Z'$Pl&^ǬW;F, ?= qkOVd(=wGw( c_hϤzp?.zs|N>5JGƬcv0Ġ+rJ)(R1†gժJ&:̎vG<$3|8bH]ªU/{,od6gT0vZJ;mHjR?d^.6 xn밀FǰbHyz4'xjt^N ]e'evLz1][>mILAR`s5.ܾvHdbkn[lOݭm[7qge%TΟ?n)QZou\NgArvgj<]OmM=S%}vTeC?VB}Kj05@8./LRv`K&r5ܽ‡qw->EX(ihov}ꧽ>hl?Ϝ&"2oYZZ3(5>EbV; i2}4~c޼9<2'esO֚kYf׹ᖻ: ìf[pe=6[mvlpǛݑHXzt}Avk̆{a vܠ˙lrMaڜ5|o2oy1կeRQJmc;A[.z%^pNxGB{om3PK͍)?%y.?CPuw6@B=`#ĸHMlJKX #тG78 ec*qe61q;IL5.]3Oa´L0AAtB>og88-});1铘O4p*G #ea5۱aqWwEG# gS uBlj+T:.wpw r@"1` RLEіLɒ,ٲXk{wouj}v{m˲mS$J̙b  0 @0T%b3uO}];IQZσ!۶?116|NfkʒHNAIGC 4v{_=1\nq40-\ùh&GߟȑCqn5NT8ȄbRW[!Qf=ϕl\M#(n,e)Gj9`;E6"HL \jHP]Nd 0J;q @p{u;3cDan^i` mFBm}ԛ}i9AO`T7EQ=j{h*ɲ#7zs$i#!gpCs|O 1s667ۥ֤("Pv;4iZ1IOv@+!t(IkuE ҧ9Enj= oގ W82|GABVܕ~:HH!1,l\RąWmdiZ aqX58]r&k>^K܊xbbCMdUl\ś+z &4lZK홳xJӛ~?^U|ŵLaQp]?s׫ukҺ 'QPDYϮ IDATf4_7X2Bpbl͚I",8Eh7ainAZzT18yt^_2J)v{EM8YƂLgrr& >Ś_*7|GlB멻9܁auSfIb|6ObI}h~3-0lX>F&mۂc{kb}`` !,mϲ~a4ޜ0_ڞH!EՊMG !:yu8HI ]װJDMV" ` frB{í/sݧ5ҺaT؆n-e^\uxj*$,m&zjC=n[/UХ&'k 0L']5\/n'*͸x6?4lK/6uvtk׎qowVfʉD`H=[1BYXZ!k{=U{*G|"Ε҅O3}64^nI<({}|u~p x0?\X[o~1mǓLټާbc,Bϓ0vn7Mq"%愻ۧ]ND.wo?osY]q _F\_',u$szGf%tVeאՁ˄kmWB{,h'Ժ}DKQOX؎>wI15`םeXi*y!c~ާ6Ѷݢqp %?)6S+Gk}>?'ㆾ`B -^ '1Vf3O??rpJouh[cJ]~nWمrYgO D.w徢"__"nv:Tm=U[DDDDDD""3MMw'<(LNV,""bcQ,""21+AIMIoي}4ꚻگ gy8()a"""""""p@*EDDDDDDXDDD䁣 dn[ 1]ǟ9M ov%^wqh(HnE+=LDDD&R3w?SED`X,r(uݵDDDA@hx=="2A~;G,Y2mN*WirYc6g`"0fo_@] )f |p4Wrv(⑌Xj\as?aq^Nd~R8>]TçhryZ-yN{-+א3;H|NW6 9?cMb\km`!|J˙MJT 6gM567ڶȃ0qͽS,"9Ht+܎5o8xByJ=cgio1z6[f[Zkw>g! ۶Okq2}  Ǎ3f~ݺ#;/?z}ND_f}_leɖگDDDn'zR5UwWR^Dk&/ٕk?ϙB)qX[/ 3Hյl;̔쩄U|j2sVUrݹ8gev}YFy$[=;~;ޛ2y(;r(Ɔ*r|XO0f}yކF@!V* ʨki^Es4~+`۩e\"""r߱lDONhna +"264}.:; ,q߳hkB \MgKm-m^[Ck`Noӑlj Ҵ"☱z#Kk8qrtwy MWP :;o]z=kXwXࢵ:/?šӼG]aL}yf`Mx#4ІFL"1їK4Bã 3}ƲC"_-Mݳ };\~rSf3).a|%fzX{jU7mO&""""""w\i{˜pZ9uø 9jxYDDDDDD"""""""gJHpC J(oobN"cI*ƾ׌1}߀}c8;QWG#P1֍usۗ-|'{k?[Xx$5: pUEQi7 HN# %knNKS|;[Bh?. k1]&RguFgyvF=xue-|5;׌+ {8|VڂC$}+>)9}H0qq}!J)~TdQo7&_;lb iS+=D g%fV 3l*~|f׵m 'u*=ƷRt5> oS sK>wmnjfݺ~k$?/)RSR%) QIR\\{5$ )kil"wgeB-]&8{#o\laƧKvzs yJ\o_,˕=9YbrC,'o+X{sHN~,^qFdۉfdO%3Y(=[qFj8u^8ws沃9`6c6S:.swڀn;1׈$=-2U~WpΦk8QITl. j/ce05vVOő<*vxĤIiOg hQ=MM5\=c1NB_s6+xNEr5.t{߮e'ٗ[߻{!1UPZLbb8>$$Rw AI 670G۾ ?&7Wr?31]|}{i%sk{1w/ywsj.8A ?y3?S4Uqq!vߋ1Me.ɩ:Z?ʮ3ͤJ'xh^8e>con9&*9{KF70֝.+HJJbӟ5~~~Ђ,y_rk$ED3,"F{I15>o*$vq^BEC,%%]#bsY@YY+9 2A-unķ9ֻ8 c̾xNV;qtVS^${-1D[9U/ JМ,q߳hG?hƹshV63.r9s&tq7}W{ /%#"rN߶&uu}h DtAIo,V,NRgvލ-+6,QDYPo4&**XI;bh[J/]uL^LK3gte?D{ngp-8$&&&xNR\e?_u$`g Kwy7@Ќ<‰̑ꑎihkllc\tu^fx;gk㬥,9n?;E訦4 K_(Q)-j(!ȀֈXv.y\{ iAMM aaa?r )w2q""2>YD& ⦕'k$b+?@ ,v-Si{Po ߾5'j3}v"' hӼq20<n UŞuS܍2wV$g Ȏꑟt Rӈ@p2Xۼ$Vn^iG1um]8-l /pBbF}[\.%kzg0+jqq;߸B?spbrQ[VNOV[[x$1I ֜Qwb&e2uFjxLn;G~a`9ߣ/&0_9to`ҍ,ZG`i=yptvtHJMvj+c"2q)*"j fSY\{li H|g/3η֤tg|Z\QLG$&?PK3ب~X5pjG4~\U8IM f.s`ayls\9QhLB.p.<&--bά5#ܸ8:01B <[p`G=Yhih[z]$[7 :i7=g6pR%35\rM5_[Lx01yDTQpa/8I*h68?ylYBӍ]{5~ǫo &r uC (RÆ?jsF42c%6.:3ff0J:}1 "_UM v#"͗M ;(p AYQ9,)(Xgfp3ō"vqB64-8fN.'aL}4:Z::r{z= " #k*f{^d+`_1w^ZV<Jdl"Sb㢸7+=`p܁dB ~6&7mus:x8%аX._Q9_;kv⠹Rޭ\;hve"9×K7bꪠ.i㱗U 8˨ ʌI}7{HL˟2uϝCGFph,3V,cF9wpi,b1tpt=7>;"bȘu&]?Hⲍ@dH(qdg=}lVu@<=ɟʼ"ZuY+4 t3PGG;ݝǴnM j#-e"(’MFUOc`lY'ê|.Un"<aX~>XXh~ob9̽\!;mj$ǯ%ȨwPq=j[ɪEOjܣ7}tP=\ͪ[a6;c>.JOR|58qZ' 8sm(}4ΟZ)iuȷ\܀<8J/n5Bw>gݴ7RxkE\ް_~?Q:&wm×#c"c}O;\,"cJuMIX/Zv<<17[(+k8(-k{*(6[AI͍;s|gFၱL"^(K6M?̱<=Eᾏٛ(3ΟQ})=ƷVCwMe\9y븮<ZƲśN G{#u .tk6"_]MGDy}Ga@sC je+WE[ gIgǛ~pe?`i݇8F^k3).Z|̯|O4ߥݍg/e0xc~l\{1=(,1,7IfW!ALOrr2%%?ۋ*5"ϤDhkjS&'p%/n\VWEdBQށHn%v Z((oFꢵ<R®_,G9.qU~q7/aGXwm5%z*s#[)hJʼ?} s٭Jp"_aaXڤ"2!ܯEXI](9A=T]wsuZ}%կ|)ؿwoOk}ylk0?/?T]3%W,<)_sМGbqiR >Wdۡ"_-_eM;cTb{bCMDdTm{xYDDDDƕ9䱹L*EDDDdG?dч_qd""cWB{rU$%%QVVƲeˈjDdhAAAYD&""CDdLmSZZ @SSaaٱc._!4,\L8zW-""wCOO580 raZϗ_w{n\VWEDDDd0^!"24OFDDDDDD"""""""p@*EDDDDDD,vYf0#E9ZABv;봚-+א3;H|N־39b͒iuRyn7JJ9W5{Iyh5d惣D9+}ws'G2b sH4:#VXlR9;hq~t94DfOzZLɎI8-/v`FɾJ2@vqg! ۶Okq2oecdaWs &:aCf}H؏pŊՀ;4er* Yy0=#4&J :US,|t9wg-]b+Ytu:0OXT8ȄbXxh|KK& Kckkdn̚L0K/s3dQ?yα܎LN&3o2gdQ͎ -jc#e|(;/P~{>#"̑ϋ8 dW`yiJ exA~ƱBGߚ-&gO%I1= WQv^ F1!ʕ2Z@+*cY Jp]I o3OY/o%,Hke5MnaL!*^?̡r:gn\EDD4"22kt2'b9jˈbn|>i-,\Ks|%"hk==T9o,3%2X[gODmLYlIE4 Ժ'+-,q߳hkB nΣm=-u 0okz 29Y8^qre-ޏP֔u? zh.>G;roեE>Y9^3x]Gxwe?0+{,z s+}gx,I7xӛj4'WۣYD&N_eN,6?8A9y([Eoqsz&+iگ}@!j#66 #> hF] Qq}Wgd\M9/W;ML8ϝ#9q28:k-Y}̕M`NV4j9(+4(&EY,3^Ec) j9`^iDLGe݀opSo]Mck[m5ϻ1.ʮr*'Ȓ-)t񹍱7{ZP3\,fEL9`I<`2=q\>pGC^6O/axDDDFcg;""/iko455 lfq|5)ln-ѳI9d%$/W0/ۧ7 ֙Ǒ $:f'FTB:jyA,M!,"74KFqAsS )$)?P"c`ōM#6ؓWK7z 8-lcS0qv4zS/n$"B{>)]'on\c-4Zg?l[}mr[!~qUDD^P,".$"1]Y:):<1oBԕ<3@ljmo0ɌkșLI[M>'pako!jD/3ҫv82?Mb?a}7}x$#;h!E9ZABv;봚DCY 0͕\='q? M?fs/juG~ on81rln=mi1n';&L_dY&*}|t=y@)&ttbԍivP]MՀλ7>kZ;)|]-e%5",=yXBEtpy[ )v1;i\EDD "2%'&2}j&8rdTN&2WwpML_N~ύ%H_U.7t=k3}h$(6gaWs &:asH[,OM7h0OlقDc sm+.䅅=g>+m!yl$xggԳZ׍\6׺_s&qɯ8pTlpc0Gچg +Vڮܗ˩2fupÔҘ2+ݗ'(ZW)GOYd^l{ߝwOdy/VWQv^ gdQ͎ -Epc#e}+exA~ƱBGߚ-&gO%I;sc2V<+}WR;9 lvRMU:S~ k "EYM/4LZkk0n[tPY9f&<9~9W{EL(̚>̴4'X_DEe2"_ŹO:;+xL#9ZKD zN++z*(r,Yf0~yYX@aq.QmRϙ~U@YY+9 2Y.ZE8bmu-v?G/=V_H0ȥ՛|i 85eю[uigOV0oe3G׌>^Wv̊E+2`6qvzi9K"UDDP,"Jgg'<:\R4 !Ɔ-#M̃T}W'd[i|L"5.Ajk4t fKl@LcI뉓 _|#68cRE䚳snF 7'd2ɺM1qu֑N2|̸̚^֍M4fPo`gX Nٗmz;cVn~6u86S^taik~ On .9}=6YƄ9ljhԪfŏ/ۀiO}Xuۥ=Y ]Bd_ef1fW\#e9pr_‰'z<{j DD䪗JPp$‡[gd{?_q>_OځQvO1mm9 ]/tc+ɲg0~x>پ,GOa\IS+z{_ -f/cΪ0U'LŘ  7EQV܇w41r,QJ~v6%=g,~m/|{猡0O^Q).aEYq˱xD񖌦4; װ4}أ!=;b}Z5k9츑͓FPTPHĹxs{/8ujLi ϟFETKg0Ɖ8Op4a8|7q qWyw|7bEh>mGq˹2}٦[-grRl*׼ke,+,j+6[٢n*[g=9mG3ǟ/bl)K~'pS{q_"B[*o sҗmLNn_Ͼoe˛̻`vճ_=ņ{m0߿Uq IDATr[/,A*rv;'7qtLv|~\h&ȧ@$BFV+?X7L̩Zmm݌6O-`e5,"ׄ3g(^ѷd2Iu?W鍂\ZZZt-`iq04EDDDDDDPpICYDDDDDD$ g4EDDDDDDPpICYDDDDDD$ g4EDDDDDDPpICYDDDDDD$ g4EDDDDDDPpICYDDDDDD$ g4EDDDDDDPpICYDDDDDD$ g4EDDDDDDPpICYDDDDDD$ g4EDDDDDDPpICYDDDDDD$ g4EDDDDDDPpICYDDDDDD$ g4EDDDDDDPpr.f|5Z_""""ǕnHOv&]8ˋT~MYWa0ŁNb`t|c!fȀ)8%YvR~"hʚywJV"DOIEn gYlV|W_}_ي%KP6|Sx9XCp+ϡyad,;Ng8}~s02U%s'Pƌ8w=;JnLþ9˹wJgacTl#gV%^oj^t(8%7lsExJR@b-o xݘ%͋kpOֹ^ǒ[7 |}${vme\ lmnXw"ffPT_MqQuT%+7f8@v[}cFDDDD.7g,"(W㎥wU-gTSlloh?c+ؓMGY6g@0"Gxr )5:rAij?PSs{hno)h߃EDDDg,22X@St8e.%/ǍQoa/fcC;t% 0j(  r.ACDDDDnV\r^V|`Ȯq1N7y\~O& 3PKm s눲.9}=bqa ?6;L l plFBcȥGLCUr!"""iԷoDDh0peQ4lŝVe~V Ƕf,[q6 vS!3?yELe_~#hbY>lK3{X@rbLً(+ՙ%d4ٙdx=8HG}oKV0\!ٹL]ulY)""""W98%p8 >ζnË?[E<0ǯp]cW[O8% /bI|./hbprSċ,Yo$N]ǑڨۺwVo}Ajnfۑ?"׳|[qus4uiuk^2xMBl~u ju)`ђK&\%"2Xy+e粚-""""""H """""""i(8,""""""H """""""i(8,""""""H """""""i(8,""""""H+vD-W """""IFVn"""""""i(8,""""""H """""""i(8,""""""H """""""i(8,""""""H """""""i(8,""""""H """""""i(8,""""""H """""""i(8,""""""H """""""i(8,""""""\{̾ ~,+_e.f|5*d;K_m69.O5N '7sfN,~-3Y^ LFi:Sukz2ү=i'Y+|Қ 7/QEDDDDx?KX[">$k7u,gYR+zl#DFZ6d g^ zz9]/qy~ݱyH$گM<'-{F/Εw@{1| abx LS5gl$dd>+T~ٰebiki8^ _*׋d2A?+W_Wxa-ytE~gyKY:wó$[phQS@ oK0/sږEXTdJR ʎdmc'bKvwlp>iwV{ߐW#fngxF)<\XmW>CӮmmugpM+g;!sSdAFn?km&e.z.`2ŏ=-P8uKfٷ蘎n$랟=m߉w&czG{&d_t,Yƨ=\مlu 6G{ttq/T='`=-bvgw7u螟|4g񏝌Eu~sPo?k q<3Y~J>v~Jm7&BfGxhr|};%ssʕ<ڳ%6rfDɟ_I9EjtĨܼu9rp]|_:utb#gV%^oj^X)wA _!i6Ț;>g`a}+:CDDDD\㜉3u&=p޶O/ /6eօk`r;W 7h-/D[8'5K~o=Iv6ǵz!8@ce3>GðQR;F=:ʇGy?dWd_hϖnsݍe8maz,p75$W=MeC'}{\n i4D0$  %O_&o?cy7}_%r>8߇Q|{'~ϱ^3ƀYLX{Oa9rq 4s=;h ;y33xwlh&w׸y(:~w;[O@<) }ԝlbw2Oa̚#{ul1)ƾg"rr/xwo}L}u} 4l|L9~FDDDDo1ouzrG߮ut#shG$=oq'2rlDhfBıI۠|1yT'2|D6Й~2M{#4=[O[_&<%_}pUwasUXur=M[N%a_Ý vHjK/]W"(W㎥wU-c.繎km,FZĝ{3–WL'Ge35r )5:rAi0 64v~=:sҥeL-'efV{2" X4Xy9D(g$XWHqfCYDDDg :Mh $oL.8gi?I68mՄ[;5x}Q، r>g(FF?"o(jBNS$6$r -Ns[0@'ᗳOppϵtnj>ѩ f,1T\lj\l>N=נa_u3+ȹWxN$V1[G>]WgdxYbk_ۧ2]q:ᓾ-7u5`f i66\i!1;GVJmlyct'To3#0zy?Z?v2?p2y9WDDDjU>.5$zΖ5[[;*4Ft_>]swFl[_'yU4BUu>ގðٻ=`ﲋ.܇}9S+bs gȺaBkKC_22\0q>r`jM1a|:TO}j6#Gx!񃖑Kiiv4:8%zm" ] g7bqa5f4@ uɹ<󏉈vM)xY3y1?]F7HӜ`Hr[ӭb\UV;tV36Fn7qOٿD$CΉ/}v$R(N/V0.a/Do#h8b O~@)c&^_fÙI^~>;0葎|q1+*e%(+VFd[ԥ+_PU&Np@"qq[KQ7DrbLً(+p;A(%?;̞3 ("""r lQ3w\J3Hm㏑7aose2mz.A*܊-̓$ x d|+)rROm(&#;Im%T% |3D\IV1m9&3,pBFkqc<8'>Nf.6=E2Y{D)'\]Ӏ{nIouo!x~@n7Nr(G\[; 3Õw797%q߉60<>qIHSHD dHVca`+* C,nxw#mo^Y?iV9v8[[tgۈ| l#p]of]e[INo|gËXRv?_ m!jؿ%=F嚗y-E%^Pm_]æڋ-5:;W(;ĉ8m Fi#x7o fѩ>xcKYp%;Iڻ#ڏ+x{`{43bf?pVT͛$#q6*{UӐx2,Ӄ;EⷴOж?w0>"WV*!bB?1kh; LN~yhk$q§nJ[i)ܛψaNGvڨ>]R^=?>8)OR4&M{z>OSOۑҴ`3>q$c3=n *~Jh\s,ٱLESs Z[[kT#o >ljEm m@con'_ /xu^) }ԦuDDDDό\7q߸5jtk3qtcY` YWv?v""""r ՟ |{i*?" """"r-Sp(#b1Rp{EDDDDD䚣"WrȵloG%""""""rQpICYDDDDDD$ g4EDDDDDDPpICYDDDDDD$ g4EDDDDDDPpI \6eϻH92tDDDDDDDZ-""""""H """""""i(8,""""""H """""""i(8,""""""H+a5?]Mfʤx6,WYddx%jϴ,"( Vʪ{Lvvɂ&3#ǃnpJ&ILb1‘Xۥ^DDDDdPܣ#Ha z`t5-*_PXHK(%L^V^ /g az`I$ bil6l&ӗJh&u3DDDDDŕKkKxĹBL\%6GXzĹŗ_H|}"q_y녑Ǎv Vu?u8W|fQLt7ؒ#;.4{_d(uwsdbaaoj:4۳=v~&E[8sk SgPA V װPp0l޽|vz}aly| oN p2/ wϪk~;v c#!NBNa { ֬5/cE5^qr_9/\y.d-xgeh_%4zz-8xۙ>*ٲ23,] ѥdd!]em^$4ɞZBNw21kIUb@ܣ;#ӛc34[i9}' Yksfx8a=W5j 1rXL)#h:r39˴Q=7ӻh+) Z;kȻa%msN[+R2Ɛ5~2 sPvy|LV||>b]4M!Kupd0sj.;h% \S֙/!e!s9Z[3ikKtcB M?5r )peי9 𐳑3iE xzԱdV@:G=EgTMgI!d&ffw()Sh=\i1Jl7ϻǑG)߼96TSO3%`0(,CŭRQB]Әmb)7s&dP0q_'eOs͖zd[Id.cF{}I>No~^z*v :91d Fqoad"V?jZr'Ocִ Up/A8bmn>|t8|:X1cs3Ypi 0xS o빢6e9y/>rN6]WCussnbKX񠛏v!LґI̸pD#Ggo)w!o|S] vm[s{j3{OKIʼn' M|l9K|\B#o*Xk&zj(?XCC7\4r)TΙro`>iʟʒnt6]Y%65+]<D2^$O𛧟>]NMMzk\R-g0waYNf`cl@muGTEqF2BsInFe8= /&F[9c+gmIj-Zf-±kwy:sxW2ng9WchyJq/i8 ?jt۪lӋq3nd]72,Z2׶d8dxŸgӉa4IDDDDdrfEh޵_eƍM؉~|}q? e 0|̸ g~tG}ߧƦfH&/xr_F 㦶aMk Cț=} Ih h`(/K0x exBD߇\lE&NY^B(Y8 _D"Ac@60׹q-%-ebߠƬaó`àvi vTDp9}*r:DUSDDDDD4MNt1m&N琌]Nx ?; #ϵ vYx=nv;;l6v;^V&.4U%@߃e(+#.,@G"Q2^t^/H/aU4lЫj_&3#ǃnpJ&ILb1‘,""""W-ӲOv>67]̔IkeVTD/e?ML" S[WaߌqO.Ypy{u1lvlg@3ወfcej ð (?H}<џ)8\@ɶ*KO}|ib 4EDDDDDDPpICYDDDDDD$ g4EDDDDDDPpICYDDDDDD$Ot IENDB`litestar-2.16.0/docs/images/debugging/pycharm-debug.png000066400000000000000000001151201500564371300230200ustar00rootroot00000000000000PNG  IHDR|x:iCCPICC profile(}=H@_SKE* CdATQP Vh/hҒ8 ?.κ: "ƃ~{*̾q@-#J|EQCXff}N_.9ՂHZ'ͮ?uWS`.#i "~Fߔn5>N,ue{tPar3bKGD pHYs  tIME0tEXtCommentCreated with GIMPW IDATxwxTםwސU"Q lljdx'fM}w|כ8qc\ӻ *#Po3Ch$|^3ѝsO}>s=iʷN#@ =dQ@ _ =YJ*^hCøG~߯F7C{?Nm*vT0E=sS6p-cUֳᗟỏD21e\g&bB7TQzQR| [moV ӟ楥m|돸ծqL 4 s1^d ),[ʄ@PBzY->T.n!'G,QQf݋:Fvkۻ$7NSM }U^΄J&ܨ-|V2^Ȅf.fUN)f>'-9\Qb% {(iKmu%ŗ I]-'Xf{_ﱒ;CY~:Pj8fP})HuHD&'-;͙&+pz㑪P xm y2m>:p."gRf_3es]fyinR|^¯wC&.|-.sZ(@%,A\kgL%Ayen!&0-)ō q;#18'B&_Q<&-oKw%``̲'8#&QCyqh9}dVzc(h $2>FaMcJ5G{4nARffRa'P R:qh ɱ$騿Po 2ڮFԈ<XOcBꍽ8w^0A,=Y/ҮjۼSPF"4E^|Ħ8* e+tFi+̥6b^j$1YZΜ/9t+EMfɂIņHx8RC|46j,VSonܯ|v6lN^ hU5u!Rw:ӊ[*hSUUX31Ɓ8¶ST,:t/eњgYlRh)_p#9vM]FIQOŏ|ˢ2gSfVV_My\ y_'f:8[B5cW`鱴{$8+Z7=o̾)W.[#.s)Ld:6T&Xzw[T)RV$ԥԫ^n;ș/7 n)\8Jq6OOB%A麿zO{=wۇzC}?zL!iO|95[xp fpon>U>bX׍-CfBH!OYTsnvvcݫ |ϙ$c׭zW-~-x`,^`vͻ~DXѲ!{*^w /!B++>!`DN_軖Pv'swEX~5UUyN~-}t }z7_(@ y[ߥ..-ڌ_%,KYԆ@ [ފQzrLdN`֒\fef8[iRpb?J[wC"VwluK,i S1 4dMVN +rN& qD~ \AGtRV-HRWwseRKM 8VGTJ1eh0E`r28x+Xxr˲!}{A\hBJH%*PWAX%C4]R]0Ά@cae3ʳ zm8Y#rF4W_% S3g֦qcZNؤetoq0)A2Ax\y(ᩎqEqY05XJ|ɢőkq9bԶSua?RnD..m%}wyd9RHKY:#^[N`ϙJ:&)29"%Œ礥:?ڍs@inRD.+fQrTQѽT鞦>"sZ7/ DֱI8uTZ0PCt1K*RqaF!m[J?wlOԟdύ)JLS&:9Tkå #cCyKkzw&;ut:u8es<؝2i^Jwpm ôC|^]UV\t/:m>A NtFM Iq25+h6,,@\H0 \33|7&e%?J@xWuRa[*ޒ+h.r4uh.@>AT!y3LoλvQ|+;yLAiZZbZ5VҟXڛ6vcK{kT,'{#2  #j^F}U_Jek>/!8LJb?g&5w]5OWu+Q^9 9m"":H#!#@qwvИeQy>ɫ;ZtFFlDwQXb!D~[R%hiB^tV?ƖPj)JV6C.ńd P]\4K{}N_ZTZLR|, SX;gsl1Qx&BhMmyEX$![~+v0=v/ oC}]::*Vxtnw:mP/w{C5IBR9f:s[GqƦ6i9Ԗ?|U*˞XAR: dsR٬Ѽ|~n 4#`Xm7n{izp|ҌBij<׌ Ȋgyy6GOJ"^ӘCvN0f[,˟!pC\ڐ3Թ,vtȚą<4%_;KT%{Υ^^Ϸ lܸQȦ%#Zs/{PUC[΃O<'#@{/eF@ 9@ _ B@pߡg @ x@7a  !@ Iv>= A7Y 5(k 5{}p9 "ƽ\e[eb(*togĚk|̞m?7Y~~p69/Dꗼ4mfF^g$W>L NcJu) fOEF>)dh}LΊDque$e[XouWv\yQYcT'˓|(2׋\ t9vY4odζd 6t0MzVhjѱ 礁<κq̒DC}9ozLgLfQB`8b!^e|$1qЙ09U>%R#[/hRa j"Ԏb3ͺuKYÐ[+^M.x WL|9;ibHE^>9[ ne]Z7&j<%⪆:U%i'9_l07kg8xnWIHEF6@ |'2aj5||6vǫP>\.f?W_9V}ߩJҤ[ =B[CRp4!6D-2.Yax'wѸHw54yf~QQI Q}26JzpF>$Y׍l9A q,'4i,@no8`N0ЦmWm_, B"|Kμ)0hp;B >k1jpNqZ id|~ıΡȶ`/?Lbߌ ಎzxqnҮGEHzsUO88/3nNC.M,KhuvX*[?.7Pn9qHvŒXnFLO޳zbgdj4T4jfsA-ʍa9E{G;٦վ#u@]fXbl* .ET ҥZccs<u{ aב֑(9㑃|brl+͝5j:`47j}@hIDSQ]P,Ro{v'jl\fgBZ-W:,(1l7#|麖`I5mM# H+MHLp)n[b^-tF5kpv IE % Z+${Mʜ8Ķ}+>rDl'/u =2C۠z/ "2%CKnCUUp7-wY@Fd?Ԉt2Re|vr\=κ//h'ěgԴKx%O?DSfU"(5u:jtfdbb=ڠۦ溙_ZS8Av]"UOćmd {;d/C[^| B|:Kg!m Gw FFT m-. 4K[C۽`u.*t>XQ[ejU0 PjGS;V̹ehjyA IDATqAlj+ s&]闦lh9[5P@&*4kKխQ^p;e[TrGqmzYxa4Ⱦ!(y%.h'ԠmpKa4wV~6Q.sP+^f3cJNP^e|MO-{ UR7#cy5_e,I7nTK"L"q|d}sdlypO~0nC+?Eu 1v =eK%8 if]5ޯ{n]9ü`z6YaX' MW ~1OgmE0vB;*$&-?3qMK(2Jĝ xD@ / -@P?Pa  !@ _ .4CtL v1Q]Mmm-55Tpܧ֓,˯v[PxEUWT"<9#=?2Ʈ{:p;*:yX&l!Kw+| [;߮52۸%q4*pL?iDx4(%? GsC@0\w͂G&+'q΂yžzSIIwRD6|u-E8 kxADN`4~H狮KEl!G[%%XUƆ*P_,^J %>@d7'yjeָ?;Juvs\bT]χ2"!Ұ<2VW@korL,A\kLeg)VfaFsR{ƹp47s)"s3I (h(C\v4n2s3 =o6} ?xD|B|9 2q/3 YP-]i)JXoYf&X$"y{7gruTWpc׮p&,'7+pOl}e%7w 40~wyD߼O0D VЎ[2D;hX≱}͔;DQ:r:M`<WΠv1dg"yKۼ 2xFԈ<ì܎fZ^?,9E4<ɓ8ջ1fMlpɖ 6Ԁɬ~d2ꑏx.P| LtL;%'sֆKFƼX󸗦b,Lwnt(aRB9)_R Qrjɡ4XqUf 4e1FMgͺ5׻i 3 T9Mj::EOF5O_M(دP,)))a&Kz6/}KHP`dMN{v mzࡱ:ݝ˃@p )'TiMJ~jPuRa[*ޒ+hQp󥹬KEsx>AԔ!y3LoNvQ|+;yLAiZZbZ5VEml>Ƙ3!0Q,'{CnYv+@&'!Ym$P iiITIiP#߷t'f .DqamЙ2;L9V1p4AX73=㟖s_&/. 3&Qq;>]!!Iz~v$>Yqk*-jpms)7Ύ3qٹ, :'yu#pG65!N7`##QXb!D~[R%hiB^tV?ƖPj)JV 1SXt#k:Ϫ..^ax.ՆFnJS&Lsl mK&Qz8`ŃFt{.E)7pIڄJnL$R6IV=< c!dNJ[!.huW#ZM/[u/%Ѝ~bV갫P[1gѹUl>-V們ջs$ I9:hFmʼncBڤ<;S[j?eO hQB݅OƗ#yCBU;,*Z%m&IoHr SkKPu{CJK>Q!Hōh/ue3L\d{%P󅓔$)@8]ƥB/i2?DU.{>ÏvPW"H5I֌!/,,+O?=pJ1 ݥypnbTASH5K ìػ-^l-NL)$`6Ѻ teTB N$s̊El/"=. "R3&Lu45m4z]6RN?ɉ4X*pr*&29?<9߻+6=Yi$DGFy+"\rd D˄kXY˙s=\E._Õ:3_-~>t j{8hux!f&K Y#σ@pqZwAlv;.@5<~=2F/:q sR˹3e]עR{j;{װN.Jg]+"\yqzCɑ[#%͇Ljx+ 6kSz#><׌ Ȋgyy6GOJ¢)jR n+*VTj2lnaL[ ȞV*K0<.Jmeg99BkM1'DZyVpu:s9r g^n >*).\MJoesxo#vw1kl0rsp! jͿj<~k~C7#Wytt=KTx(/_z=^cSlVJG )ӟ_ ̌SRلS!m fX_/?:|o{Dr7}h$m2rK9~΋Y_:3l}패}X8 B@ _ B@ _ _ Ch4$%&J]}# |^=@ xPܸLrsҹà,85hN\z^.?5=*$ jF 1΍$-@ xDe&Ʀ^ߍIOv* $̾} ⧳hr(nk9՝t3 ^++;>YӓьMZmTeٙKx7xTяJoXwA/ɿ@ɠtuB?sJM: yQ4 }(@ 'DǎDlYNgaa7M3t.TUqsMJ~) 3/+ 6}Bf5k'K9)t:^/)))|kW\yH+7ΎZngo~E A7mz`H.d钎HW~a/e#i~b?oparwM?! ~}}bM-eȔ'_bI_͞DZ ZˬxLg+M5V NXTJ)hok'0C"VwluG!2Dmۯi9m+dMDLG`%wttDg/e‰$jq5X8p7 [F/HM 8VGTJ1eh0E`r28x+>$7vbԉHNREvn Y})?&ƬǞfEP1wڀPR #o /3;v'L`H0kXm; zD֨da#s*3V4nW nB}- 1- kkEoL㠱 s8.B"Yɼ:olD 6bG >̋kq9bԶSua?F*Y c:M(&*;c*85d :$gsɑ^գ fLflOvr:*-E\2Ktz R+Ѿk4+].ܶ_dǦ Z t)JIܘCߛg[A^x]f<Ĥ903@ݱmñ =2o#aCvF(ﰍ)߾W?mD=̪j+[/9D 0j\4Y?Õ\ t_CmH<؝2i^Jwpm ôC|^]UV\t/:m>A NnoCRL nFQmXʛY>!(aڑgfnMJ~$*W`_Acp󥹬KEs{K CHLBԩVsŻ~CpxgQk6Ҥ6ꫪJ>W*[{y )>>t;@>85qh.^ڠI]ŏ~{<+7fRm=\F*m00ڻYxqQCc&.;EA${aTZ{LgڝntF4 _ Sz1q\RA_:+?RL\ (NJ i]NޟPi7FTb2I$Na9V[mmg.tԆS=v/F.>u.J}]vj?s<:ǻE_`1蛘*z@pW"NAU=7Ia9oT{WtIq„蒽CTgssF*u;qKswwUE#!#i(݆-hF90nx|m{D1C.ٗs j;.07;a!2Ԋ&|h:9UU)u35+SGj0T$vڛnu#q.Wܴ4Q5:5j; m5U R0SSCqUZ Ů<`yv{b5qΖ¹#rK#㧐j@2=-Y}B"l:\:ޡX  !=I&+.XSSZ"H ,6s\*!MFõ X5X"cIÆi)յ_ClL(FIMlL \>u\J!*< W0gJx,oAj9w+0VZTjOmgO6pکzE,냣Ia9)*QoNkbj*[qiC.R -sO EiL!;'ܭ TOtm4 yiviВ%*[|@=yRV/|oPH7nToiIo@˗m@x@@  @@  @@  . V´,ML}MӲ @ B_Р b^&F&FyY0p-r v34NLQ`7m/íWc^|sh?lGiw?A=VnP|<Η"6(Duq@☱l)'fa}8QC  c\رt\Ѷ b!&e"sb#8#.A[R:@RSyQeݶ ѓɍ(o7WvXr"$KQ:M=C;y-7,E5-YEY]^ąڪv{Tv }3dZFx,PQG shX≱}͔;DQ:rۺfO9U>n"~939{3 ,{4ip_d%3SǩA:T- cY&6d˅/Sr>-֢FOa <o3l+6:)IHGYFRqz|J'$*U {[Au[3~ێ|5 b^s' ^좾Р` vplG3 G9zuUvO>Ks|b7e()\x7yewg}V "4yj -`LQJZe/`f|_?Ō&.ӀE5DG2ɸ/9k !+?fV__WC)zF*-7n0 QG4e9,H|5T #"(: yd9`(*˜@xȼ* !X7vmȨ`hiKHJ5^JoEw|4{/csMhܴJL}5w҉bxK6ܥ#_^4,2C9VtJ.$B h_@tQ IDATze* hjjbHրEt]`"~sN `j̍#Cǃ!@OuS;:TL`F`UHI,"BSlSe['Vt'λMr_Vʺ޽7/N\&Yձ$K,( $@A A![ 9{9gk׋"Ir6em%<|m }= f>t6a5)fvlS`u#=PՔ~n~]1HIKVx3Ic5X] Hz]sr{x`]5ebI>83Ge6:<޻ϷY &xueNw~2}|a tqu2h;,M^x 9=HG?fwt3G8ֺص@B./\Fҩ| 8!?|yNL+1WW&FJBA?92Hk8fl31t!a@6Qt(T IĆ/ EoPrC[EvqwzE  U*oJ!3?$d,F˗|BH&[wmm Yi2VN:zns+Ɗjd9sS0xy4e޳Jk趵Zb\W9~.H3&#Bгzì//ߥ50A{ڸxFOZt:+v=f>U\/8(:M240b^K*1Qm!FQH-o+o'OMFQ퓷9<&ʤ꾇(#?9-Z_kwE8oyۃdRK/ [7gʼnϞ|Z*2_X[`n>o/GND8wQu~1bl)H kx5G䧗 d&FK3&,]%k9I[`3$' lD~;Y%7E>yḑp7sRbO/EĺCYlg pscT|Aj`CZ57e}G?s:ݥޓe_Ucey(/!骹NOűD'2K=J8nG Qb(Xk̄mL y.%$=̾ꭝ]֥r͆˞5`!(2Tc'x˜sb3);ȇ3!˥'戦$<fYI[ M~_<޹~ֳc&j,4ĕ>ѣ+Q99ӴJ8FxT~6+`yٖAVE,͟DCwIx~ه q_YꝒ$ĩ 6+9KHC;vP__??h4<<:Il x]Ë1 STGKYO;c}n\)ok< p57CŹ7mzmOPZ *s/7)&l eڜ%28A08U:/os^[Gq JLj'NwdRQiA HiyFrvyxǓ^[%#KA%7c})l᳋*aKAYpm܍I"~M8$nʟ5 j?sq˽ PD{ڸ\$IW-zin]r:2i(>m_Yby~8O#+˂cd^{{﹇i@pЎkaꫯy `y2/"!qC|k7r@pD~?{Iez@ X@ _ "@ _ "@ _ wx+Y_T$K~p.ǿ__ͳ,H5}@ _4-Upv%6p=b"HGzO]*. X>#pN6;,bm5ÁJ;B4'o\S"I8/o`9őrD}.9W2+-|r,!F)l#e1r {Ðy`n) yt)nʺ]mu>3i'հvߟ*Ccwgݬ/ү~?Ccȣ1 Ƨ8XUAU_ZPv;qqDZ|qgkrذ\>aZ2-:6bR6i)?;1z%O=&3ZI3VlA>F}|9&0]\Ke2B}?ɕFUƩ?T| Ti;bdHE M:?~ׁly?wW\G8,SwR6n %i:S%U3Iާk; k'|&&B|.܁,{ۇ-$Fj6Iw9ٙxQXV-` Yٌ2)emp0ζocUl\TWB p_n>See0=r.NuB|.q?y_&p#)Vsg0{"N%= O}|זSc%ٝ#RF~ӗ"0wgǚYk_N%ʦ?2ٗ=[6Q@pM( 6lN ߦ%GC~{gO)?].6l/Fi{GbTE_ݏgpu/Tɷy?k '|Rɖ6jNA>U۽ -J3Hх\ XAtsR_k;P~S@ X4f nprC[EvqwzS]4@ FmY4ROU g]E x'K7M "@ _ *H,b@ ܾŔ@ !@ ]HD a`LgOY fˍόV(.Ki#ݽ)IUFK3HSK#a~/}\@IΊps)a@I2}S+31݈-6g@pǣ6[OnF7$9YU 1,k@[)K&1m6zݑCK~ 4.: ĐZFz2 8<1,IRY[IE NzzhGKjYt]_sNw#ӫt9Lns>{ǠPfca>?]M㇝a}9Y&5 #9 90ҋ>=tl*WS`k;[Jӣ xtr7B^7h{#QЫPR.'6F-A(ߕC:2].\n7OxQ`8Kzj"y?ƠzQq K眄h5uv$ )ͥג &QQRCU&y^/Eww0K* 쿊=MKɤTJ@yiorgz&%).%:#0QZSΥ6pdzc%'A Z$gp^9U;FOV%Ye+x[j')D'X~\ZÊ0M uqBDmd6DЏ_a2*^a2Hc4v[%-݈:Yfb>|qf,KgN44Kj1"MMIc0O^IDԅØGT\wy9Tc2pAsK,+YU G4̢jueyYdddQ]dB:7QCQFZ5jEI= ZKq9}ʺ*5*p/@xŝ0UQkBՐ7[Le VJ1Ъ5Ld۔A42̺4_A`.eruuX Y bs}Jb%5q;)t z =:{nq.ZAX$wF@愌PU$X %U)Uʄ.z;OPvU> 0V.OBg>)fBcJʔ{K܈b1|*rKѪȑΡV$ݹLfXғB 5)G>[#ms7'o'ץ8%a*f:sF3֒iIGK(IW*7@UZ=a$^N]AmgeGȨ,rS~ f e%'F=aŪr}^.Sn*3t^iRa.,lrz8mA"g"|֜ZDNd}c}{i|X5l/QNU]!1Ots'OیR6]HH "qƮwDJŕf sx.nO~;c^5+ 6_V<GkĎݤkN'JU'Gp0AWU$Ye&?[H{;ɲvtcXA,<|2ͷ^%n b9Q >}\v$FՁz,V*G%nK!1Pn\Ҙ×s($|Ex63`P&&U0틍x(QKB89Dq% >/}}x|򬁯({J'ꧥA$yͧ^Y}yynNcLq" uX;!1p6,/9C z}sFob5sˠx73lw\:ֶa"L,&ߠz; (.y۾C9KSyn~[!1kJ̕DR'did360ȕnBYU.@}t:Iޣ>rh*Q4堟A;ƠK0ybϷ^bH{E--#37o1^BbVb.)դLcUGq&nʑ1'!K*)6jd*/w 8Y%ekh dNK}AҲd%@ZD~(rZ IW%}N&)kv-L2LBi ֬'~^,C)xJ9x_/!1_Td,dm}B ,c[ks ]J IDAT)ZLIQ !)b+%Vb5Wit1L[i+R.p ذ*(u$sѮ+~={wf075z:XQA}#,Jfk8ޅH53 m$Z=&|9̤)+"DḨߺ$撞bѶ@ X466!߳ӅqzBD% R5:FD CH|5%(5w1 ^ ˭/1}\8 F@pCG@ /@ /@ v}/#b[`,#r` v$wdήn|PF})՚z9X\-S/Ot\n+P2(*-<K5M*,K9E ȭr6VJmIW@]]˜nwGX5~N7 M(݆lrL$NEBewY[$DX_ ³˭^^E! GF.%nB'ӵ\^fPe`5k!b>ꁙ੿gnM]>ot8W ;$I._\-7#$)1 a&hlT2M']245h-H3ՁF3csx\}ޟt K,3)qr#ayd*wc*!n4aқHWxqxsALZv kWQ.!<)1gpR׃G2gBs':-䙴ܩߏ ^'"bo\_>pIBVŒ GSG=,Fn/@yh#9cB&ӓ HbR>)JOt2{  e}-+oPr.VC\3 M:?~ׁlyGmrѷ킾*o1،roeGzh\X  _+™;$8 b8a^_ +y48lC/h Z= kx5G䧗 d$1vLf26hxN t;o)tbʠc )Qk]Wk{rvps|3D>%a 2|%ey(]RMev7p)Zú1X57eɐӧp:UswCosuH?>|9lP2zJtҕ^zl=^܎~{`1CwC;Z#Dt遲K kŦ c 2^qc̾_El0*b ƨ{ |%aԦ4k6md1s=c&Noo?Ku{47`DY*,z={0ppÃ9d[MIνKH~>>'?kأXukX_zI:d^͘qg^7Z*./|Nٶ/?8SE}Xs\GWq ;n1 #T cAfGN/}2A!ЦiKA! Lf: &~̥0G g>k֭1uymse YKԯGC F7R^j3EƉˠP(f,Pl=Ņ:p$GR&EEF}v|sW"lW8e©c'esm>:T@Da2.ԍ@Bv*?-dcgD0!Pl"+ٳ)oLe֍<@VS:֪ٻ%st|tu:0L/%gV6卩r*/EXa*$=#5;3]mEt6.^184t`. "%lw>rTX̣vdٙ.Rڄ"2B1KJ*28\b@@T*5+_~rh0ū/s\kSQz?z}[پqB^Ft7&$d?pOwtY曼@2dOY{s?ӖJ\F_ݏgpu/Ry|vXW ɓX+ luGnse^ `IM!Z#6~n i?yq v^db.wO^;r$۸^Ek›q)_ X n߾F'|R+;P~iFF[ 5LccXsS=,zF(2(ߴӻ8p}-pSyύڷ@ ,)!eHd`X~`3IK>Ѱ1z^8Bg, 118*f%lVlͮ}pMSP@<~uBr.,rI 0 #OLY,t|NQ>zcvk%$2Dߎ|^ýU9//l\Pd5&G=2M$ҫvǫ> o\yUl(6W=?FRUԮzTqtH<:B X n9uRf(x4fW`s (:?z{ZM\VQu}JUA>yÃnBL{=_2CbV5ov(Ȭ߽̏t"k<UR|"3 ̧ [*%OY>e.x6ǯH{_\"$C4!> 6+6F8:91WPa%Lo^b{\ƻNr(s6q٣哮of % [FbI7H:8}2㼐Wh)ʮ/>痯q_6>Ue<.[w)y#)o\[ĩIIZ,Ʊ,xm5IHkfے_SIlgLڷch6UfAh437}J| C\8y~w RVV俈ڋp>wV`ƗXP@tgsaFON*K7>쯍MSf(x)ByTn`/c5z20M)8"TTZPRZ>i^{0UOcB_&?r/|2)YIE}㩊ӧ1bvdCxݜ;j 3+]_F ~CyEx/u~ IߏҠN8FebY@Xy)%H~'F w ,3---Mۛ)[[o͔ - m蔎X/'w"@@ D @@ 'sZlHӐ9pv$Æ4J*IPs4k6,CTQ aS)>W|oG PY_vܚgP#|R=&cRı(WRb^ah{Y>tvsե?\O+[me/!"!"L/d{PS5# epo >Ҩ>~ w$HB6mƦ2u>9ȱv_I{<=޵5?{FwhXRR]ϟQd'C튃Jݎزlg+8Ӕ/UOBQUQAquZ4ʹv58Un V%w&7C-D<մ'ˊh3?z?<0fP\#('B3Kyq^iccq@Eَxb<0<<?>:0،/구ת* ^\a \}VN^Hu Ve`?{!g(fVr-# Y6n%KӻPVgt(Ds 趵Zb\W9~.9wzV~^廴^{2 =m\<'-|gO?|cpOu.__[@ҠRL3VBuַsJrieu+w<Ȏ %/+8S:N+a/ժBw;O~o=O8:(^kfh``UNJ k'8G8zOYQ3@h_"1g_BBZ9}&= }Ms #_cN^h+Q}ovx*h` bUt/SRbfF*tBDPW0a\|UZmǰ?w? ĩGs>-N`ċ'seT/> xoh7wW^~ 2nT$f5+l\rDeuA6‹9US%3Ή]l`𞍬+=Jw[0 e_OUQ] pmtYSdsSTgחS.*4OaǴT D)of#]9{䄈|Vܜzڼ#z.Oݯo^PLjq6qz|Xku~ %cr m޲<[IN6vO vPdd 34Ǎ;O1Xqd){>WMkSM=1Dz{~Wճ,八KbV=_#~| rC/v?|!&P?xs^>mi3}k$; ys-l䃶˄2jYWL9)n:l+-FNYa38 寮Hi Eq~ D䲣SxRF{gM-`FBOG^?2+W?ZTBqAe|-se~Q݃\?S"}!F|;~CbrAu˥:$_v_U!G7}yRܻ1M[}'#o@s5+X6w:"3Jjrho'=bVm*# [4Fd5wJm~<'E\+}^ů8o6_Sp5L{MqܧIǏ,'Wg7P0L3|wCxf\  + ez-cQV=y<&##)´…̥) qO1΅ =3_*OZSpg7 EO*y"D30E:v>| t,+Ȍ3p!#n/$2ς[ %E=O}IUPk%-{=L*yn^[o]VQU5;5[lWi-Ζ^QaJ6?–AYp7m܍I"~M8DFƔCqA Rgj?sq˽ PD{ڸ\$Osx?mA# p' Z[jBwxOe m`~*ڇi3l)t^|yN8x5:|z)"r@0Yfg╜o4/H$J4:w>N?8-B ,NB D @@ n9TX#; [/[A;@ _ ~JPn4KӢV%F Cx>qd@ _K#lFeh,\FJ&]Mޖ?֚ 9ouidge"2NϏ|$Id>U٬PA G˜j_h*YNwzٖDfzVjlnxQTƕ 44Oumg8^} T*df0<$}eW(dȲ̠A(4m q{}r2 މ9ikh@|RΪM6\d#eq< "IҵU fFFTma>LN%П2uiZdYϺCOF]vCjL?ҐMYE ձ PQea%IDAT[+tF= l=ONyJ5dnP@1hmLGfvsmd(#%e䘻p:rBu&9fU{@0П2UJ%X|)2x&plM8SWwwX՘l&tv;е9`,?v!hK+)H!K+ x1'J.ԙ`")--,^ztbts# (^Z: I3LAsU Ƨ>ĥ!ҭSC=bV L nr S0@!@ /!@ /YE^|ʛݻw-2yf'yĿYF /so1G-&% ;wIIbދ-?gW@9:|gn`?>HҨ^/2]aqgr'vrdouG3_U୿Uz2t /_x7`?H c*ko^}AIģ ,~;}@=&n'O@JS9u \t.g[;?G+^y<2ɽtD?Y*/h k'L٣|]H W@N:!*=t=$׹._wISqoW0՗ s>Ɵ=G|z߉ɛ;Zw̼ )),&33jbm//cg_>c%ƮNN3kxh,{mC (c察e6vɫ:x2s#<4H7(1*wDz N:N`|_cww_ ޓ+ 5j#<2ܯC?^ߴ'!}w%4*vjT#'m?6J?-KWgHYv5_^S{WѦQXJE{b? 4DMm4Oq=(%MOm)J$^5/26n‘ZB%N x'(CE/ȯcjo藜4p^o)=ɞMɡ/)a2h8B3 K y좩F Kt8n?j9e9_(6oÿn\B45ySB"+3ZMb8Ȑ{t1̽{@'C~2z=:TH UqA;a$vTY8~햔Dx_pY+A ExgIVSGŕJBqn?ZG"|Mz9bD _vb(/LwI,SʹC6 ňŵ_oZh{8a-*#13IόԵG͑}|b]˪OAWk5?8B~X` )ٖ!8>|Zh:,Xp+Oo.[);cheb6_ {˟y2G(ęL򊄇Qv%W;jb>6/m>@c95{@ L)2s u|@ /!@ /!@ ?:>+~&R<A_xq&Ǵ×T,Ył(%=]Z9J~Jbl&ǠBiTKIi *7ވ\RD \c\+Mް]po!l cJNs܈/29ům?ej}2#Y멉Q7kV0/1wF ~Dv>#R >k9vp/5m}(}QG'끄YVOad9ډ͏9,XSFſ]#q4R,( <>E9'+%w_`boc[mԉgu%,a ׯ@[k+Oҿ,Y=]t3L7R."9 u!ɶBJdc;vOTnرGbIUR  z4Ng^5s ^O)w٠mf~MSa)d)O? uQOD>[O|kPzZk(>J~M{+jl^mGx4vf~?{٫Y$?^R e8|bðƼuYnq* e@cՂmĠISII'_n- P3 1T@|w88醵?%[c{; 3-}euϱxni,N Ww?S5~1xIգ(7b6Pr_i (Wt^0~E@0A>84g2%u&R2i+^[C Wld%zVAJI0÷惓:BA[üZuuL,1$de,[M:ecb % dmF`&'>U>4K1E/bF'qh92[5*Kkظ)D z}HNV)}tՐ۹ٺgx&k,dNL>J qY%]cQK߰G+:;Cq Lq9|qi0;F7rSմ7S\YrqulIB$>?~u`X5_$ b[8Hnrr.\hf~Lɠ.]o /Nnqa*b=57<>< :Ttta{ @6Nz`*I&=hqr`<[%~Y"oEOxM= UCGD㜪]raZT5c@_!/_>fos?΁}e=&Ƿ"NG?g6j{&A9.M:BAшY7ڑ}Sw__dBVz. mMHYKps Zz-, I~]̋m³^W(d"S\%~`c2ܒ$Q*= 2x'Ha=$ repd >20%ht p\ ZOTa]әqk 9zj(g̼xp';Y8QK;kEd R[YihW}GƖJWSsv8c;t44 (? L(88RK9-O<)$Ɔ '!m%;V&`V4|W )i1i4~?_sEǶ2}aj£gtO>DqāruA4W˾ptɐ{ňY!WKOjGOʬܷf b mj}L<݇BCD%2w6~e5sЀjN ەݮ'=,>D"]`ULbDD$cf fsȄk-y [K%-:=\(Nyv1Qqƨl\HxP0)kش N*=MtZN`nJ2V6*N9 pp[6i0A~ٯE,&{t0o2/.k(>Kuq2tcIҖao`r Ŷu鶴Su WW Ws*~`գz[;usɯR}'"z9O<Yv`^\68qa?aV~/"wk_v}34Pq'Z9>Me&pʮ"X^NkBhc#e 9yj7EGՙ9tuB3F51܃(y1f\|v1 [:8_z}ÂNjQ0c:s,^ "1>2c!Yl"o!^ /pB@0)!@ G$i{~yIENDB`litestar-2.16.0/docs/images/debugging/vs-code-add-config.png000066400000000000000000000574101500564371300236310ustar00rootroot00000000000000PNG  IHDRI iCCPICC profile(}=H@_SE*v:Y'B*ZUKIC(X:8* .N.RB 4*L5Ut".fsb+  IR$<=||,s^%o2'2ݰ76->q$xԠ ?r]vsagLz8L,;X`V2TI⨢j/d]V8oqV+5ֺ'a(,sf ,b )QCXѪb"MqOK&W BwfabM Łm ݻ@nǶ<W'=&;.#i ~Fߔo5>N u"e{;ۿgZ r=-bKGD pHYs  tIME;WqtEXtCommentCreated with GIMPW IDATxwxTeԔI -$$ !.JPwVײ"bCB$RIi?2 $$pRVY}ӧXl6[o۰Vޙx?ѯo_tZm0[,h5fgBu05ek~nndB)T*U!YI4j5^^DcIJzy/<+~XA𗿽ʰ!C8r4ҲRwah͟;v5-z=9p0̜69`Ҵ̞1[SQQ/dҴ졹|2rr=s:/wO"o_^ˋ8990m6%{Pַ-!b"#ӳ(;Ӆ]":vO~Q6tj4mw{b4?N3D@Jf(cǏӼ߈9wOW<#7uy_dʕܽGy̡#Gg*mߑ >fuWyOo)**b5nxI>z_|٧=۔ΝYlX7***Zm8vq>|crrXWrvQϹm(Mm6_,/VD]:u➉ Cǣ9rEb]5iJX͔4N?zFCӧLa˶bٸoTeTf ww7DFBHn k(RRGƌSTS)Uut̨+M{"/󄅆7ɛrlۻǡӦ(Ӧ#s3B:ƂiY ,Z"\]] ūIj1 n)!RKYYTVV] աu:舗rAwwsS~*nȷ˖F&'7Z͹x_>3 Nܽo}fkqm+Inn\䐶V+*.;1lWU!Si2)5w7dxl’ Iu^}^W^;m6.^>CH׮UjֻlsuۥYǡ322 wm+$vl6 RV6'SRlt Sl6srI:~]KSH׮c4b69N҉N_f)S#t fc.223\ύs]?o\P@AA!/Ç&_{L~^cp\,6n&,$wOo}xzz޳6 k?TQvͺ ! kE>y>fCc4ѹSG{aegZO}}\O -/M@?!^zm8èx')*.f̙9%D{V74TTU%;fk]57UNlASg_kp9BJӷ^;!)#rc#Z!tc}$[:Z<%VgqgWLTwT*T5IncMBVZ^3 P}F?*[:,-6 [e)Y, ! -@r$RA!$$BBR!$$BBR!$$BBR!$$BBR0rRn'3'h[kCGGSh4Bvw5`̤y7+єuL5ӧQT51k Tl[I``rm%Kͳٳ+9}rOyxwX9v즅lFm:%&Kn^ü_@'MHƍMݯU{̨hfM ɌL y`l:J yy<t_2yD>G\¢":wȳ<GaQ!cFOxN@x6Z7xy ojZ/*gүo4xxxOI[޽•cyVK/[mZwQk5(ᾴF//]PTTdsۋkW,W­[׮,Y)[6qߌ<+8x/>^Z:"gO:vClt!~.!g|;-Fo|KYY .7aXh4L:GΞ;?{6z }}|(..TJ*iO-2羙lܼYy#w~GXaCq<9Fa@L b^g߇NE7p'O^h+6o>>W~'{Sܩ~XAn^^n׌&_xYsr+cޫ]2+W˯&77FCvN/'77772/eRi21m֕dG#~ρd6m9V_yoor{]<ތ {]BCR=< 32HIMۻחio}eHԞ=w7~KXh(J;׌ ^GS%<ŅZՅ|URV6«>At~>J#JvAaJgbZ"oy#㛷.!Z..>NVclqVn_?Lz:;o-#2//O_2689:ҹS''bFF۹zYmfٸy O9FA^ CP>X,NhRgPPPHaa!,Zĸ1cݻk_@ŽJ[xϞY/X,ۿ{~lr{su L&N޻npGQS5++g׷/.łF!77̉v;~}Vf;(*pEk9rw$2luN$8AP``l/:;lR:1rp6ov%)X"zl6s)zApE:u숟2||zK:r?rrr9bϝ?k'׿ƌnt+R%-vŰCJXh(lXZU+Q5 WxOEGq*%̣swTVVN+){lnNN\}S鈌`C@Uپ}ew_Lkm4 rٷ9z̮|>l]<^}'O6OR WJ5YYٔӳ{:Gٙ)wO>BJŌL7QeIY%:{2Vg|YW>.1r@\Cѯo4ƌfږ?tf/7*k+VQٻ*[cȒ2Is,)g{\ezO3$o˗ ҹ3^W(**"541#ʮ*#YUk7 T>jIR WlNNNL&*++..zƌl#L&TUzGUTDFD0y㭷Y'RףOd%%|r(O~_:t{xҴ>d`6ׯI&pkK){HSv$>@uW'GGgP[6+)[Sp~ܻhpttwx8_KE_~"Zʵx{yEf>̆M8~lMJ"9rhOBN&%a4 %5M[V Z_- I'''f͘G,J~2xQ}}R+_?[kWS?=w/oLݷd3y{<<_Shlߚ?{Az,}̞JY] 7''MgؘqQ\\,_S w^S >֔]q#CGK,[_{NЛ.qDF0n(KbX;~BM܀$$&G9rX,Z?]#GhSȯ%&R⎽ @4w*n !LBBR!$$BBR!$$BBR!$$BBR!$$BHH^ !B#B|dQQ_} ,ѣqhݾt;w&'7Z'bcС~&L߯e~GwҥGX Vuk+bU\jds^%((Y3g\\qߌrjϞU<4ͬ\L4 ʥ,TNGGXFKYYܽL&_=zY-9Y5Y\\LΝ1DSg#ܮ]7gJۦlQq1D飔7Ei߶};^FCAAa!۶o';'g''b$,4T DFFrY*++ !C-BӺꊻ;_-YQZZ;V%OF]/^duTj?G~CuɃDt:bXiV  'GG jR/`¸q`BH=} 5b2"nl]B4vNȼtc,-[׮v䑣G9{32ط?uϵc2ZPIYum-b0شe ={WUCa8n7ӧLAVJRҔcbؽgOCee%QQT*:v@N^He۩cGrrsѽuKyzN̎u림ήǚu븘LU`ɓ.رf2 V3gXj͘q{TKtsue]wPb4ylfw`j3 vWn튃KK/ш#X,tfZ~mM^ш`~Rk]ץVhu;w49iuZJ_gcİaX,;dRjM]}lPl,!ݺ)S/ xyzIӷWIY^$$&^^xX@hWZ/**¯zTYTTd6ENn.EEEVpy`Μz/W=}())t 暋x`h4bٔuѤ掌oֺĝsFu SiJUC5j56mPR#ef3KJrDauz_ط?LRRS>:xeeeʇ:U_+))!5-5ҭ~~Ʈݻ4Z瓕er؞ΝBΤ]t:|ʅ9w|ۛfKܾrNވd2q)JU%e'YSIzXjFGGG:wZŝĐȶ "W/&Mȶa Ό>\9ͲeTBL~ clڼN'Q}(U*&L q.XՊq*ˇwչf֭$8ݴ@V3axlʏ!(АeX{n)䍨|QkZxzx0~Xl,\Ȕ{EUk\-!-n ̤Fٳ\pZ_mfnKƦ._uX Ǝcwa-!0Bn !BHH !BHH !B I!B I!lhϱj]L#:vd]wYWnFڦCъƁCCπZԲmc! WJ~Fv## %/9}rf:D] 'Kt:/\ Inu\_V))FC!]vP9XRfgggƎʼn:Vٻd+*pwwwƚR)f:vh˫.hb6ٵgC[?4$n-|wnm4$kplsuvvDr2ϚEEh:BUqfёɬݰGULfNZFRչrFePQQa7bRޔUz_Wm!qf6n ChktTf\pwskp_pz\.(>}nZ))G+nDyy9:^ez7tm!9f(ztNVv6kׯh`9W_صgt fvj;q$G Jh\iwnfEņH9Zq#1TVV6;(z7tmmJE?QQlOLdԩ4XεWx8)++cӖ-':#1iS(ŋVIURV(?__8zM砾SCnU lGo $W/.s…˹瓙jEףPW\]VG35җRVVK|\;w"qF#-!Y撒mx$Y{ޟ~Sǎsv젰FC_]ʵX׸ヷWhjK)G+Zdл78xDZ-i볹l3FRrBv@ q /!hLBB!!)B!!)B!!)B!!)w -@ff !/$Hi IDAT!LBBR!$$BBR!$$BBR!$$BBR!$$BHH !D#Zb (*.fu2(6Lj3}\L7??y„h+!y*%p^O]|jZ"//V?bb >r f͜ g[$gL0.cU!JHKJbמ= .tĝ;Yb3OGѴKJ"q.ܥ :.| dqq1]:wVnwbaZQ$Gy Rwl6g1$>^&ϗ,!v"zf…DFFrY*++ !C---e\@է} .$:*ӧ 0 Q#F 5#wo6͆Jm۷6nLjZZN@YYbcv^^n@qrt$~ C}aeNcPlSRS1jj5QJI 7/|j'"4e٨H4 N;[g@Nn. FCPPaKsuͺsr$:* ZMܩSit>h4T*pvvFVѫZUVc6b憋UBFl`2899)k}쌱TgXҥJj[] VKXgOM\ +yJF nuU8y#GPZVJh4R^^znd_}}|cמ=58=BHH6/]sMLz:W+**¯zWTT\suqA9sFs*5)qkC2J2 F'@II v!,ȷrYHLdڔ)ʛgͦ\Bh?7NYYlaO?qСrf n7+ZbcIܵS))TTT2kׯɉ=:bf֭$8K_!{XK777|||RĐȶ =ȾL&Aaa!:пv*_j 2{uNj !鶰Iii)6gp|.˗/v:,V+cWgBt[!$$BBR!D7/CBHR!$$BBR!$$BBR!$$noI=c)`hSoo+\꿻ބKTZZv[#z:` ~:MV2L,YыZ} ˽F,64WTZB`2V,6X~ ^דi}B7ѤH'~<]AAiJIa\.(@ӭkWpttWdҥwߌqN$'OpF fJlƙtCdDDI5kIZ\Bn챤$vKKKIܹ+V0st4MَjE 5b”5矋@ֹ\Νܹ\|WEPP˕i,*.>}j6ܥҾmvJKKYf j؁ уRŌ Z-Q}7*  ٳg$(( 3f3aH|8880a8>_ɔWTv :ѾDGEl@j% j sl66©&}7NNN899ʉ'Hb0شe ={g7>l6#sRZV+WNpp2>u*֮ gPQQ<5t:]9OTVVZ1i&bf&3OQ-ٌr'Oj@oj̘6 GGGEXj.G~~>qqhZ\]AJZ>jj5QJIs]eeeZz5m2\]!.\V% I}FJWؖǟ~ǟ~ʷߤeL&2멬]$kF [e6MEE1Z]o ٶ};998;9;p` ]Q))۔vl}QwOJj*=w?VUkIbx[]gGqz$-.)[H4 N;[csrs)--e`h4 d3)QR>GZrF\ n]uZѸ)){ۇdm]Rz\M@шjUԢfB\_Iz}K^ンbIJj'tBPI٦ۑd[()\ugDm'cpvfHVq+)X{KBqB!!)!))+2B I!B I!B I!B I!B!!)`…۾/^W| 8ruRR tڕ8P)3+ړι+%((Y3g %eJKJbמ= .tĝ;Yb3OorE!ڃq΅תQ_\\LΝ~?l&O:ۥUnuIY̮={<1L7ϗ,Dr2{+e t BEs52/]bKKֵRVf=a8=s縘 h\(//d2?)){[]R6;' tt  D>G!ڪι)L2lrrmcF.]ݫ32sUB쁃پctؑÆZ}j5%eOZ+)[5%e V5圻-u.HI[XRODz=`$nj|vlں'puq(D[ԔsFԹ %eo"))+mB!!)!))+2B I!B I!B I!B I!BqGdrBqZS8pyyyhZC`@ a !ڏJή\gz=GHKJ"q.ܥ :.| d]W}%gF N:9|3#ǯzdcneCdU*Um-FW %$`Z`رߤZTkIY!h B!!)B!!)B!!)B!!)B!!)BBR!H )GB! @9B!m!B I!B I!B I!B I!Bq{tDHMK#]B[ok 6kiOHg m7Oo%MzIwoһW Mlrs{plȿ|[y3Cӣ{Xɽ c'MW1 &N枤WĄqc)--ˋ/%ylFնO4Q4Ѯ==_ *+Wҍ)L&++W o=vppP찃YwO2dh̜eRxG<|${iAk_`Lg _RjqݓH;}ng31F?b{4lOԹͣǎ1vdvQG,`cڬc2ǟ;:]vMxQ&O_^~\53`=SXzMPkhZ\.(`P\::ܥ ǒl_nAX,<k 38q$}qcFy#=k׭[l6O=_ >o[FBFXZZʚu뉊|'|F߭kW,-ot}EG~U\8r KϿhXq-e_/t6l oo/[]\FMa9˽=CDIry珡7qcFfz''Sb4sh4 aPhS)>888Ȝfqz_5 AFzZ~o=^JjtƍFSptpʺ%//¿yQQvۙ3kNzY۰cWg߇NE7p'O'ڴ[շ鉻{bp1#3͖-5@N&''?+eFk˼IĴYsL&=j]Z͇I$$&ēOW_w׿rkrsh4dPPP_ossZJfeeTCoBR5z#GC?vl5Zihgϝ㍷a*OwiƫoɂExG얿AxϞ\x?_F]*h{puqviS,n]ovxx{yEf>̆M8~,={bpvfbo~vWQl CP>X,NhRR>6f3eee۸t Fܩvp1#ny//O~X/kl/_1n̘F]syyyry9+EyFȞ{ɿOs$hx7y7X//?~u^~5'1W]H6n.VwӦSYi"Kg~^^Fѩc^zzb?v,|@ztL!G>|د~F9?f ? =b$>4IǮ9~#;?xwO0}9EuL:l2ѡC3NmNÆ$n9~?xPB6{ACcHܵ?Đ& !Dn7LYTVW_߯BqGMBB!!)B!!)B!!)BBR!$$BBR!$$)+u6kGPPpb۰9=ۍKԀXb_>NJjjz["VU-riIjZ~yW;ٸz%!!Ҽ:WSExʮ*wC9sfٲk ר4+({t}II>yJX!.\K{j4$3rp>Y蚶J׏aZl6seGKD2^m~ܷm$~s~Xۻﰦ 7&p ȲV:FBd(۪*_]:Z7U\8+QѪuDP* H j\s'9_'}l/+qM\׶il|d$l$'D?ݭea'֬߀6[fmecs"#ijժ|d.]V6-<&#b$z7f4,+NaCTEi'N".>CQZ5pwu踌%7f4tu˾GLMLЫ3lۦڦ. ]lԄ5<}|1+ #b6[cIj {+@~ivvˎfMJJF>}I*ykk@ժUP͚C$y>sвm;t*Śu5-TƎGeFًU .R|-xe+V=ϫ^6k7#b>L|E9gDD,DD,DD"ID"ID"ID"ID"IDTa !!#ADTZ4+m""I""I""I""I""I""I""I""I""I"")ݱG/\֏[qcxJ)ygi}nÇ1[G/W7.q]s`bbܤp Akס tEëX$JUmX;{>jOݺ'F?2}TaᇱyzؽoĦ[= 0e*LML;6Y]{"䟥)0(Nr2:})))ot CMl–/`~ 8{<&NE7_۶Qmt 6]HLLTSnn.MfMXXf}?>ڍzfL!V.Eȁp愫׮ ھ'+PȘWHjdY~CvO~-ָv(sD"H$B޽ڽz >ð=to޺;h \~HFBBhdeZj!8kY>h޺ vꂠ3swGʕ⏈a\\͛9+b1b1nm  6(ֶ j2ӣ #rrrq$ƌ=;äf~$SXlݱ]aհa~H _ޖHT㔔E573+a&ի6qcFcϿԡ&O5xdaժW{w6US y?M}1j{TVw턖V¡˃@FFnܼ T\vvqRDEGC⃬)Ţ%!E+KK88IRRPӸW% =ѻ3ayXqN+.LT";;[Sڇ+ztuQ#N .>>oEZZ:ӱ2~}Xc\܉[ǎGH};  . yJ%?pLU,J̫YYAanf ޗ_ǡAbAET ˗,ާ/rrEm1vz:wib/Ə]s33}@[[ײߗ:-_۶Q;3C# HD"pȥR|3dXqDXt ,Zm!HYgy "ep1&|; Ïa#T֮V-m0l7> h]8!vH|XO%U"m""I"= q&ID"ID"ID"ID"ID"ID"ID"IDD,DDT|RFj>9"мHY---TGݺu$-}-hYL_5g6Ο:=;[mǴ?ڝ9U_-?"N\8'ƒQ/1cONH*sDɂXUSȤXx!=+1W-J e,e>vI,-QhҸj{R5])S5002}B~]]]/e'`->dpD6o a#%53_Ԯە`xyzi4)))H$}eSXѲ3||IN,񓧨Ʃ Z?-_Wb0zԮybѲ=t8>' 3~?"",z^8,222l++Ѳ%dfիVV)e-K7nQ\lAh-K. bU~QRl3K ^:e-KN1RhY &"b$"]Oe8$"b$"b$"b$"b$"b$"b$"b$""I""w1D,[AmY*3^P 1쑓ҦC' >nƠ1,"Y8խ~13K,oIA9:w7UmeEsnێt ծ ̟jnfKKK;~pyY&cfJN䂹>&JL &MdI4x5ZՅz>I怐Cp愐Cp0f`,{/@~dj&MorE.Uj+V"1 6o\1D1f=~pSK^{=WWuE֨V665w,qe,Q*zzzpsqTѩIIާ/:Ie=o22)D^B >Du;ٳp|P>52~]]ZmۡCW)֬[QlIlde5vRe,U(%%p3I""I"!'%LEEEEEEES#QiĚ#AD6$$$$$$$$H}EұG/\V]Yvu~}{ǰ;;Euq7Ov!#F=rrr1ffCnlѮ m;v"77W HRxDFGCKK Dx 5jv0g6Ο:=;[mǴ?|TϱR n1Gn g|8K׮ CCC,q&C[[F s7'#QV-L4llݧ!h֤ 6i 8wU݉Ys޽{ia D{҅?b``P޸<| #5I$9|dR{MBb1LML`*5AkAsM7BJJ .Ź Յ<=T9L2Cڵ+Mmmm8t0FFbeǘܾsǽ15j/oc~ؿkg"PŚI^ 7`báGW]ÁAص}+N;+W1fL!V.EȁpOݺ'F?2}ݟ`8epprss1awڹ" A!k/s(X&MFMp,, p 6,-QL)dž5k^DSXal޸v7i?~S!#H8菐05[dTl\};J}}U"b1b~׮HYYy ,,abRq9@"@$o^՗h$$$AjFVVUבo@$k6hӺHNIňC ֐IvXfld ܼuwbc1с<"= 5 `` FC3 H$B-ֶ+WV__{cg:::HKO4lPƌʟЩCL8= )Dxr@P0ڶAKd' AA@RR2LMM!T[XXnk/$46V- wbcՎIFՑ89 \^+ 4l@iBcfj'Iɚ#)5P˿&&&k7y}dԻ3zpӴ4̚;6nIQIX !@XYYY8~yy;rr`llԧE*ڋMߵ*1s3Ɠ$(JUOH@Mc31]q`cmJJV* --o?~F6=]](^~c -o ؓd abb"ԩQھQZnXD_RU@_ObQ~04^.={=]]s/@%v 6-FőTŨ]_9}!$X Q/p;=7 !ORD'?r {.h ׇ9 ŝػo[tL*-4{kSg>z'YE_۶#77.\3iܮI4}};*cZdϟi @SS3$-jn)( * 33> 3z 9s={=\ 5T*~D"XXXR"DDDDDJBUUJKKQ\\7>A{4&&&ppp@"@TrOQ%P( 999(++s]AA H3`'"""NT D"A6m`bbRol3T*D"8:: WDDDDDM$ARA9uE,,, 5hŐH$@cbz155mUQkR`jjW`]M* Q J^o8DDDDD)4͉U/BRdgg#&&ΝCyyy Fǎz'}sEΝ_Z_d \]]q lݺsvv?oc`PDDD… ,DDM ڛ{%@D4 X 333 Bhh(rrr;lllPPP Lwtt+***Xu{a`y_(;wvajj3f<ۗs:u 'OÇ%222wAdd$~ʕ+ӧi>>>8w|||wvvƸqHIIÇ.#1vXR?Z~=~qqqG}#55Ő!CsϡQQQxwZ=1.0DDC´3gb8w6oތ .`ܸq8pw0yd9s۶mCNNf͚WWF7&O$lٲ.\3`hhw/_ ڴih߾=qymmm`X[[#""~-ͱpB8883Ư["//!!!-M4l۶ F=0m4N"jNDߖOOOx{{cΝz* !!#GٳgT*<|Ŏ; ̷l29[liz5 ɈҪlTz}ܽ{>>>8z(|}}q- R)BCCQ\\ HJJš5k0l0ٳfff4hN:%'&&{8<;}2|h6NDBuu54vvv´2!`Rhk׮Q H⢵D"AϞ=qe󹻻#))I{T*ŵk4[Сp%鉉 /JD[ډ 000:Q[[[ ur􊊊`aaX,7D=*++ݯB`` Fcc:3332Lnee7jZBw}WT* v"Vk׮> %%%(--ņ tΟtdiib!`@;͵W##FmOQQ1l0\~:+))dee%|Hkți} b\z׵cCn(""jM=GO6l޽+$(>>Fvv֧R1<==5_|Qck,}H$ ޽|=zhv미y&Ξ=[>>dعs0_||<_3g"22iiiH$ptt6m$[ZZ)Sd8p 7\v ~~~:t(.\{AAAP(wq̚5 'OիWaoo???TUU5za'u9s z{ǏN:aKQFHIIA׮]ѧO^ \v w܁' P:ѣPQQ;;;tGAVV /1h'"j- RRdeeѣ8w51tP~~~Dvv2 iv؁;wܻw󃿿?222p̞=[#7n`Ϟ=>|8z ڵ GYYYG~~>6n܈qaʔ)DHIIAxxƋ9H3f@*"%%̙35amm)SIIIطof͚UzDFF"??C T*՗999jܨT*%ﮮJ5" , >DDl;w D"–v"9$#GDZZZo%"/DDOB6mڠO>033CII bcc? DD=B-=C>=1h'"""""DDDDD ډA;q߿\#""""ғm`K;SA;v"""""bNDDDDĠv"""""DDDDDĠ1hDmQ3Y""""3߁ŕ0bmncc#RT*R>JĊ}> $?sߚD"AṔkaaX HQOWBR ! ؉UP(0}^3`:v5+p=1hoÙ DDDDĠAc J ӂqFo'''7&&&dvaÆqv}d2dggkƏdy ډa4ĉ'pĉg6% K` ,͛/<|o Э[7 ##@L: 'NQXXڵ !!!2dx4..֭ƏΝ;EEEQRR"vZ$$$ @ͷ-_۷㭷Š+PXX( ˖-J .q]{grرr|8lmmQUUT|gdK.cǎn޼u㰰y`lllڴ +V`I!"""'Fꪪ9_J8;;#??r8*32<<!!!,QDDDDLpndEff&D"ﱮVLDDDDtcNDDDDĠ?؟1h'"""""DDDDDdvSTw\%"""VAiLwrZW3^3oT~W zvUV ̏Ά9ǰ i'""""z1h'""""bNDDDDD ډv"""""bNDDDD,2x\ ر#֮]UUU8qb޽;\\\}A* rssXtڴiرc(++-QTT* ŋajj+W6{SL+Yf5innnXnfΜǺO @Rik>Μ9 ܎.]`ժU2ePԬj[t)]_Caڵ믿gذa(((+Wٲֹsg̘1...000ĉf!,,?u|W+ 30@@@.];w5{cɁmۆ5k >>ArQܺuKcJ!{__ߧ>h gΜ<<<0j(bzo?`9sO4/n߾vz2oF=|0`ll ___L>m¯5=++QR)qVVRRǏcԩ裏?.|S|$iggg  h߾=RRRx4>|}}qEfD+ѿ… ZKJJ?~)FUg(矺D000@UUU(kRؿ?ׯ)Az~ ڟo&v={`ҤIpvvFVV"""p5a#Fxxnܸ!tIбcGD"o---7`۶mԩ^z%TWWcƌ: rrroȑ#P*ƄpB̝;W%A"`۶m8<h"J&L深9sbCHR^ɓѳgO޽{سg5泳Cvi&]eժU r9~g6, 8F / b׮]VS޽ľ}^-fΜݻ(((i4ʘ&Lyyy~4~~~h۶- j65u`t^]^KKKg}N:ƶm&&&c;uꄕ+W"116mRrJ̝;Wukʔ)8r/^ ##:woRb„ ķ~ddd?T*ŤI`ddfMsssJBDDѿ|G4NN ̙0$&&G7o***H߲4vXL:8JOOO _}U̞=ZH$ҥKaccm۶A&a߿2퍃ԩBBBP(?כ ˗/P(|ui_nj9k6xUUr9oY%Kaaa(..<==eM:}Axx8`iiݻаKOOGhhVkVRPZZ1-$$ݻŋcعs'^Zo}>ppp@`` oߎ\azhh(RSSܶ+V`Ŋw~w`\x .z-XsEEEeU;w~dH$XlgW_ŋqzLxyyO>۷e]vpppؿǏGNNꫯbڵ?P?[볼KлwV{ִ/wưa'NeeeP*:deeaժUBX||rrrx>LGpǎׅr@8qz%sss#227o_!!! o֨r۫W/t˖-͛7 [n8Aܸq ©Smn蘮y׬f`cc???moJ9k6A, c]FFFA޽qEv}ׯ8$$$ٳwΟ?-kM6l 1]=uCLӧOcعs'*++묯_j9ֻUWWBc3 &]!CuEE P]]'8y򤰟bbbгgO <sXoޘG}e? ?**JGٳ'BCC5?tN֭[M6a۶m:t(~'t][s{QQQäI ڧM,[L8nܸ1O \zaaa0`~F -Tʪ$2hA}u\D-??_(Vכ :vkt\Di:QdU/IDAT !H`cc3s„SNaرŋagg_|Q"lH;w"ݹsgir\htEJf}qdpqqxX=ѱcGH$ܹs 'Ν;;"&&F'ݻwG||׬ݱK.ER@ׯ# NNNHOO׫,yzzPV݉'bA]cʭrO.]ѣ5򦪪JVիW1~x888h3sLa5 HGSYsF4ir vQo}dffbҤI033CLLV~`Gtt4Қp:9~V+gm5 KKKHz/7fdзѥ[n|46w}m۶IU_>]zU<;w4l߻wRuD՘Gs[Ce֭[nj3p%$&&jnݺJ¶9xr9RRRͭѹsgTTThOuz7acc1ݱwza]vEpvvnT=_2jccàq{.Gh Ρjvo 5'Hw~Cf̘}"""(++fϞ]-ڕz|J(''7n!CpE <- Ȫ[dj旹nܹs>ufM5[jN}nuҚkV$쇆XZZCطotX,|J%z聪*zmmmWYR>D˸wbbbTnjOT*ZD"pW6tL`ٲeP*ؽ{7QUUٳEYsAΚ!u]>WƤI0m4!##{ݵkr90ed28ql37kn~43hFrT*>|-ۍ!Я_?:tOwԩEON… aooٳZ{sZg bJI7^_uTXXQ]QܷUUU:t;w$ヨ(inQWz%leiڵXx1>#YF}UPP+++ku*))ҥK,Ѯ];mVk,斸kmP333'G}#>999/!о}{bxwr޽w#  qy%ꙺci?+֧qi>}oH/EEEY{N׵mllqi\. DׅzT*UewP*ظqFx} -@~֏zf߈Z%Y}Ν;#++ }Zt{\Lw}Iͫ˦17oD׮]51ssst޽FKKJJB+1P*BwDtD۷:%F k׮~0K3/ѳg:+(**Bvve)11:thy˗LJ~(} sss2H___aff;;;yӔ ]s} ѣG+gwtt|bRpbnjݛ7o/Vʪlyy91h fMFFFagg_lA걷wIμzj-NOO-^}Uܻw%%%®]j*X d2ѥK$$$|iC} bcc1bܸqr#F%H DJJn!ÇP(pm?z( +WD">|BLǏcԨQF^;v,N>-\;v ~~~x{nHR̚5K5E2$$v킱1BBBDԎ9W^yk֬cǐ333to 3335 $$$gϞ8p v%T%\cԩŸp0zL\\VLXV‡~kעJe]z)))x{nFFF-:ׯ_˗xbOHJJc`Μ9DHHHM߮%C?qq j*̘1_5d2~gTVVTfΜ͛7@8oS.?ĉkkk!%%E8! !!ۑ6lɓ1n8`߾}c[~G/2ˑۗ:HQK,uZJ?O̜9fByy9i<|޶1c`РA@yy9222E]ʰn:L>"oĀZ5gO c["+Wǣ_~6W^ի#q4Q# ,0N>.EEEXt)&O //Oxljςyh &&&bذa0`;&Jg}&>n8!..Nhm9XnFՖ,[lo˗P}՘:u*`jjtlذAj1qDB}'11[lA@@BBBbM -9%Hн{wUUEMjR>>`@O԰ 'Oıc4_2}IXbT*U|k6lyZ+ՉZ/Z#Aѓw}oֻ#̄H$xO=6'?޴p)ڵw 1'3m껄4ono˶piۺi׮'--ڵj3O>meػfמ=L&B[`0HKwcSkO#%%ǎSpoq 8tRwϒtrvڭ>#|O겭/3V^;}K=Ê?ƲJ'8{7oQ׭ob݊?sHyT \y(c| ~q wi%B1"+/-*œ3g~^cW#-V^R-Z޳{7M7K۲kȨhN=7N#0FiVu錢(3fk*'ᎇ;mZqF4j^wTdTǨ( FBߓо='OE2_|~#eܹ;&z}>kubP11oxpʽ2(g8/O8vm9"#5= 77RI}l1Le {I3.ő˽=RD'ok]o/OHfVVoFTuYx)ItZH)R^9ƫ=]}k /AQC}{zI2)$81חՈm6zuЫgG^05231:+S+/̛5U(1K % ܊p%f|5_4{?6u+??"]+t;&&p%|}}$X;~]ZoRѨC[ZCuؑq񜌌DQjr)E!>!cǮUſ:5jЀ@LL, gΞ%RK_WQuJ #Ä#Q;vnKz\y(ޯ r%%T~}{Mɿ[ձA_VϮFݻtf4iԈÇ`bv//OB7CQlE~m ?QN7_|L̹f֩#>l6[)NE˟U_ֺM7o^<иQC֯zݢ-<?~{3ϓNݙ4~uKa.eeߚG(Wwl:uj+ZFOg~Hd %,:`[`)=wB,nxy KPfywfOpu T(pH2AfUI !ypZtuh4*.UQjQQmqFRQ iAR_pTj%X !-X^}GW`Y$HӬꞥK!M %:h5,JWML2B86Ew݄TwogVB!*K!YBR|SҳB;HB B!R!$X ![Ԫh䧿i_?!A[ZZ~ZN@VI4`sQ2c׽{c~43( g-b>[{LoKr|^ܾ hQJx{cGuyF~]z79>+&B`#Jn mWXֵ <םuJɒ$n{^q \KaVhf~zAz<ߛANAQ9-lU~[:d]n k턻Ww}]|℃-q%C׸4+qiV.^2!_Z8kE+=h_HE&~cRP߁cqwҲxOŜz##ɔ_NVO<\2dz}KW8kVAf9j9|1iR7%DMeƊYlJe|Mn^8ɱ\Ҳ >ZJϦ &Zxhv"6Fufh/F}O%o:F95/wI۰ljf.]ZX3\:o_L|Qy~@'x3+?(cYr=&Bl ΰ$- pq۞}sL%Y ,6{&fޖ5?{@ֽwS|d-gr1xõZj㵩jyg'o ɥ3tpEUQs\ۓ2*FlAz : =tp*f-kuٽ "=>fwyIRlEޔ<׽-!jodVpy黳2h[HND%TR&!Ɵ3yy::^?$7A)bٮQ^do=t96&|_j5]ljy3CGbza6+upҒkU{Z \ͮ+k[BTl_ʇ:F}=zcL{5$*.F-f We&{ׯFi4;~nZ6wdXo6xx3)V뎳QN | .(ý]pnn?^x#T g9l&3WaR'tks+2%DMd0~Z<LƎ숪*Yr4{զ4dݱ,~y؏T+D'}㦤m^; SsI5h_n.J~,zNù$ o+o<|qwҲDsv44mfeGt i$0O l}/OO:wț|Jٳ__uو1[5˞ 8x@ Ό?ejy_h48;ӠA}Ąt5"+z~ŭ!2*` (0;3 Œ/C|uί(tQnW4l^ 8~ࠠr׷lh2oXiڶAӼY / SEh4j=/,5Z zZۻ77o) p]nr|}}g8;;̓PMMIKO'm}b'@-X,+IBB}..0Ï8~_|1 _MQ+8 uj38qV-[ć|}qrtd Lg[[v;q3`>su6<@Vw ]Pڅ-,]5?O?aךuoXxUoZyWhӺ׮_oqr'ysjתQ_}@.}?-|7O?l"N<Ŭ_صʋƠٶq=w/׬"lKT~ΦyӦ%ggF c=Hj٥X&3@yv?bђ\x ok*m;vfl /?|֟< z=.:rD(};8}h[ڇ[־}냷7}{VIyG0 oʠY/霊"i^xi b=Y 鉧= k֭kN dɓtt kvW䵼mʕ`h4Vxvmۖ:l*{{v,s7nޢh._Bb~~ņAA ;c53pm6iVd'd|P` #!_bsPvW䵼ml&77եԲb%:t0faayAۣ[W7j?.ԯݩCbb [bʽɧё\sFJj׽6[%K mF}!}}Yd)~ԪUh5lbQfLl,~v[EWS\thuZ\YR'/"$˗2C|bbI8_W\V޶ ёNwr,.j4:\]K|Ce/l7sF3g/͚6e\xm۷iœ_}hdhh[aKUz{{q 6ۛglp!m:6{, q߸e5iB`f|XVOHDs瑒Jjj*?Ξ͠ԲҼ9.7V+{c.ܖޓNGn#L&rrrbyى+))Z/'0͑N=K n`ĄqǙ,\{x_~EoxaۓL"OhJrr<|۟?,-Mc' O7 $ {{{u{&09;qy/Vˌ?"!!ck IO/L;__<#ƎI!k֯g2wǘ' 5Jܖ~mZbP~9?3_8z.;-sY:|aCXM4hE.קOj2.nZz:لi]_ZeV233YjZΝ:Ҭ)lٺܩM7V[ܹs]=z, @Cޏ亹憇 -".>233ٺ};bbmӆvm 8¥K\l7n\l_}-=.c09/VRR6o&%% qrt[׮vϞ3!Ymۖӧ!(0~}=%Dym@ˬ^Sf& 4(v\\a}؞l6Z?Tڛ 憋 6m"ysr|ݫϞ-mlXƍ1b0Xr%|2cG`?p:]/??݉ϏkȃSş˗b1Ѱ| gXf [m6Ʋ|JڶnmWyI,YBdT͚6m;Oym@dTGB,^TΊЯԯW-Zp)&ʦv={cX6%^gܘ1?p۷Nڵӫn%$&IjӤqcNFFM\֭ ߱BŅHNNfQhZhӪjlص{]KHH 77жmh4ԮUu]^[:kHMk[co k^-5lPήǪ5kN#F6mc^^^jǠuV8;;9} Vqnnn.2L&6nĦ-[L&..nTdhDgggLdёtV+ΟYmb/Blsnnnv/dťkmW^^Otwnﲼ6Rz2ѧW/V+b6>ڹ36TNx{yKӷg*\WZ-

Z2Lb)QӋ54{iii^X{$$&FpPZ{'M*u8=#aEFFF `P/9C~-L&Ӯ+k[QR%Qh4;88&j.[ҩiiw/W`XHIMhDDlNN|}qvrb}XVbbc*vdee7Jfddͪ5kh԰!]5l$''W|d?___Ο?w9{ܨ`Cl6.^ .ܖ}&o2fN:E)/nıc=w%ܘe{F4XdёkӽHaal g˶mtڕV-Z0lPlʏL޽՞$@&MXx194jԈˬdžٰq#///ڶi&h4 2;3w.V OOOt꤮߸qcG{w6lL㸹.hZ ̦͛k^jӸQ#uQ^y*q[eO%qnٶ ͆'$R-gY9jl+V~^DW澄e<(&6LEs\xE>[^yeKQC\5kl0pbW澄w0\!d.5K!`),BBHB B!R!$X !`ySH]q+ƒ Z IDAT-S6ﺫļlC芪IJJB@ǰ04E­ SѦgdvzv}z)h_`H%LG#"߹ݺQ^= .^ɓu\ZYI[ÃeA*Zww5h@jYYihڻX,دO(1={8q99xxxpW;eA Ȩ(, k*7eu4X,vMnhղqF4jذW_M\>JK9+pkx,3g@QR:;;s &OiiB^{ƍёc'Nz:2E=bc?v,ZFS6!itōOH ''XJ}shJƯ­rƍ߸ztPfжmX,$&&{(zjтJJ ~iSi)$zFc-:'嬤­r@~4kڔxV]KɄsih|}֥ ;w&%5ӳ{b:~$&3+ Fd";;[-w`ȲH]q#1[,V8`uۛrVRaF!0 жmθѣqsu-3 mZņMճ5MIIa{x8cFROsB6֔P!itōSQQG*x Jʺj+rV}STv)}iբWpbihfa41 h߅N^3.ٳmgNI+*ӭKvIıcL&rrrf˶m>+JR=ˢ֭Z←Svih`]SЖni&_www|}}.4J: -[^'0 aa>+JR^gYrZ$Kr"$5dDNA&itd.2 B!R!$X !K!`),BBHB;>66V΂B,,!D)bE0\!%R!$X !K!`),BBHB B!$X !˚"&6mBm~㐑8v\^,!*R^Y™puu]w3OqS|æM̞3(hղ=0m۴ڷkǯ?NZܴxcZQ=/DMY.\i?g7\%77zђ%3j-^IJŋ{\yێaݺҬYbF \BT@ ɡs9B]Qcx7z4}a¸q܉d]h(2$>Sۏ#'N`I<&Mbͤڶ &zl >͸ѣ)F L#"LJ'A qtt+Wx'oDy8{ۅョ'/<4tc݊F?3ya߱|*RҨ[6"ڷgcR0 <6!#Gɇe>VoE7ޟJ-UJAp*MNJFF5eO?^WEQ < lVf/(8SQElʔQ>Jvvr)&F1fu{J٬L~!e5e'==rŢշSrss}(];q®}=+K_}-eί>z󜹊bQҩ{O_~>]JBBx'*_~ϠϿ(6MQEYfX,e%J,EQaшcŎqnX#S[IJNVEQ.^\qٷkRV-ʇWRRqrr2___RRRǓ&Gף`}P Ϫk+;q &OB1,]:WN8шF`Ƞ;z;ȱtz9lV`"n[U~Ӄ,L&5311O<>ޅ7x9\1&ff͚=(rpp %5xz'#TbLHLϯ>8(ӧ+k/Zm{T@@@ϩoFW HLLB@JJJcmִ ?4_| ΝWϞ 2*n[U޳ i777mTlyVv6[msǎK11/]W (nn,];.Z%]_S( ~~'$yߍ|}OLf=ё\snpڵ=Js3xYr9-%hF[F}nXG?fŤ2YҢȳOOg|ΚIٳbas;+WHIIe 0 Ӥ WdffbZ>}#Uy>K$!1t6lĴҼ>8k6fnX)o.7V+{c=YӦܽ /m2i2prtn:lݶ؛.^(>Qѧ9x0WҢ oĄpuueټ{л]>oO??gaQjٍkVV&1.]h0!==(q۪٫!|W๗^Wł(|ݱ8xdE!|N7=u# EQgΞ 7׌~|F ! Å!R!$X !K!`),BBHB B!n`/]^$խwjn#Oݻ_{ltU}\dXJPYÆʕ-ĭ,WZMVhݪ%W,,O_gKkoyQk^zbnK}n(moBfMJJ_TB"+Qc *ǎ);wU#ƌSfΚX,eǮJn=K۪Iu+T П!!Ԯ]Uk;qTr:n]:ӹcuʯ&nXTK\r]t+ eܹ,];ۅ|*Z4o#7mVö_MRύc. !+{pQM?Uk֠( x:Go-v\ӕY^cF3c 5c:TM OTk֯g2w^n.Z̽<Č/Pۧ{\ϿGdiuoXoāC~,LrT1Iu+ĭMrTIu+I>}\$խ2 B !(K!`),BBHB B!R!HX9 BQ^ =%p!K!`),BBHB B!R!$X !`),*UK*˗/c.jx{yѵsgjժO?̰!C𯺔"#ٷ?WRR04lЀn](Wd$''[vϸqsa8Arr2-BBӫWiΜ=hݪ,- W]h(# fq9.:G#"ع{7ﺋzub$|.[ct͆V+u!үO6i>.h..tԉȨ ߱,2+WlJ| =eZz:لi^[n%33Vܩ!͚۹^m6kfѺukΝ;Gnn.գGAbsnzt릾@ 4_dmߎٳܺmKdT&O DZ jܨΟGQbeVS>'''hҸ1O}...lش /6ݫϞ-6 WkȃSş˗AաѣXz5c?>!(`0hP>.^{norss1l6"6x]ucW)DIOOl.S<3GGGnٳYb饮Drr2ݺtAFVVӦukZ-Z[s*2meeea1ה~D9KU6mth4(ǖm᧟᧟Xd]fSh4Y.{u&7mbӖ-=bD"V_/lm,q[NNN-Xd MIHfYuBk4jPۣ Hfb,deeѤqRtLΝ$''+WXv-NN4o@kתņMpww!nfb( J6 Ȼc޴IKvv6/_&2:fnh$--e+V`2pttntM}N0e6uJ-6d;w2g\6tI]I&,\l5jDX֡MV8ۿ7пrM4aƍtڵX!n_{} k׎nglڲYH.]N8F-`Y9"BQqBa BaXGzH^9!,BBHB B!R!K!`),BBHB wkGs;:R q'T&Bכ [>XT%)H_u4999үOZmIIMe֭'$DNJuv!D'=|٩pKϸXȨ(5mZb}l6+VqF68\~yyh|Sᖦmt:t:uj&!1&$&ItӤqcNVpBK[pKSw6J}dťXNw77BzR1N{=ާ &6M}q+Sq}paN{=/NNݷJLl,QQ !nLYp){5!nEXCٲu+?:3}{V{BW^*ʫB2H*\! BaI+ҳB B!R!$X !K!`),BHB B!RQfΚERRR!.>.39|{T"#ٷ?WRR04lЀn]Hz\q+)͕sNp0~ I[ hD;w]wQn]Le?vY!TF iL;==zuގbڦM 7;baM}2h̛'hղ%_}-=b[x8;ZG!j\˗Yv-L6h.4Gpy.İw>m ٘R'pKqS'$sEZlY1TG!j\Ȩ(FXtKzh٢bbȶP غ};ԩ]>z?'rTcGBK[, RB^Z;J YYYzF5e...HWͅo6lHBb5^֬RL:++u˶ml߱///wz`YNwR:99aXͽ&`L&u۲wY^+ՠדQF: Gʹo1IkN燃QѴ QfΜ=Kި`P_yPjUnDImhh4hJMw+pKqSztLΝ$''+WXv-NN4oyuNMٳŶsuis7ڂ­p۴jȾYq9wޝ 7q8n=,BDQUmARVI+. W!*@BAR !,BBHB B!R!$X !K!,BBHi<2T IIIzFP` mEx !ne]r%gϝSFx+X |NztFz0 \x'Op,d^UZ\>{^ǒ%jN{SԖ~yI+>a=xzz1,ڵj>=RS$$&IjӤqcNFFmjWNNyۓWQJKۺU+>se+VpϸqŲVe`Xj*ZKGR,%IE&y&dW0eoOJ^!D*-UnѼX^^}t*hOEE]HQ\\\0lIKO;UNמBeo\VKuYzuŽ;8v DNNQlٶ ?__ػoVX"J,ɍ,+=)yUl6s)2L&8v,tU˖8:: CXZaCeV~((H΄B"\ sBa B!R!$X !K!`),BBHB!R!$X5d("u;xQw q+Wv\jٴ2|Ȑs2LD@O2iΛ|&2/SO-B%yHLL*쾉 '6nB˸8ӣk75mr=Կ1, P^ːAo[NeϜe݆?O?a#5RШ-OLJ'ILJG`0о](g;q &OB1,EHyE"i^xi b=Yqcu{_Si۱3]{a]۶3ixu YsWcҩ#O'jwwEx{y={Z\ٳX,jMtKHLϯ؜_pPѧO/Zma(\b/ǒk63f$ulYte[x8<"ؽ+Wۂ$&&OH %%E}3?B5>ޅX+G)TiFѭ+5@εZ-F`֭|Li޴);XP&ILl,~fSb\\WUYE҅h*TGH}GpA7Yd;emL3f}?nлG'|13g,B7K{*^x Կ!͛͏fcX8p6l`4o3s }ؕ#oݲˋj֤ WdffbZ>}#ύ, YYYY+))4ߠ7L89:RNnΥb{{{7_2w^#RRR9{6 (U.^)n /c20L޳+ɴiYt:f|1ӦOg;o>!M~v8?2ˋh#>nj%7Lzuy'JSa0Sf*kݺtfL~hִ:(oѣE<|LZ=Z?~Qn/@Ͻ z(1W~;xUUQqjǏۥ5u~;i2nܼF=Auea%]={f)HmW>0K}!:4[‚EWzX߸8;AG[ps 7r'8P%@\MBBي Tn삄Ĥh["zփ,[Z%^˖-ZVi-RQZZLr_8mZmKD`jq\* 9W^[)455 gO;8 +KJIb- GwveݭpB,?xnV]Ѵiå˗ݪkd-UO SW,`qcc6eݬד >>hڴ)""petwW鸌%r&&>m*ih~}Nb vڥئ,]"~02 m"=Ŝu*XeEEO;$R6## G?;,C^5fF4if&bc-]nz@;lRmuhoa _\kmY\Ѷnn0oۮJ텸8 s߲]nbC*1It]1`-ћk o04l]CC-sѦu7E2e$ ] ["p:त"6>M4/1'ao;g"8:#??6|3~rIK'vqvB\,@]]]V(H1꣱p憯/\.W&H]W󱱕2 d!++ sR8]kpt9#$4]@Df͚u3n%߮rs~*r\ NCqM[=n""ѥSJcckQOecgϝǬy_`-=/]m?nB +ƩgANq"2_9Kzbb>FCpqvµqH(މS'au0G,5Y|%+~6l5r$I$~\Oxz::CWWbÇ:޻h m--hi|UI7nVr2f|:016wD;Vm㑖{;["|.=ѫiS9hذ!D"~*_bNK~?vR8H$H$PDdfecPSSUWK8",m*=޽~}bάG,iڬs(޻k ;  bޢׇ@_i&8o/D"Q,,,DTQF(.!//I7Юm ߬\xH|U?SX߬Z-S)07edT)5- u.V` Ky6̛=Wq^\"OMWmmr94n F Kxg5Yd)-YZiyVgR{{xcck[@xܿra;cxWWA]kcа(.{1ej2F[8+1c:BXjjjx|Y촶~~ZX1O 1s$@jgO2NbkW²+k7tuuYh]L 5X|%f~: m۴7+V~4Tkݬ0q5kr?̞E>_:ǭ g<. #0Ataq AJKK]' )07דARa'S??x 0P;^HLJ?~,7Aڵ[A· qB T}88 /]Vڇ8& '\vM%dff*ڤ.+WU]۸<~XX_;q}T:}PTT$*gXD)rR@X7n8{Nurb+gg3L&d2a¦-J fΝ'dBL&x}4V PɹX\vM AR1f1\PZ*`T3ZZ035%m۴& @BR,(WccįΞ? tԑ`<.2Y.[ gW^y󑑕 ==]itFjhh@44@y=#(8I0`<.Q)@Yk= +V#puUFY@[G8/ؾ5q՛x*r:ɷo㏳M(WmmtK`jb,e<.qelԨ<ܰ)`"5## G?;,C^u~( qq`_eU\8s,,ԩqt?#Q,]nz@;lR6"C[;9 BII qb.1%"p"7 M'qfIDbIDbIDbIDbIDbIDbIDD,DDMQ "RV,9 DD5H{p""UXXXXXXXX%eu]Un(TnWvٷq #n޺ūX,_0O A;AhQ?EqqK})'بs: Dְ|'zk^ !2TC$ᗘ/]RE8 o//ص_,_r鹽p#F{z+/]:cт/!ug㏛Ӂ(**-#s)' !a/4733ʪ}:;S @EۭzܹՅ펮Cz78R9who{cz:tuɤpUi^d,D DH$024!kkء=|?]E# x􀷗=s!\ /̆:==]J5ĴO>ܼu ,ŝw+ߌiSUjoc}EEE8ooN:PY^z [6@$߾׮_#ط{'N8@O }]]_ÇO˖; ܟ08I$u ++ PRRs?s, POY{E5Cǚ>{:w]  07)b!fΝ#C#D-6b9bؾ5 Iض}J)ypE̱h= *ҷظ8{j|}b5r$I$~\O(+bHq2 &044q졫 XC@C]wݯS_㑖{;["ʊwBrsshb|=+.'$"3+'ZaJeEEzzCҍN:L9Ǟj1҂;8QQ*ZB~~44wƜDJjjw4q0xC{a!aahon u ;~BQ*YϞC AWG!w"^yXr9=B< @hצ LMLv÷GII n%W_N"77p0kg?x'+7XSAv.\gP]:pgvwž~'ת ߬Xm`4%%%80km@[?x-, 1 _֭Zq .`ld==]HwwED@w  >)ŵ?sH,cUv[χxѱXzܾFz%e/ vJdddbаgE~~[q67.ЦUkh;{&t>E>.m$XhhA;aogO>IW֮JT}}E?*."A'͚~}`!N2Ş9 W7O~)^xdggC:}gϟG>PUDzܾsb |J_QNe8Q=Xv2H,DD,DD,DD,DD,DDoHI왓 ""e̬,K"z[g5mP^^XIENDB`litestar-2.16.0/docs/images/debugging/vs-code-select-config.png000066400000000000000000001476631500564371300243720ustar00rootroot00000000000000PNG  IHDR3b=iCCPICC profile(}=H@_SE* v鐡:Y\ EPjVL.& IZpc⬫ ~8)HK -b<8ǻ{wШ0T2҉ͭݯ f/1SK_.Ƴ9zH}`fֲ J^B!-R9666Ջ1 vpNWފxkkkPT(  B!⾧J_~X[[w[7w&зo_㙄B!-B`O>ƿ;ңv{{{JB!BtATR4euJmll:=KB!B666&1uVoxB!sfffw:-qB!tC_O޾urrBRI qsttd,^"""1bdggh]111,Y[fffDGG>2vvv/4i>, ,`>}GĉqttdoIIIC-z*27ٙeii믿9aoo7cƌ￿#SR1k֬;:?rqqaٳ0 S__ĉ&KKKBp-B9r$άZ4cǎU6qP*8qrwy綟 ~ȗ_~)*$hB􈕕j{&! +++غu+n˅1eNիlܸJ}.]ҥK̕+WY o߾t:rssٴi]ɸq㈎ё|֯_|dܹx{{pe6l@ii|<裌=JEbb"6l08O< /,cffl6h |AOee%{aĈXYYjժN5p@f͚JbǙ3gaʦMƄ prr8vm;j(}YV\̙3ApQۇ``ٲe3wm㫯ԩS&L`ƌ888^"66ݻw-I`޽<<<կ~|@VVo/_F0n8~<ڢV9uq[ׯ `l߾ݻwoPTTڵk d֬Yj9<6m8[oEzz:UUUcccCNN֭k#]#9<۷o';;666 lBUUyyyM6sIvڅ`GGG xyY~=Νr9;;s!j5;_wy6l?8WFR1g̙_&77_ၳq IOOLJYf~k7_ҥK)))!11דO<{go ~m1{8ӧO?t0Ο?7ߟBN:E}}=̜9[[[6m_VXAaaqZG<,_$mF^7o455ϹsXr%O<>,X څ{rrrشis_Gӑٳg9~1wy+J޽9s&?]wa_65𫯯VnHII3f pYfq֬Y@ZZMMMV˲eիnH 1 v/\Z͇~hrm:=ƲuV.^Rd۷$e$''τ &11:cosUU,--ۭ˗/+-- Ngr#YvI|9\ihhࡇb 4 t.9s搛krܗb FM||qFo1';vW_ͭ[W~ ! ..aÆ닿?>(!!!Ơߟ6i4)))L}$hۛ;wJՑɀLOLLlzҥx{{(hŋƀNˋ۷IyJ%gϞmS3f0h &󕕕ѯ_JÃQW\1 ء#ӫW/͍O-tttMָyfiiikgff'eee@ څ]!zxQ*EPPG}&R((vorD߾}yWHMMo SO=Q(TWWLjhZk[[[J%fbƌmu}i<أr988PPPfF!(NM[_ѵur?R٦~t:]D{iS/fԨQlٲWj(k}a{Cj46uuIVk]D !$hB9|0QQQ׏Djkk)))aÆ .@W˵YNNNSAAA >s냩.tsss뺺: {%!!zt:]ƀݑzLz۬5>0 C' m`ң`\ǭhmdnn&׷SG !!!ٳcǎ@Suuu&us}[ttsB\K)U ggv{=<<7{yyQ[[KQQQtgL0aBI串Z$`W^.Lnn.C5>ds)))iSk^ѣGFP @EEJPKKKcbС&stiZ;vlxfggLHHPzO677GPP#G޽{LvnnncԨQmmjj'??؎/iBȈ#={6N"''f݉8 8x %%%X[[3`%ruuuرT*ILL4΅ hjjhjj4/2k,"""8vcҥFڹs'/'Oɓp67nW_eŊ?~F&338zٲe$$$лwobbbHNN6泧h?>}VVV,XM g?̌Yfu 6m_T*ٷoC1eL>GRQQv̙3ݛxFUΜ9Ü9s8{,999 :I&ukzt"""HKKɓ'3hР61l0 FMM m߾˗s#s%//}B۷rNNN!ai~~~3PKJJ k֬1p466ri0a'Nd Μ9cszСߟ-Z&00I&1|p֠ "##4i.\h7iӦ1w\ ž={pvvd땔PQQ<9seӦM >,c/jee%)))NXXgRSSӦMc899M@@III[8,^'##P͛G`` qqqT*JFVKFFÇ'""Ao>4XҥK 4pqrrƛ!/]DSSaaaL8޽{˖-[ׯ߿$ hĈ899qQ\]] Ɠ%333f̘ARRTVV2vXqrrbݺuaښhOw&$$'No$[6\L~;v,֒¦MiB{!B_Uz (B!Nv!B!$hB!BH.B!B!B څB!Ii, !B}Ν/=B!B$hB!Bv!B!B!BH.B!]!B!AB! !B!$hB!~cv77MyI !B{MM `(<A{WL0 &z C^4/; KB!=MR:E춽(J Enm58o/pJB!܌?]644 !B7Kv!B!p !B!A{ϭ]VZF!B;9hB!BNݱz `0jիzNooo"##ohhhQPǐ!Cprr FիW9<٧ ֖2vҥK9tw0tP1>o<*++9||B!ACnn.( ,--quu%$$vEuu][JӧNff&iii466舯/lݺoޞs9rssill`0PUUuC'B3hWmF!Bnh4dgg_¬Ydwmŏ=>>QSSÅ HIIi{{{LZTΝ;g|?** sssv6}O۶mEQXXH}}=~~~ذqF9r$} F˗INN6mѢE899ѻwo8}4Νk7=ˋQFLcc#999ǛD-Z‚R;vW_B!wW1h:u*$&&RVVF߾} `0p###IKK#)) wwwijjj7ᤧs̬jݻ7vk;aaa 6d quuepqy'Mɓ'Ãɓ'S]]MVVqqq0i$lB}}}A`fƃ>xK___^ʞ={hjj***|2:gggFN`ΝDGGSQQA||<@<^^^DEEÙ3g%44mfp___rrrرc'O&**M6!BH~Z:pwwˋXcu~~ŋ&iƞ₂9r$/^FEaa!G1nN5)Gg6l)))$$$R$00$檷ϏAV56soY:8|Igee\m(,,q`^OSSSj8`hJBCCG5gΜa֬YՋ B;=1䣻;&r#1H6Pݯ86ۼruuEP\rBB;;?QJJJڜT*Fɂ x'xꩧgg6uVdzqqɜF19B!Sܕ=Ah]]RXlYd@[ckkKeeeBaL1iDccc˶w't[ouQT=ߛ)[g_&>>2puu%<&Bq&zҒ^j|jM!BH_ػwM!B!|v!B!]!B!AB! !B!Ҡݦ&!3B!}/Zm=>ԡ/RB!` Xm v~B!>a%]!B;B!BH.B!]!B څB! !B!$hB!Bv!B!-wdiifffRB!DCCZ---g !B1XYYI !B·5h!BqXXnDB!'AB!< oδqh:V6oɅ OʋSwg6J+'Us6lm1ůE?9B!UwQK߽ IY^cĢ*T YIy ݝ\%I:kкUF{d|k֬… wxyy+o@;-[Faƍw~׏7x7u|rغum߶B`ammʕ+7oڵ _$!7.l!Z IDATvjT +c٢,y 뷵P~<~m3Gy@CCeeeqjjjz|g)--e˖-io'կۮm[8;;HYYGɓwm6 VZEUUZ4JJJ tz/W|ɼw^w'dsG>ϟd@δI}cvI/B@Y2^y;S_xgfI ~K3Y]v"ܱ擴]-'wI谓؞_ t|ҎÕߐ\{ `ҤI;?XoJEsP(P(>Xnm9z(CQWWGrr]Yw}AVSTTdv !?5K\\:ݗXt5#&:ewvWV`UDǓ%:t?%W^lQ;4[ԗԖ`x/?價V$| N,!0\^m |,<]/[W݈`|itj/xk8j,~UVO Y}_,\O?8_DD'NZ͡CH:::|r|||h4ٳʕ+zq~]vŅO?ͻKyyyG `===7onnn~zlCM~ܹs޽^>}0g|}}QTax9}4q%K덁Q՘3l0ٶm k֬aƌzj;4իW3sLzEiiμk ))5k֘),,ٳgDaɒ%XYYzvb=gggjkkIIIa&)I~~~̘1wwwa͚5,ZökM㷿-ѣ?~]\o~Ç~Hnn.̙3 ,,,gdeeuzQ\\ @^^1|~_?x;v,g棏>^ s%,, ^O||<;v`0Of͚Epp0c.]ԭvmϲe ?4yoۗsKSSlٲJB ڻZfh38]1WB-$-}ެ_elÔ}q5:((C κ&:m8?+xt;M&N,3<+V&,12UgxX>&|k>9ƫ73xi\]cpA=k&eÓụn[wp8 %lbS7ǭj9v111XYY cӦMC=V%11Ѹltt4۷o#((Gyrrrm~iN8c۷/tuakk& ssN={6?ꂯ/O>$[nҥK۳h",--ٰaC۳?9yyyOOOʞ=n8;֯_J_~ƓTTT`nn}Nu̘18qUVƒ%K3̙3 ?7g}?.۵5֚5k"00+Wg?밼vvvg?#11xR1m4{9?ݗWz.@v>m_Ǫbw~MKIn0L{Av3,kڊ,ʠK, 9}5=›Q?.ۜEZⵡy)E,zw#H^:Ϗ?+[=B 'aOIq~SPTTB333"##ٰa.\d:ĸq~jbccx;ƏOEE[nTsN.]DQQ;wMѣG9qjl6nHXXXA1 |PZZٳgcweeeGssIΝ;)//`0];wDѠ9rI9CqYLz.^r ##r222غu+G6?m4RSSپ};raoOfdΜi8p  /$;;ZOvv6!!!03#,,se=z!xyyu+罵缸$RSS2dή]HII-[P\\LDD-m׮;r6oLQQoooB=&y:QY&?lmQ8y!#9# .dmv,נ5 dcA VXY ,{1T֢mwM1b׵9q銽=*wbGo  s ٧O,,,X|yG=;;QFu{}%77פ3]UUel㪪*<<<2dmsrrjݝ웾I=xqk9ɓ+8::RUUEXX 0e郕fffX[[ҸgϞ fϞ=xzzҷo_cJce*TT( Gĉ?E1b #77$_;@ee%...@KjyԝLnyv//64yB ; dtnNneN/uX4h^ׅ.}GG_G |v߰>:uM u`hO0y~ MMMmu ͛7sȑtTNR&vu@kow꾽u{U 3f )))xzz_w8 ˗/'66;wR[[>,*ꖶ3g;v,{!88,cB_=^ɓ'9z(: zljj"!!cǒDHHHy]`hS+O|y B;DdH~vA($naIg,-,nh$Қݙ0->o M,NH!R/q麟---8q"444PRRNc].ۚG~֞†5oeeF%%% 0$(~AbO1bĈ/3p@;v&##O?hVX`Ԥѡ3w\^|E֑Q;+o&;w<ݺL]]۷og<#$''UCC >aǎ[^Jtt4666TVV޽{bn7xKKKo~o-^ RRRD!OKB!u1J:!B!l5h1B!Bn&A{CCB!/L,|[oDm}fffrB!DCCɃYGL'B!f冃vkk.gvG&;ݘ96rB!⾡T~hI&(;lna)&B!K7 wkYB!baV+ !BqRB!BH~_~/?m6}Ͼ_#>bL~ {g CB! @LvB1N\А{a`K/1h=_Miz ˟c`;~]\yX[[:\ä?2(/vm;U!'HM! p,AVg8@)`C BܳAW@K544hQ|9NO.3nj)+-D\ZGHh uju^:+*ɸro:.]Z+>|z(@^X| a8!$hoK:K|pT/^ґyKX4ޟPUH|kmgÑyXC®kӖ89VDNށ,ZBUU˩[hHCr!"GҒyy?p˳⥗=|_<=ASSi2<<<ܩD\P(?o[EYڤ'O ^O~>>}5|9#ǎA@//f͜ɡÇȠ@oԑ %-i&/rqc[1uJ}}?k3, c/,|/Z cbhjjq 6}!^|~932رsWKBIĉ>f}2ވ6M:c`ee?xÆ1&4s 8tMmK!$h4qE>LQeoLuA{pNM?4H6ݙs}m<0t忈HR.΂߄ڞ3b]~/!FJ:}~ϢYz᚞*+S1h3g6 l۱:p~ _} 8991חM[`nfδ(x`:l}}|0n{*2~8Z]˨T**++Iqرs'1FX[αykݿ;M?nǎ'!Өԋ ` ݀#Ee%ceRez=66,!##ceZT$*3h~~DNʞ{~6}{xŔ3ӓ;w&88Gmu!5cN pppJ555xxxPTT]uu5O1HNJ?ٹ0)z=UUUtf;9b@om߁ 3gFcccCcSS4[[fEϠxR_ B{]7I$i7ڑc1&a> TC޲49 g)؀4gIȻ{!MmNb`ۂ\cE~pζ;(,,gRXXDؽw/ju9Eۿ^^8;;w{Fŋ\@uu5璒}CC'Oj.ߐ!ޮ%&`0/ȧK.1b0t Bk=nHDr+&ڲg!}ml0ښ6Tb %i0 kJnveӮT IDAT #N6ڙ[pquJo着jꌯKJKinLyyyիW/RS/L+(,2@?cffFeUe[3544Ac x^a^֍n%%%ƿ uL}}=?_]]UUr5kzavmmmlxEU~޽Eu݋ 2ror1*Q`41Iz~%i$=MzI$m4UlbM"Fm"Qx/tPa.mqgkk>kjBH$8;9!Jh٬\{GJ5QJM;/ @uu&r'Mrm-G֖saV_RRBlLh0Օ˵Z󥪺 D ?H-Ag񾭵 7cu5]ʷYYYIXhR++|||`-ۛxEѣ^oumi14er9zӇ~hll_j`@[+ a54 ;gZxfYՑq }uvF+@#`o+Cf;c]9Hd-i 6 o>*` a͜_-ABGGwR@$7}?̞5q2<09qco={`RW[[K]}=XZ;_R{,S;zzzHu " o}cqqu%}-H$>_]&tSP^tXqb);?*ϛ41o`EZ[$00JΩϙBdz?]I_n?o,R)*ww<9y$2y6]_a h4@oϠzC {@Ƴ?K](d8oAشqZeM ǓΗB/4"LSqeBcc+@կdYk|w5Kó_uA=ᆿh_x5bccMsvvKC/ȑHV_me7ZL puuxߠkU?9{:Vs455S_@KKˀVaa&2)ʢu˄/ T^əb':0L9>>(ac3ۚ*Ti须 މdwu G'~Jo柲8gk-52/}g>etO~wyG Oϛ1z iȕ W3tvw~$===S#G(,|Ҏ(̊ޞVk.V_kY\,-%Gҥ j9IXYHbŋ;3es"?}7W=$&˵,RzPZ_1cnxMyy9KKY(6667/wdX}_l~ 3.'S3s-:d= a駞K82l<87gW,'N=3mml3bf XKijn8|V=7}S̴S),*ֆ3\,-#(0s,ghmem'Kb2RRrF>p|KK?ym$/\`ҥa=sa8vee%S"©wchvvrr1st=8bGXb3XYY~32/ $~~~CJu9hgF Y]Jyz-Z~X<<}F };{w LJ}{8Cww7:^Ϯ]t;w۷STTݻh:\]66r1 ܃$NvcYOAؒ%'"<6(..6lb/7ֆ$vAI,]F#m]Ac'M$;;k,u߇zSm2Jxsڻ;*  nFAuuERwwwl2 Hww7ASSJ{{;VVVB>!> bbb ɓ'3ydZZZ `0,*NcbAAvL&߿3g~1cD.ɁD AAn1{ž}&# ߱rfΜ9x .\`(. !D[oT*QVVVOH$455[#8(ޛG3IߔV`!Ii:Ȧ\]_?%/aHv#^ 8K/S&~A4h?x 8::"J-zz/xr\pcL#H[:_e$䍕1(~hE w⶿2ȭYNjrvv_y<ʼn666ttt`0hmm5?Hoxǎ:;;1F؈A?ly"##~_| & ʿd7AD" T:H=Ӯ\A.(# 'ĄAe B7eQyɾ47i$155l( Ϭd1 Dڇhσ=3e)x)]K=k}z@ϲ^5lH% P? %EطoD}~E2BfG2Ih c^{(2sm?L{a2)&$xBU8*YCa?߀̛Ohznr0МC2(l6Kh Dz5yg yVXXF}}a` FqJڱ6- 7A W_bk2iϱtQˊ'Kj-Q)lzoqI]5j~7\9s5o<AyoSFKxW騍elyuxuk v %ǥh. g/>Mw9HFVRv\ObUmlL P!:Ȗt͠ '1eAGޗlQɛp*[_>]IH HH%-{ lڴ Tɓ 449s/Edd$2G}v1bqqq5 LF}}=識bSL!""ggg(..6oΎ1 8p˿`Ǝk^/u먫\NΝ;޽{1`Lޞ&9qĠ~3mFLL J?rNJDDz;ӧˮYݻw/|444/zrDGGT*ijjĉ?~J\\L&***} FܹsQ*hZǻ_5JRΝk~`ӧٷo9?9| G5/CW_}{+$&&MKK `r̙JBBXYY9M`` gϞ%""9s&_O+}cƌ!66#GJqq1999tuuiCzLرc?WJA\.Gfr ѩ<1UNٶ]2bդyW㉂jPK l9}Ew&BoM≕sogw cyx8ʭRRPm?g1U,7:݀рN¬%uQ7 N/$NI&98uE7cP98vn"3UAf :D/$)<;{jaB_$w yrH!@ZZZߏd̙31i$_7%%%666w^ bɒ%7A3gdڴiݻK.aggEbcc9x gҤI,\F3`pW_aooNcϞ= 233qtt$)) kkksxNlj'hmmˋ:u*}MMMX[[3c BCCٳgZQFhXWLL ';;(-ZD]]N";;3ġ~HOOcƌ!)){r|}};w.mmm Z?ٳlܸӧc~[x /̙3dddܹs-zCBB8y$6l`ȑ,XVtJ$yٰa&%%%† bĈyeWThZ֭[GOORFMii?~<_5~~~<ٳr > 666y&O̮]D* =.#|F6?gcp8جz9d_NKi 2/Zjh5AiA=(]pLќePX/# hM;y& ג[`4 P "fl+$W3k?OŸ>7YW#uk@:,U_H~{\.C.ɓOh4tttvGЪ*N:E]] :tVK``5**\N:N3gpɡaetttsN(++#++`;fKMM SPP@PP-wA须HvillDVsQnע"S^^Nqqy NN###9{,ǎSNqIoYz,ꨫ㫯BTp@VV \pܢ?666o>t:%%%9riӦ xR/r /_fΝX[߼-ٙ hjjad2{nsGŶ6.^h}-ќ8qSN멪"++PNSNa2WIA{݈zȈ֛yIgռS R\סsl4&#N 8[@nrG% ex?qvd‰S`VC-{cc )UEEE,\LhdҤI`DEEݰnGGGo8oAݥB OB JHNYFT;tt:P0@Ntw:i1:Jt޼'igI١M(@r>XMILL.Ov!Ć+(xvVȐ pr M<+WZNNN7uH||<>>>dggЀhdѢE+Ϟapv'N$!!ݻwS]]MGGSL(9KZRׯ_Omm7V+++Ο?OFFư,}%wޡTjqqJJJ?~Syoef  ;gTesbא_%efM;Umi>GI$G=Ü"# !͈li͂.,evF0jg4%3C5~&뵂ICF5l{2a%G:-1ZsϝVN|oZYY?Yp!aaa`2@ϏǏ|% ...\r:7OL& er 'OCCCÀQ.*A0}9nܸ[n Ӽ7˗6mڐ[Ju[>ۛV2YbnGGGsk666܏FN8'5k!!!;v9V4irV.]dQ/Ǐe. 0Og0 ?1vH'=UxeI1?NԬDRY[ ؙ J|d8QRɖ}*X#C MlZj*p#c_W_!e U Ct (8ÖU` sLEȒ,(8-!?'DM 'jV2^XNϱcbooɓ'}bBCCH$zqwwYN3zh0 \ IDAT&JH$̞=@ѣ̜90J%DDDѾ466关3vvvH$N: III1ΝKQQѠ1 xxxg„ 5dL>SJbL:ѣ;ydxɓ'b쌟 ,!Xsi z:On1P̜9sP*L0h  J\… 9r$$%%u_͛NNNT*|}}CNWTT?[2|quu% =]z(ǫ(2W_'')ħ"j8,Kgŋ)ЮEsro@mހגʢ^'UvmLT%%,F: 's${V,2-;#9d%B.tۃM{~ʿ&](O|ߢ3[Z~E`"F= ҥK0;w8)--󤦦bccCVV۷|3--]v`VZhٳ\xbݹtvv2}tϟ`o}9~8Xkkk~)qqq<ݻqssgd2QSSñcn+p?|0mmmL28:::vsN[:0X}lܸ9s搒5MMM :fKK [laܹ̞=cHJJ2:"ygFĉ|rL&gϞE֭[IJJ駟pLFRRttt;$샩 777vaK.3gOݍN8N oHɨ[l~9O ?fZtqAV\]]F"{]{4n8F#8::VmGA &[[[ΝTTTm۶MGAD.-(**9 wJT   ]AA  AAAA   "hAA   vAAAA   "hAAaȬ{/Y;yS@cps/CAH"B)W+|Gp^H )/,Ҿ3lSI<ߤ6=|9̳{ˣ-{u2,5ewmA“ܙCYAK w#d7E)nK]Oas5Q@]뤵^_}3\BxF7`Ngvo%H88NëK͎{,&003e <W㿧SSj'eW &pҍ/ xLe}]K=k}+UM'X4=o79th!o6];>O.&&l *[Wy-jdYt2QcQȍhv{hBʓd /Kb߄Z ;vɓ'S^^.CR$*BA?=HFVRv\ObU[z+~n'ЉA7^W_'}^ir͆m;xO_EaxwrdŰ29B-(<E 2OWrRIjj^"7_G"(P"|9U"OÆw!KV2)V3!hn"- "ISAu;*`]1P%Լl7i8`@9s5o<AyoSFKxW騍TEoby_E'oUl&@7e*`?8W';f1/ciO²_$)ABOXwGMA!%LC 'NΎ6,Xرc2e }=W@V_@dd$#Gd2QUUEVVz$WWW̙:v˗ܹ̲e,T*%66`r MɈ#cԨQd2ٿnd]\άY ֖&:DQQ2닽=MMMsĉ!/HV.]"33VyΞ=KDD;( Ν?K.p 9s&Fj :;;:u*8::9vO6/?m4۩"##1c0k,0Lb }cP98vn"3Uت赭t4ju0vF:_ O(距“?bu\,VSWMv\d9ҶIzIm9ё*2*wx[#}}9keȾՂV|w95z6m'kep8u^&ZӅ!FA2 BBSjW"ԟԔ(exW[ *xDΛYzdSRy" {wxy:e8ZjPK l9VgO֒yxm;w;%Y>J`hA{XX*9~8_}t:c^&==e˖fLd2r rӧxb>#zzzP(?͛7ΨQH$uĶm۰⡇b֬Yڵ놲766ZXBBB̤x{?pƆKww7AAA,Y45 Pxbٹs'`ccimNĉimmxȧ;|HR~az!>k*ZuCww7<VVV_d2?ڵkϏGD"13f 44={j5jIIIF=z4111l߾Z5Cqپ};+Wd„ ;wӧsyg֕+Wɶm(--5͍0qrr2L&hX[[syM p?Nć)0+ ' '$ϋ$.zY5Ķ-U_eu Smſidm!FwߊMgXh KdԈB7e͆V6Xns8`D|::uچX6mq.#F2 'گ=pviAopT@S 0\fСB IcEħ!0jܺYewoD w)vZ62j͛E:T̘1ͭF1 B1rHR4!X+++9r72QQQ;r] ettt2>X__dbĈ栽f( z=nnn5\.7O?hVZEii)?Hcc#%%%<Ӕq%j5j* vPF`YLL.("ܴy-w"km A] ^9-q}&9 dJ'Dz(5e/un ]P qZ o|zC+-,Fw@V`l'\{|͆O'՟"! ?1\zsCP'T*%44'O}ҥhZ233iiid2rJ{ $-So>37fۏLJl0,Zhvo'ݻ)S0a„aՁ e~@7Owҷ"\ug_5k6l[1c4ifbͷu> Bvg k* ?yTbޠMtt2BjhҐ_E61xocŢãwP 4);_Go]`( c£?bJ8氌(,47#곥s 2gIðψ$ w=NEƥ2*9Z!t3qDN:Ecc#>>>8;;I{{9]a\x.Z[[dرckk˃>hr{)"""̝ [[[@SSSs[ztvvrqbcc1 #F`֭7 Gӧf888 Vۭ~,jڼ-L0 & {… ٳg̟?2sj@*++&99ݻw;655s9?ٳ}FMaa!>|3fŋ͝Ie2Ǐ'((4 3DB}}=#F 00/҂+...|7j*p޳b iٱpR$Ÿ$bVlߥ 3HkHe *=םUO.OWBMݗ[9j@d*1KWl{mv]6wX\Ilm@S͖{H]mPF3=)%z4E;x矛Pa{xv+^ƚYg燛н÷Sk,%/@M>wǫ75--?]ƢYOfA #II )HvAGmzǣe(E`. dЖ F EX îg=R򷥑3! r;w0,))!,,SNqq<<|OOk'|B\\K.5~׷},KNNVVV$&&bkk˕+Wnڵ j*F#gϞŋ*Pe̙… 㸹b2رcNtj.]-eeedffr/.].]O?5-ebbb6mFFÙ3g8|0mmmL28sѣG>sA*;xhnn'O ·L7q}^b4Yr]4o>E}7Ee @`H$^pO=UָEEmq &P3?% 2D  |D.O,sa4h)=ӛR&. 1  =Uz"AAAaA   ]AA  AAAA   "hAA   vAAAA   "hAAaR/7Q716hd5zӵeEoU  {H4'֒UiϾŻH%DovڪZZ.DGGO`ʕ+3/2C^'..GyLyΝ{_Ϟd"""XbŠ9!d}/VGN>s&~Ϥsy{8;;_͛7S^^.Vofh4(--㴶筨CT=>-[FZZAA`D=Dщ즁cTDd2fmYeGUJA2 >%Q :m~4\]PUG|vngj&UdBv^C-ݜ$N>n:zzz5iӦLCCM8'AD~,ey!H]r3PktUZL`dWSxG@Ȓլjj^"wejx&M喁W$ɿx w;h؇e&YkkkJKK9y$n:?戎>PVXAuu5(Jy8x ===xxx ɤ;99Ç`Ŋ,Zcǎ{A?VERtR,,,L^Z歷Ғ 6HBBǏ &,(J֮]IKKCVյg͛۷X~ivލx㮝0 ٳg/Ϗ> K>\Bbb"c@( hjjʊX֮]o-MPw}^ϒ%KXroooϚ5kz*j,Y2o֯_Oii)888*4n4 ɨ&8qF<<<|TSSիWinnsHHHȔ*((Vn޼Iww7ӧO ""AҨ7nd2ө|&<穬JyL 1r5zϟϧ~Jaab:jsDD:,Z[[ϸ|2Z,innȑ#T*r:u6JKKp7r9gԩSTTTʱc&~}ԄL&FbJKKikkO>Lggg6$;;6ܹc P^222hnn,fΜR95SYYT*z*___jkkG.űcǸ}6픔pEf͚eRonn.Ũy?mtz=={QSf<ܾ}SNLSS.]Ɣ9dOG}Ĵi 2{L F;9@Gv k?_toM]pbُ_eBaZvXkYVCh׆{Xy::GTm KQ(DGG3c ښ/ᷴlZ[[1 <&_exZDdIdsJņ ƺu배0^L&3 Q9ߑ n߾MZZCr0~7fÆ 466IWWF͛7cii99M4{z4 TVVRSS=j///Ξ=++ݻz^m4||)s_f^w23SAoyzǷv [YY8?08d-J!`N PΝCI[nD(_ܺAF2Z-z'''Flm?MX[[cgg73*xKSS888LQ+#FkrkfºzʮM0飡ocaa17TXYY1{l***\iuppɉ3gP[[KGGS$2 <==GI9}tMVlGkh4TVVJ7L?>͍1N|||>DP(pwwU9e&^R3g҂j@Jey hc^6?ۗ(McǞ+CHV.G)_$~?r2_ow-ˉF/{`I;و?-'z^4I?{҇Ӈ8:,JɏB@\.G&xQDDD Q(,]tʫ\W^E&zjPT [[[)˗/3k,)!44t\v gggå+66֤Laa!]___h4ᎎ$$$R "&&$?PJ%2H hPڙGVcmm=a?Q8;;IDD=Ү #z1cpoWKNF«W@RRNNN|{UIII)..35fHee%X[[KEQ[[+F˼yPոyLꌋV/..~6ZpTojj*5x.\@`` /ΝYP#<ݘdݻֲrJpww'۷o _Qz'{ TW|tC;yf6o= [}m3ў6/Sض5dulI ,ts^xa %m9EeF=cǎʖ-[0 ܼy;w*n.\0*xt:'11 6H[>=z{֬YܻܹsRpd۷os9֭[[Ẻ8t,^NǥKLٷo YKKK:::(//7z+++y(**29&??ggg^xF#7o+LNN֬YFqqF2 /ܻ. Z[[u֨d2O59|0IIIDEEG}0.HNNfΜ9466ʕ+Zs=''ONXJzUU(WVV'9<̝;Dz{{ihhuSSHNNFV{7-Si'ONZ9eSQQCc ~Y]ww7'OdŤJ[>d6l؀% MhZ.b4<888k¯~ѫu//3_CϳX\2/\tr֭/8=˖-w7~ }ߍSFAfR"F7d\0Q\9+iQzo;H9,[42Šfh/Z s7o~\_#""CABBׯ_{v $YAM$/|3mEE/8>٘x}wF1dƢV1466Ȗx>>>Y/$hwqq!>>:::(**vvAaxᛘ#  _'X&AA6  AAAA   ]AA   vAAAA   "hAA  b8˙/kQߜ5<1$= '3ø-ǭ1_hEIh@RMC}/Ffm%H@AA=@/?φ?GcTo@ܓkqvsC̽>ό7ge\џ`BBBؽ{W|RSS8rٯ g…ر׬_&N<)>Ma}uV=ʭ[|Epqq^b }l 7ΐ}F?G"5aDE(3o(#wtiO9܀ e}Q~o%ϿʦeΕ:J3QZu ?Fg>tRdN|J]g.VsMH$dQ7~+FjذuVK]]9sׯų>ˡCё6233ihh^VIHH@ hnnرcKefΜI\\TTTAOOϘP(XlAAARڊ po'$$ȵkאd3|w*@||ŋ9{Tf}?`ү֧P(HNN&00kkkt:W^l޼BիaRݳf͢NGTTԘA{?---477#/_5, |vs6?쩚t'{;Cfkb, Rf̘~HGG111QRR2{ $!!ǏS^^NPPqqq& Fի9q(J.]5GʅQXXȞ={puu%55^O^^Y.Xj===ٳG 쾴7ybڴi8::cǎqcMG˗>ad/:j ֢hW*KUUvvv<3ܼy}O?ݻTtFF| lܸqT攙ڵkqpp ##ښRN<[?tttpFǸ,pI>3|}}INNbaA(  7TSmg46$QܕJ]Zc#eU:,֎L\)N#32Uqj;[p/ݦ3Zbw&eɅ4̤ h>~ӟ6iP4RNN:ݽ)HAAIDDI̭t)_9??qٳ̉v 5c^UU%=niiΝ;q r9gѣ~)\t <<<2111s{Ւƍ9~[ΩS{i&j\M>777vAgg'_H{{{+H_2i}455ItvvRSS3 B[nb֬Y㮠d2xγ룾???)h'66iӦ[DDԟC9O~o]ڷ隚V˅ 3ל2)___v-Ԙݹs $$$K.͛7״BLLxAqA;Uqe-sۈ}(i|wδ yoή~e/NjNÎ}y4NBG^ij,/B(72?israUWMy kGGG,X'vvvʷÔA^@Tى;&i!#iZ ;::P*taaaL6 B!T721T{kT*rIb4MRQ<*`6 ===#::zjkkGճxbllljwZMWW7k>>ԧwU~i^|E˹sd29}dc՜}:ٴb0x'k,5A+PgzmsB_{"}5O\ FrəS܋l^4T}''':$dj?Flmm笭'͗9s`ii9jTQVVfMuMb4qww7͍q_܌snnn#<2ie:f||jd҂=Ҋ|}KOO)..fÆ @,""rssMnN>jn핪$&&w288ݻw $զOj=Y^sLtZ[[{F˔IMMM&fOnrc---&SA %E& %(T,D [~.'.vaY&buS辶bSLp?nJb*N\JO\'* ~z=}}}.666,]ײ IDATtˊ """( .]:UW"Xz5^^^T*BCCڊF4jEr2FB.\-24._̬YtBCCMʜ?___-[;j`M9::J"((@{.MMM<NJJ >̩/66 T**t:ƣjlmm fׯ3}tjCFڇԠT*>}@vZ|}}qttDѐj2owL;88AAADGGל2{.w婧" GGG[J.#Xx1&u퍣tmtEBCC7ojfϞm 1l_w|oݽ:):{65j(k쎁Qkyq ^{^gߜe,᣿el5(Z\ |lM1, |6mܯk>LRRQQQpQǎ#55twws4͔ؿ?lذAq.*Suey饗RRRx,Yd4hVOe3Y}ǣR{O O:ҥK/ <\]]̚5KaS%fhJo^z=#!!5k`iiIGG&i9颢",--g~z)3CO<5MCcr˖- n޼ɝ;wFOOO^|E,---+++### ''g5" <8F1kinP~//Q05lٲeijjɓTFAfQ"F7? ]`` HLLW %vA !99{{{zzzf>AA#  pX&AA6  AAAA   ]AA   vAAAA   "hAA  b8˙/kQߜ5?ᣟ+i5ЊE-J [ĀAA|_vv?or z!ʜOjj*SzMxx87ok֯_Obb7###yZ}_ٶm>( TlԼh+<ݑ-B'Ƒ%jllȑ#&ϵ ?NAAhlAuY*QNnC=ʻ ogY{8ZʍEkmd06f;yg.u n̜D1f |R~ks%+FjذuVK]]9sׯų>ˡCё6233ihh^VIHH@ hnnرcKefΜI\\TTTag ˖-;;;:::((( ??_*~zZ[[ 88"rrrUr9?8!!!F]L&~?5eeeܽ{wTquuEs rss|$''3sL]ƩS4T*˗/LJ.N>ŋ9{Tf}?`ү2122zzz!--mxꩧ8qĘkӦMO<̙3y7D.//Rp;o, Rf̘~HGG111QRR2{ $!!ǏS^^NPPqqq& Fի9q(J.]5GʅQXXȞ={puu%55^/dZ#M쾴cG}DCCYף>ʲe㏹}eh4Ri4h4L& Ή'hllà '''ǏBjj*>PFEESWWG@@k׮Uyؿ?,\u?I_Ѱ~z.^Hff&2 麛<ٻw1#Fff&̞=~zd2g  @opưgzMaPZ?'  1tS֢`[8-"JZ;WXsm|hjj'|OSܦNw ޞN"""$--MZenmm|KDFF{nΞ=+=noo͍PJz;wƍrfϞѣG)// 33 O),,ҥK!!??Wjbƍ?~\ǠSNLj57;h>}:nnnرN222&!~ۛW^yEz_:ƴi顼Hgg'ܹs'--mJ̙ӦMښ)h~r9QQQ>|2/5ku떴/Զׯ_V(--U3g={YfcIco"ܾ}[_MMM̝;,i2u |}}7o?g  +fon䵟j:.t8ǯDC{{;OqkCc g>άWV`UNvaC}O,XOOO8)à CTى;&i!#iZ:::P*3<<0MBV߇ki1+SףRSR!˩|N8nggg$hϏQwpp`jmuu5/ƬvWtuu>---(x}?2}>o&&&-[PVVFYYo`:{GR]]%8;;SSSCyyjxG1ޑCCCmGNPxGGApuu5klxxx^QmtQ9g  (| o([12YP_HVob[UC@y`~nPpsÆ 466IWWF͛7cii) c2<99cn3qsK,ܹs r#O8dQezMvv6/_1'jL%}ctwwk.4 ,ZEg z<== 4h룾F3`ooZKvގw~y9xr~] ߉>#AoDwx?0qC!yܧ25mprr̙3сIPۋh4_zz||| LϏ2immEI+jFMwssuͣVGެL1[ZZAzkk/c| Mv8qv$lCyJ˘Uf NQG~s?m=iÏP(pwwNyN6y~ lӧ} aʮUEr?J9(ؼ% A^mlz"yaKbsK+hdHKF^}e SGz1cpoKZ ""\B`ҥS^z*2իWJ"44OOnV<==4Q1pBlmmd̟?'' _wef͚%J8<,[ wwwj# T*1Y޽KSSO<B+0DDD₃>(2lT 4pkkkigJ¨Q牍e޼yj܈`޼y&u#QQQ Tv^JEtt4]pAjj*\3܅  dŸܹs6KF^f?;r3쇶n׿5&))(zzz8cǎCp¸wt߿D6l m8||2μKRK/KYYnݚ5oܹs[wsŋ5kI;pB100@[[ۨ+++y(**2 u> %%{NN>͒%K])5f0C)M rZ[[IOOsv(p߸q# WUUIhee%qqqnb=<̝;Dz{{ihhŋ&Μ9ã>?^'##>mӧOOZɓ'INNFV{g1CC1 TWWsuγ'OxbRSS-G‚˗ccc#dۇ _2Fc՟Q?^^^v%ol2jwc۶m>|x+6~gv. KVS[[L&*_@ ---888Hcc?AA_"hƆd顪ÇZ# C!cAAl D  &vAAAA   "hAA   vAAAD.  "hAAA   vAAAD.  ,g9~m9Fjb{y4(2WJA+l=  z_vl^+\מknl[8bbbx2瓚Jjj^͛ד(>i@V"99y27o&<<[;'k3saغu+!!!b ˯6{' ?ia{}(0`0~_/pRRRx7#ZxLXXtttp-rssJsUUBmc=_?SE |ӃvSi wlߒTE7S~Az?~[?ΖhCi1ٕĄ㤄>-xWZd;k"ٔϴ>ndͽ\G!Q)HO+=F9s K!r2R L89nh ==L( ?~bNaVVhlAo_ЮM攰|uιd , U7C7=V;RμVAD>~?ʇ9 ޷FLV~w%[}08և{v+KqނJl#u4(HZ*Rpp0QQQb4!++ ^ [ne޽I~3g~:^^^<:txikk#335j4 2f;F}}Tf̙akkKEEy e˖닝/Y~=XXX @QQ999 JA?NHHFk׮!ɤߏgDEEammMYYwU& x\]]ܸqcԪ\.'993gp5N:YRX|9>>>tuuqi/^ٳg2Sr"jnnÃGy,SPPKL^b444~zZ- 3f7,X>#ݔri{)%!!!\r bccCII Ŝ8qBUV%DO~_Wjkk{4F?h4֯_~;"!!`,--ijjԩSTWW={l,X-eeedff=sۨYfakkKeeIidZYYh"BBBsQ\\C:::!((qk`` ?~r3 4 WĉTTTT*Yt)=zT*Faa!{ՕTz=yyyfL&cժUgi`gg}?rA_NxxINCC.""cǎ\~o<88jT*.]I;99ȡCP*~|4Vw2Lyd'r+>+a}pf~?Ǿ{ia# 7|<&%u%W(C_]݈@qUjjZie08C^ǰk.rZqq1'| nnnS+''N 8;;coo/iQ[[Kkk+7n0Y=dSWWGMM {nΞ=K]]ܸq+WjR7n088HKK wܑ̞=iii!33SZɛhO?V˥K0)C~~>W^EJ+XX|~is)(--… ̟?v>}:nnn|'455Q__OFF_hd2G$8 BIDATyǨ7~<==2aaaVz*FQZ%>jjjj\x۔ILss3uuudddaNFU9e&o>|;wj)//Vͽ.'Aoo/477S^^NVV3gD3`0H亮k׮M! V |na@GW]5ua%{klS-18cH4iiP$ꊔ"1!j~@bVi6ۇv_6MjjeRڎ .BSJhA\scǎ>0$.R>s?=+[͟Sx7G~PLy*C=QrN7Ҵ^dz>bHoN'ˌcrrRz]vd2ahh(ŊNSvs'DUUrssT*,_p)G 0 Eò&d"(Ѣ"yqq1ʰyu:q \Dgh|LOOv^s %%%8~8BA)pݸs1::kBĴn h4BRAET*j8N)iLNNNQPP<bG.[g@QAy?-J[[v;1==`0#GHDee-a ϗNxmm'lhhh~<3x'.!fxtttsN'k}"Ù3gcž={pYTUUۘIVmmmz*.\χ+Wb޽o%ed3A8Yihh EEEXz5k(J޽E' ߏzkI?m<0tt/#'!"z,;^h7#MU.c4U龀,@_YA=t:\W#Qzi6鐗/bdd. jZ1ߏ`0liZNY/fb 3QVV~`rrGʠ+3LJDQXXt͆{,>fs ϗVOLL`Ŋt4(/8&&&ݹs@555Hx!l~\t cccpݲ׳T&&&+kON[p]O? F͆ahZlذ###҉fZiᢢ"deea|||Qmeftw&$;ju1q S$l6[jB[=""ZA;t%eһ==CPnhjxeYtzuC[aJC%y Uf2b~^/֮] H1v\}wxꩧ "J%vڕq& _l`O/$"l^0č7edggClڴ yyyIDuuTcXbjv\իW & I^ǎ;`0PQQ:Yv>Ubll (,,dBKK f*Ƿ~۷rI8dggK-Q588ݎzQАֲR~(FܩjmjGee%z=f3n݊YJ477KU,-,,Dqq1b֛֭[YLLL@ӡv]v㪇AEb۶m}6v""dкp/Qv.-Xb|>p%QĆ ݎO>CDD,""""3cDDDDDD?n ډCx&""""Zd1vփ-.ZG< Že5EQ6Rct|QОn&}nnA;QA\yiA j5[( 333qcE+L{^deeARPT^7&=rrrT*DDDDD vV1"= \q(B ^G0D @0D0d#KEJQp8x:^]̂T|@ 1 ;; B#- ann333RILd8,=Q=2{^x<%""""zT$428fDOO4ѣ'I Gd={l{+bwDDDDDtE푁z=:`ԁ|b1̶%=d5hA<ݿ/yM{`|;GDDDD^N6L:=S7IENDB`litestar-2.16.0/docs/images/examples/000077500000000000000000000000001500564371300174465ustar00rootroot00000000000000litestar-2.16.0/docs/images/examples/template_engine_callable.png000066400000000000000000000116031500564371300251340ustar00rootroot00000000000000PNG  IHDRtl7hJIDATx^p$GEk5333ӚLk03affff;\kNy{RJΗ=+ǽM$ЃRzPT H@/o H@q2/X/ H@q2/X/ H@q2/X/ H@q2/X/ H@qL;<6lX`p= _~a]v1o_4./.w|p)^ᅦkɓK@W7s'I&$*g,kƓK@ (^AK3Yھ_ --<1#:a5 .lj+ _~yCXcx|j/"\wuO? #8bXfe!F}_u85\>0h/f{gĭ19;n ;ay {WmnڸLp}֞Ka) G8c¹z0`1:03CGux=U93|7s9'l V{E8d]k…^kQG~Fvuװꪫƿ5P_~;4ӄ>fs/ՎO viJO 7\j{W_}5^SO=d0C=4>}|q_};à^{x9䐵s^x!6SZSM5U3]h@%FS~!Ý6GTŋ1|p;x 'z~;#s<)"<QX!M7?V wuWHN:餘8 ^+꘯J{cnbB׍784z% /b8' 1)pÆ:V_}(~3I?0Äz(L?}Z$c-2&`B\YgOzsꩧ-"u2aW_}*;,M^~(>c+RXoagq5+gI@'T~%3nOH2hETedXa$uʳ>FRaJH3cmwy o$^{l9aZꘈ 7kŤpB[*#`Uة~XA! [e tycnᬳΊ?S}ZV}Q?(.ZhVW^I5SaǎUoFR(bzopT5Q%s|@b8iAx ?qs饗Ʒr,>Qر7dN0ai,#e2zmYp?#Dc5 HӖwf=Xm?[_X5Aԗ^z:ɜJgUS-1a5X7_ 9caeW^T"ԷoNieL|2_y/FHE!M7]6)b;&*^o$^YSFJu*4*01G%~qbUA!b$1?!%E!65V1@YWJ<RB HPhHD'vm#< gzCWW;U Gj~f!bniZ2aE.V}IhM,6*Y}fRRAkOE%ɸGmZT]IV}{GNy &T:u&Ahc?Ќ;WŋM!iӊb5VdZyUkO񫑠,D0֮7:I1ma/7 n$z6˚U3cQPaLU,D^cS5LvhV74VūQjZO&0=@sMŋv "f5/Z&3Umd67 TzDmv-i۰A\dE:0]tQs"1iIJ%њW+EیuZl^` I'I`2*vdJ~A5fnCښ6M6$_:U{2Ujēb6!dZO{ܪx!\-laD~͋]ܯvaX5/k>'Z޴׌h*^ې[lbb"y`/ڃ2K[o l9')s)N0+,,ypEq1Ubh!B)P᱕:&#vQ $Y$U ak$mXÜ lnbfOo-Q !YcQo&!hQڛՌU{EEe'G}tm drWը*<@$̆fmC .Ɵ4&G}WSN9 B(w`E{22.6QrnZ͈ /k'c5/*R}΋ #I]sA:r>7S\;?[ L+k,8+SD )Q=QW&ܺϘ;#UkuZQL IEy_Fd_Gl)ʫ}ޮfˁzsNQzqW/ ՉHWvZ#*4n|<.B޿NKMX\mkjۇmzMfYe)*Y:C-GEkSt Wj/,ߪ)e3Tg yeY=K1Q?a BDzs^\^U;YAƪw5 z?cOl[oݑ[I̫k0_|d-EF&&ssHUZHǂr-Oz!qfkLapJs5TI`߸;x&J~5T7Z|Ro=&J~s5TI`߸;x&J~5T7Z|Ro=&J~s5TI`߸;x&J~5T7Z|Ro=&J~s5TI`߸;x&J~5T7Z|Ro=&J~s5TI`߸;x&J~5T7Z|Ro=&J~s5TI`߸;x&J~5T7Z|Ro=&J~s5TI`߸;x&J~5T7Z|Ro=&J~s5TI`߸;x&J~5T7Z|Ro=&J~sm1Sx3:۟)p!VeXtrJKFs6*DCU߃5_z!n%FGnDd^/&ym(%24O<3m7qV n"S!^f~4,5x^mzBzT8/r'm`ÆLZr˜،_Ȩch'*r /`ҽ$ڋa'XYcbTQ(pO[>VI˃G,u͗Ȕ[Wk|[V/=D0 |4!sr=(19lNrNᜰ.l||zQ~Bj˂/Rּ7<7N$.-!.}lLe=usLL.$Z^2V$D?:"˸'i  <*]8.7y:o+qGT|2&" [rsQe*Rw1 Wh6:<"\3bDԜs^3L標kuD܈oz||<Ҽ[F羰xbVOIX9(){Hfg .jL9lMf݂_7br$%*g&LlX1#rlTTTTTا5kJۼij33VmNj}963k=c+Qf,E%UWsf۞lV9U[no||эpU|Iu m_-_gWZxvSX=۵$7čxle۱z̓2y3mBqR08>c գ75bJKgc  M=TF¤QvO= &:צS-#\WW<#MvMZzK [TeDt6~ c\kR.GʵV~`"rޮEpRi&2B GM懔5l7sޥ.'rCN (׽W)؜jUHmV#W?9U6d E[46^FJ]@kSM'vB)Or/:Y^%[{l| /K+%Ǟ3f />upS6/Pɓ0{'~U٧˴ᖗXX63KnrB\UiDO$TD3Hpt(kVLȻ+-ƽ~)9]?4^JT =u폭q^+ 6ƻ^7dr&te]gBvOrڛ܎I{ ޚ4\n&/U-O5kdJŧа1* +%73]ԄzՈ u{ j\kF ҚR9}(=R*\MfR<Si5zBZFFJ &fb(0{DEUU\S-(tmZm%`]xtEi5z޶$V~*v+x b mM[n|)(Rc;Y=ɖjƪ={[ 荢2(l13'G=L7 ԾjCk]5FbmqG'VY3]pr*1XZwU=yEdے""56Xڕ:K$V W]G9>ĖļP4i֎(S/^XS@f%u2vs۪>n`JQf경ënƞO\<'tտaNSq6uIz->,H(랙~؍s]f>UW)-FZ~=R[ xNL=***.J8T:ELFӠOILR,kʛQv(ⶄU%D~J-n:|)V$80 &?ꫵWj=DVKa\TY9޶޶؍V=2TT;G$gK:0ޑsG|ʵn;=_B`=kۛsUWW6CvGtg] jU[ HTIluE"57Z.ӭr"*9k_o4 㾏LdVNDEKQavIlQע8cuVts@7N /qMqKk]b)&4ZULD]!qU3DWUE~ITkZY k"¦@@uV+itJ4M]M֦*5MsUU9_/3/xPcSz|W-j+ל,F5"ʝ~e ;g!b5)3J*,gt/mv^rDtMs v5Ut21kS$DMD%x& 2Z=tn;;M͓ov/F1p1kZ""n"!2+,XP!>#ڼ 2ŬMClX1ʹ[wPtſs8- k5n}[𓌱h&A=~Y}-Q=asS5l8rv)>xw]G^y|Z7NsZdZV AdfwQ\eW)4ZuzEG@s],4Q*} bUSJr/)+C$n9RZqIjo},gV[4k`V$EQf/=0ϵ.7WPxĥ4ÞC`iCD)[T~w߻{ }j $5UՎTT]qq72QgDAt2:cZ)V}M'w\GKTTT7C؝I\[B+SU%O'P"mf*٘8-jթJAkm^)6ŝ')Нnsw cݾ Jaf{[Gq[iSQY\ywB4s toR%TZKnh1)M)rI!T_3Q"Xht8tMԿ<Ī΅JM-DCm,v;sMkYL`^5^(Ӷܵ9R~H\nJS ׼Y<-[ER̦M TZYfʐ`.V }RDdRB\DQv;b.ܜE-*--'lT%&4(,}v"t*.j*+Wj)V{Z}JdڙKׅW""*~HH5dm\~Uݛn%#[!7Qx7zJzM.mF"(џzl[ZnU2Dr"H湭V&9Z6K".Ŋg|:6Yuew[ +/ mIHĊMe#$>+{jvڡ*SO/_[ᴭ֑]F-'}7ʣ#9Wȱ[`9Sܳ2ՎX/:Rh{`ZsQW5YhgrDN#MH iYl4I5|UV;jv7FߠS>b6]},v"DTrwXkI{37=S]ipsYr `bW𝞑˺r9V2GErLv&W(M^Juad^a53UW,_F:Uv|aV0gca$[z^KSrAcնa"&=TjrM< {F4*/my\'k7c=anY'_Zro,v%NniD5nGϹL:om)2lzEVrvq ]}k7$_{=./Jb Dڙ=k[ok|j Ǎ篛(u{Vؾ)\Ry{ȋH. TU2˹V;bUqYI gV5BZvF+sWcڻ%Ej[cŻn$IPtl9 ^[vKyQSs%_^B}FwC;hպ-j/N9&OIZ FD=ՙ'DEV6bațʙiQn#xmt/[#z#CIjxlतK=e';'Qd>n ̼L"c\'&ڊ!6e¹.U2#6Úd7;h\j$Z%=bac-hptd.ӕ\ݺ2ڹnh݌w/N"QJqR=VcDbmL1/eRsI%2'ܶu+cEJQy)M'y,ּ- O~*A*q^沦f4M/a59'm7JtŋO@tUGb&=`+CJ1LtDYdW9nNMgl:֡QB>~ǴܮΎ_*ҏgZt@{F^Cd#3Y;-5ȭTO)J/41ZKzUe({SZ|ruvoqvR7Ftns>hnF]J47#?kr/a~$3:bHkVJI6"1f#9SuYS3򐸭ui󑑪/4ȎFUj~^Ō6b5nOj™OfU3YyNߚMUJJ]xE~m%XM+#A~\~34EEES 0Sp|9& *ҡsidמyަV}#Ľ(kHTH1cỺةPE 'ƍjrjmUUC1 ƤTLWkZV8{SZ? RIيFVJݨFl6UʉH;F*s )Mf*(P=Q7W$URqFs)ўKAsvݒej&X-oFwr 7'JJJyvU6*.xkl^WngYcbbGGbxw4Vׯ&ZSշ,` TMXu>H2tڤ)"lt8NWTبԬ)uJeȮhuZOdW%fՕ޶}ێSZɩp\Ƽ$a% ''(šXqT ,O"= ]W5֋QꈪrG`X49)eÃ? V01-|fѩlGQ³rMr^נ !Q%&YUyG0S /bI蕊,993 Gǵj䪙9KfpڗCzzc7y {KZ24IfOf|[vڗIOA]AE^k*fDZEc]\Sr## v#ի]]HRjbN2鲳:jr,ef7a 6og6mZYl+=,[Z3b. J,'P""f!ι"*sxlD헉?B9^"*6+Sj#b1wjm|Xf 37##C"dӦx!Q#o\TVU*V ,)ft3EGp֥/JLT-Hqe9^V9LUqvRnZ~V vGذskͮEEEE2v%K55 J=֤ښkk6|$)RjIy9X-։fS-)) ѣƈ6D3 d1g+VwJVoz$=t~柨ɤeш &EXlHMhM"k9%#@lfBP"vћ_䩸j?:+*nypn/+7,{KdufM埑'\~1a]R+ I cY \G*fmCńLhE(0Pe9 'Ud4W={#\0qUwÊ9HW$'drjO".h8kyqO_;kn]w(q}3O ŽhlITiߖމ_ױSbK ƤTLӗm*ʅx-֦tn^JF2]ʋ sYVc{U3EEM oK}? tۘshD|df r9Y%Mn3jdUb']BjkyVrGds<|[6irk؃b pz|tL6D˷\3=B+`b ]˪ԉ8ykT*%1\PGLddjԦRGgDsڽ")仴lŋ>5C?)3#+[ڙ#MپtK(QjXujpK>$|< ,Ȱ&#?CXQwNV`HNF>"z2 +3ڭEht TR4Rz& DO†LSĠYImD#L&CT,mMkkg ^! :K}u:gMn}T9_}woLҿϪGF>o7{[?e3%Ƅ4gW<ҿ\~"S:|o-~yagEbtOKy&sh{x:[4˒z_ArZ>Ykv"^islUsje+oHgЪ̉%+%>y:Ȑܬ.KOnHGV֍8sClmK`Dڑ*QR8\5^V\vls:KRQf>Lִ44,x阉AGv:'rlFTl2z^n],|j)Hkz&N$ :mQ!3]a+*o5njfhVuya}̼̄]IHUrt8w}6MEy=qm4(S2r'MIǁב=N9ΑaKiԃQk "*Ur93ϒxN5"z/qoRִVYN-y5buZ#䥲e9Ӡr=Z=C8,,]0 {rLQYbv7_xZmgKT ңe6Q܉:.j"*Lt ~I.".mpRrY(,fǛEbУV}G>αkUUW:fRxƽ\Tjf8PI:]=ʙWNHÙT\t HКVf{șu1Bꓷ>zwLiœ%HS5xF:MQQ׶&iiFUYE,gatz/g7*C-_K{=kՒET nU'n܅2]˪g&W*fZI78f"Pݷ5䟼Ҵ ګ"!+gB n,mYd…a؛]&j %C o" sTTSSzIf-mτӄ1!Е\!2&KTSW l9 ߋ *0v41z~Y9WLS^P2*6z@L+F.O_<[JW#=D: եib2ɳ_6j=G5ɚ*.hJ}m7-=?Z(r|:3C3l6:"}gtkwzUCFa IbL-#fō 5n*h5TETr#Q1YI*>YHO#Ij'.aֱn*S$ܩ>+iqWLc_fsٗzdiW leOøq+]jjՍʮ{9囕W,-Z7q9]K("YCE[- TgTюYkY?tjh:vbUf–dǢd${?7=\mRkߙ6ȑ&4/)ՎYHhl?7-UWWRrթG)i**rWos}yKBUZT㗬U:MRe!la&"!$sI9޶GJ59RUWeQc}N֪딭8ݑ<\=2aTF~ ~B\PVVsRQ=}l[S.SUE;**n"/~;k{JkX-+ѻρN/RigNJϲ[gcyD-QKlo([H>>UDr9'#nnK2< Xpޙfb*5T]Tc.]4)clm5U W&jRşX ұ[e|8[%No1\K$;misV'zQe[Rl67&2m*.\ 0$aMa#f^޺N/Ukؿ}cve<ָ^otnmV4S6Iqly?1hUvFdLDևYRE]ɑS!«- H?XH1Jܝ pSmu{(Y-ISYuH^v|ghf8]|):&s{.ݾ(>q>Fn+TY~Ud>%5PܨƦHvf!Va[ NƤLRG]"wTںHaŐ飅rF$fBMt6Zu))I7~vTf!ՈN&ZU:3<̭EeDg:c7vM{Qh+J Tj $׬˕J覎Be5ԸYOBfe[Yoչܚ?hN֝F|d/.twZpIpj֊/f0.>fumFa\lmF&]$ ٴ\4}V,7rf9jW$"(g ?EX051/,":^#U9ZUDJ$]3VkUT*1w6wwiNX_ y.G}kq:kT4 y/`F. Mj*soo[74QoݵM]Sl۔F%mХR^FBB}ʻUUwUL0,#T׭)_J+i^%+jPz^ͱ#AK=ܓ'%%\,W#j9]ҏa]oM|83QU1ab+*vUY4fizFD]lr~6B|EO!gq_Ԋ)W^bV]/m]b5Uɔc~rQ~-EIBܚƧjTKQY#@r'9ߔz23Rv H/{LkUʿA|ۤ+UԸгO릧]jJě=8u^R^GE|v61]5hD: 6%/- V֪K3ZtXouu#}HJ5^-,R샂4DEr#_Do1}j~%:WU]V4r~F,1!Ë ТcZ3G"@<2qt>+͖"v@3;t"v@3;wiO.}软j0[ǹwR}|F'ipsdW/*%O]ЦbIt8 ]U}j&{S|bŭMᤫd֔b"$*4vv3R;эK$yyٶPj͝'yT`\yT*:jRdĤ _ ȨŠ6Y2RhMFKr[s4WnCf݇GJͥDFvItWkyfwxbk)|pw5 u Jo֍_I+5x5&jz.%N{?.;HwJwqiV]r+ve/ 5\kLm&]X7Ta9?SLЦFK9ė2 W"C^bO謺_9ݢ^)CUIo|)Ǚ^RJJJ^^aB 1L2& e'W 1pJvەYfzj"swLWvկD={HA~I&Hn&1Vۢ)5 |9:t%5dXb"&k6(*]\喿%΋b7'k1YCk5obJo6_c^uhT:Z砫aT`Mֹ{S|VxVU2Ux37=" )4ֶ5h\QMN-4?FzNй%*OXSԉfvy/qrS}3:]NT], {m{Q$zBiQ_"W9UW$MwadWF,EϡH*m}Ţ5S$I|IyrƛEr|?jP"ƜaKkܝ DZ;s1u(G\0Yyar? ):)|hG"=j+\>3iQqn2Y4L֩499N Dͯc%E=@4ɟPN&38T4YĘz=bMR#|[J2L{ror.^"=S/+ؙi5",D璳^:"DoJ誛~!Yhnn! Dg)ɰ㣗kV5s] kk B[˦G"tٹ"9{flxhtZmGJB `@2UUUU=ìcaARͅ4%8*jqiG{!rugnz] v'&qj"zG:)ķb[ܗ4Xѕ;{N7wֶrkl/>Q=RSYӥ俇b-\YLYbR*Qrr,H,kUEW.{Qv5 azDŽG'D#7[j*9%ٛa'{Ӹ&Ņ`3-z=DNw@cV\8R,'f 12haa8-~Z@1Xګ W[,TپXd^b]M8n^IǗAK.ڙqÅƾ,%LEՉ sͪtFvV#/QP F=DV|(҆C'eHbDr+`N(Vfkۑ}U]ٺ N3 NVwU,jyWJ6NԴkU,]+?)V01"S2縫O֭DK-WaJyvޮN5}Mr6j3jycȋ咧iT+u'NQ|͙,)QRU"]8:v [1Ļ~vޝcODʫJcۺsw6i&TFTLwVkJ.3ɧ`* ^Gr#$1YaQ>\9fHĪ^*l&>n-WEjԲW-1፵=fbv|>SVFoU66 i!g ӰR b*~  +s|SV'NӢ LVCv =~B⽘իU$y}yeU(T|W=+{4{Sȴ請Ի9i\ӌذ&b;(p&Q5zk^ܑ\Z*ClamTsy)9/_,F?Ww>\N҆O.E&8lXOkG5\ȻcnKh+J u+wsqUw y"ңREJ[y$m$˦ZJBcӞ57Fp{4"uQJ#1ݷ5"@38|T. UH2p_pٱ\kartmcظ]#gRW# 7TK\ϴ:SGmyUMywV\EN2S9.o w*94ԼOZ&]:+Z_iJ}JaFi_]Å/tt25IML0b^mB̶$Xp'kCkɽiEE7rIBqJ3Ƒ;zVYJ2kʙ~ .9=là[3E>𧲍F4T?nɻ~"*BQ4#ث +se" 'n×eJg1BT,|c6򹲜ck5x,#HVyBMž-i>LjzU`EF9Q&ń֢.,YFUʌD}"е5gb"oWoDڨ_jMb^Qjs օD{OxpiYkԽq}JRO<Ϗ5\M7* W,hKQ:,FgUI$Hpa,Wb+.HMUBMjZɵ'5JXSP8s֣=sb"[M/?A (*qn%M,f>-K0خS/wiߚtaT".@l5ӕɒDr,Wq5Z]Zɣ%F}/yqJXuO5F fIS(=#x:[qaV'F<:\`oņ[Æf{Q'ܴ̗%/KxoaAaZM5!ߓ^QHݪj7ΕÖ1 auڗ}tΛ+ s2R]N~!U}:oyi~gKi_ή? Nyg-9{:-9Ett_[n΋ZM4}#/>)&2c2+s*1wZM!2Xc!;IV!>6[cEj9OT+ Ξ^hŭ 'BgWZ{|% X0j{J+Q* rv[ꪨrf]Oϖ BБ 6T!6f3ٽ«hwRm껭&s`AE]g\V7u]L*_8|T.w$9XTUouvi:vA?Gr "xT%{%ǫ6gBuo< ռ; QCt-#FC: a|)Zs(>}Al?`eiK輺hE5zwQ|9&In~JQQw\kroC/K%4fFIR'OSO=z;]H tGS{3$X)H8ol!ͤ9Ghhz(˚;-kSw4HG}<0)F5̱aZyR.2I{ާɾF~y| =4;R?^CN[?4*s^Jyf {9x%< yt{1~7zvz|ZV&R15bT_QmX7[5Q.دu> c3]]&y3I b #Zc ͽ45 ϵƷ.~0c.Ђl64e!;r/޺E*Gn"عTJ.kI\fZb8Sntr[TwY-m˅kEa&Ű|M`nxPY3-hQZdHnG5]EMLz.s'/Ih[3r% C ,Hpj5j&HD؈#Xbk2OZ982bs#kzdqMmyjۖGo;<;~U1\4'nxF'08%6=۞_7wI}h^w~m?[WyKM+rť6t="̗ D{3F&ܲ=T;sI6ndRec#Q{J]]T=PW.'ϋ= V5nA:LgW7cBcIG(SY\;SVbYjlsSfno¨}f5f Y1'Z׮*6[練CaRQigM;L*򍥬V]M${#Ŗ4Ut[*{JYs1#Qk}e޲#w3\p.R*is6-0]Å"=#=R-$ZQIN3\-6^`$fbk\|·dR#Z =%z+詺TMteGŸ<S˰xbP"uf6jձ7MdTETJ•+ֈ!x:gia(MfIb5kM'Skjjh]8X[T|*J3|EtE]LULLU03Ta?s]|fW];Nn{~SG"5\SFu҄^#eNH|/VK"Z;bm5 aGݵnom^2n[p+f ovWGɈʈԈ Pt5ˍi'Q)'sɾ97 ϲ[gcyD-QKlo(N}B|ڏv?j(rO3\ (&I\Tʼh0>u3rC~YUdETT]js9a\Ix2wEʮ!*@ׄ~YyE1ָ-+Zu' st[^j'zf4 FʲF7trrj@鉄GodͩJXKpObJ=ΓsD۫j*N"g09J5:y|Ku֮ |)'׬$)p)奡 PۓaZ~$ԍI)97)<|Oz6øf*1bB$8UحsZBa`)e̗h 4){mpswN(] -&B 1DkZ""n"! me]ғU*qc{-f5:8kJ/*u\Yu6ݒubua pN2~.[ wOZt1vBjjj'kW4>![ءzEd6j˕cD^ڪc{Zjf'&?,s3tA?Y\^5!vGа:>:/sѼoV{/=\VX}>'+OTeN T-IG#^]SfI%j` 啍 }N|,E]uTbF+/ky\txԖ1Qt&èH#]6+rEr5Q݋c+N]ëG[婧IOIړ铇E4!<މMqIKjsb}7[&] ,jk> CG =y?qq=գԧ<> o6Yfnz>q&Ə58mW9XNDj"mUU\(#===$nF4hѩQllv++rDDEUU/2d/BhƚhŝŝJjYjkV\dQYqEܸ`x1&iQbs9˵]˱9Qv*n#(Ɇ1I]TʺY#†ֽ;GViI6KWíU(SK%Yq7?:sj6槮8u|~BJnr F)G>Y\7;HCEUv8ttvn~=X-ݶo tV5E'ɞ:="@hp$6 0ӭcq; UڧDsI9޶s{Ak)EbGƍ \vF=^4mr7Ni_/5Т;3k3EEMJ[ᐷړnYy,WM+ t-RMqֶp!E@ҮƽOPNU)p!:|X3nf;M1k &Wi3̹&: DE*Ȋ e 2.vzI'<:IF)&{͜ dPsP nwQ\60WJڇC$r2k!/9ɽ^IiƗSN%&yxB{aSW~jWnXcU&C0=ʎj2TTڊBİbQg&&ZU>Z))DY69L]4HQaHKzd!qQOؤv\$VDO\#{hX\7ֲȊo{$ɮk.FPLD93Z/9so-W=S Kq UuKÆd9fGO#ѸFnT*4mWo8+YZqϏ,,jŪaa=r}N3}Qbu>5h ëvZġOj?VUf&9<#UF5a"57V?oRr?q[{F(F*O5N95I%Öd!E_%qPYvY,(FU3EL3Ex^Zэȁ㘬B!R*.% 0ꞑfZf= h4dF:;ծk*****˗)gPef h4Xq!jrBj+Uj*.̌i=jJ ƛyK^|dQBk D>f* ǁKF+QsZÉI jԖAH+=%DTXgi],:W|\l]-4KFjKy5=82&BH w޶.Bk#Dhfy,۳~GU2l=E%`­&o,YjG<ѝמYdQ̎c$T3U,DFݍQ&x{\ZR_dy[+ :9F1~Me+gn)kZ”fyk9wݗq6f3)TI/1۹__O}9s%Ky[KUMVM381Q3tȉ.m3Ms>1v b,p[N5= l Uj"Cs70' xJdEF{4Es߀G4ë/ٙ&ҧ_&_V7?jQg.׹wKm9MfR?[ Fjfjn) O &#&lL9y"L538l^Sr#gKrΩ,jD_DvpgWLnKG&.zw^4c3 8 V\ob^wXGicH[ѮTDdݍ2Mu}cS70 JhS-E(\NYmGWaJyvD^34X`,=T,&=FDknyGX?tDUiO"h2}1Ό:z0:.sYvX{esR:瑟;,̎71*G(}pMzMbZcV{NQzIZ6"5Urd,ɉ9/cS)LNJpy53cpbBͮ"UEo%bӏrY|GkEٵlDjf6 rKupZEX 8؊RvQa=Sn"] oUCYDRk0gYD{W67ƴl̩W'ZKIpzwR/?ˀπ i/38#Z5a.ǥCl'*ElhT.h#ծMǬ4*&ؒqaxmo!B糏oɗ툹/ZơNM Z,Ei'JŬ=vk|2&5ge\>a]7esOC?7H I𞒔ILS6<' ޵܊jP99io}nIؑl}rtUXdѢ)n"+&Z*(YkLSc*#vU0CgV;a6Aجcj5y:L)5,9}JZ5͑j=j %2W~'|4FDY=3H2,MXφLx '~߸m22E77zZ U73DUEK;)SiqGK(97K7!m9oejUwDX":PUbD|nʻsUݵ$d666ZDkQ2DDCvcoJ4,=5qkվI9rgr%y<*d.rlۜJY\V&'u%N!qvR:#VQiִg> 5]pmJ%;Y'vkК͂o``ܘũNuPEb@~[ 5ލ{gO[{l+ʋz1.!ߊoIy3`UUQ6Rӏx\5UТ*e5TMhl+sR齊Ƀ14AOȭjffG1tHJR:J %Rդ+KIhk{M"ӑѷE"_Wwf_cYU"5n*P_\C\oeM?qB= %rXF? g~K._-UtJgVOtt[*˟I@:36NrC8qd$}vB]'jTSY?8sSÏ'9! O It1$8qd$:36NrC>c#[SILgTpHuOg>ld-<,}%;TF"Ω ͓|9ZxXKvoO%0ESÏ'9!?8sHSʟJ`:36NrC8qd$;!ic.ڦ?5?uOg>lTpHvB]#Lk* ),|9 ͓셧GjTSY?8sSÏ'9! O It1$8qd$:36NrC>c#[SILgTpHuOg>ld-<,}%;TF"Ω ͓|9ZxXKvoO%0ESÏ'9!?8sHSʟJ`:36NrC8qd$;!ic.ڦ?5?uOg>lTpHvB]#Lk* ),|9 ͓셧GjTSY?8sSÏ'9! O It1$8qd$:36NrC>c#[SILgTpHuOg>ld-<,}%;TF"Ω ͓|9ZxXKvoO%0ESÏ'9!?8sHSʟJ`:36NrC8qd$;!ic.ڦ?5?uOg>lTpHvB]#Lk* ),|9 ͓셧GjTSY?8sSÏ'9! O It1$8qd$:36NrC>c#[SILgTpHuOg>ld-<,}%;TF"Ω ͓|9ZxXKvoO%0ESÏ'9!?8sHSʟJ`:36NrC8qd$;!ic.ڦ?5?uOg>lTpHvB]#Lk* ),|9 ͓셧GjTSY?8sSÏ'9! O It1$8qd$:36NrC>c#[SILgTpHuOg>ld-<,}%;TF"Ω ͓|9ZxXKvoO%0ESÏ'9!?8sHSʟJ`:36NrC8qd$;!ic.ڦ?5?uOg>lTpHvB]#Lk* ),|9 ͓셧GjTSY?8sSÏ'9! O It1$8qd$:36NrC>c#[SILgTpHuOg>ld-<,}%;TF"Ω ͓|9ZxXKvoO%0ESÏ'9!?8sHSʟJ`:36NrC8qd$;!ic.ڦ?5?uOg>lTpHvB]#Lk* ),|9 ͓셧GjTSY?8sSÏ'9! O It1$8qd$:36NrC>c#[SILgTpHuOg>ld-<,}%;TF"Ω ͓|9ZxXKvoO%0ESÏ'9!?8sHSʟJ`:36NrC8qd$;!ic.ڦ?5?uOg>lTpHvB]#Lk* ),|9 ͓셧GjTSY?8sSÏ'9! O It1$8qd$:36NrC>c#[SIL|-&p>3Y|MO t- pj7KPxZ>8z)-5?AƼ$W3*ILD|©w7YƤZG yUXI{yJuge[ۅOܧRRYcg Sk'.:ۙaVª[qadCoVmL_:]8)x;hblpRL621 ?2+b39-RjbACn9LE7en5y:nîjJji"1U!Om.y 1-sz䖧M/<"(4p{Esj5WB *vҮXE΅iu֬ V/ibA1ԭGOԵ{s*^/URս^`$]ۆF*&,زDd^ҵ{#zVUmwT6tR\QMQ9V&5ITdczwX|FлH V6,cDE}6ԔTr'!ڎNh }pr%oΖms;_IqD^1<4s^fvűNCYBG.ߐ'4_E:ms&ĖSG{EQT6N:6PS7mK4WstEWBMX.XD̋)vJ2́ XMQn8eO.m6E)b73WQ}kIEd~;iȩՌL.fKM{9^2 [g&Yk|xS{(^D'2fS}ʛіLƶײNe=;lGf'ey!UMܯER5ZFCj-`!w{='?"_k:FK@MSjJMDMO|-[eaY!mġRN ?\B+b58{K{ypiWt[W.M߾v7׺]a-+掗X}K{y]W;k.GK>%p%p< oq\4@cz{phul u~\F oq\4t[W6:~ޮ#^u:]a-+u_|oWt[W.M߾v7׺]a-+掗X}K{y]W;k.GK>%p%p< oq\4@cz{phul u~\F oq\4t[W6:~ޮ#^u:]a-+u_|oWt[W.M߾v7׺]a-+掗X}K{y]W;k.GK>%p%p< oq\4@cz{phul u~\F oq\4t[W6:~ޮ#^u:]a-+u_|oWt[W.M߾v7׺]a-+掗X}K{y]W;k.GK>%p%p< oq\4@cz{phul u~\F oq\4t[W6:~ޮ#^u:]a-+u_|oWt[W.M߾v7׺]a-+掗X}K{y]W;k.GK>%p%p< oq\4@cz{phul u~\F oq\4t[W6:~ޮ#^u:]a-+u_|oWt[W.M߾v7׺]a-+掗X}K{y]W;k.GK>%p%p< oq\4@cz{phul u~\F oq\4t[W6:~ޮ#^u:]a-+u_|oWt[W.M߾v7׺]a-+掗X}K{y]W;k.GK>%p%p< oq\4@cz{phul u~\F oq\4t[W6:~ޮ#^u:]a-+u_|oWt[W.M߾v7׺]a-+掗X}K{y]W;kQ Vcm}&]K t|iLly(7P5$o">=Lp2wUW40ua1Y;4T sT觍bTt*'䜗ti\bs9tc:"mkո| b9g=^Jֲrn4X  ֒Y^Oߛ3q]{^d~>ۗ$4LEY?^ ErjU;SM[=}gh<{5f5\o{/à ۶imJuUE%QE }޾xg7&5uO}YoPD ?$UN7#dq CDqbb]FDv!ȭr"TR/`M/ +LL^rZ&XUMˈ5J5=LOpl 9YT'ŞVo@HG5QQv=ծg^gܛaO9 $3TN4JiZ$SnǣMzm7vm6 FFuzK3Yc9qFO\FGH6^zl5Չ']| UDn$j̣'Yh#KFlF;sUQMiZENڢz3Ub)OyP?+Z$?3=@aWJ};,f$S#S|",b2Q𩳑*tB*Zob"&i=a+즸Vmbݷjg[?zu{6^]!_TUWj +ܸ;@|a}#]ݤNU^.H2urM\lY(˦nJ+"Dow80c'rabs]Tg!/mUDnػ Qn2jG.'dsen$ eWy'q AMSh0it|u Bl(PkZBGI8 ,-K̶ G7 ¬R֟я2y;i4F>2vl7U5Wȍu7wK (`(PU2mW<3_Fpq9My.Glv aTlF\ $@D̘6"SF*MzMSd)VF\{TR|ΫZUj^ˑ' jZH>w/7\ mRaUU}btsA]H5xn ˞Y#6*DEU,^D1o&'Ζ|~m4G3ujKm\,Dz#r"ȹMMg0וi-ȭEJidybW\3 QS5oV5Z95/܌Wĝf-zjŠ!74-)@1CpnQ jz*1z_ZӺqR\fLL4R+Y۬>*뚲HLG~{I4-J;*05WHI4HPWIƖd6665b#Z֦HV=5W+MKf/#EŰF~_1rGƒ+ CO4˖5v$Zq9Mt(*/bX1]Uu&[-yHTӡ k$ 3o 1,FkbfyuTTօ>_3sm jqɮF%%(m8tw]v۰b:֭== U[퉼ܫ53UO.w ir&q˼t\qG*z?Xl™ BN*&h\ڻܱL]Z/6gۖ`RUGVM-hl 'E>~Af1?.kW:t,7kMfIxhbM-*U]lyu]u>ʥѽ.t\aT+ɖ3I ۟c!68Wf$`-UO4 ԉ ^'i3UL|tEe"wUĊlaLK{ %+/54Hk3q?Zf59K_O˯ts[޽ѫf޸2izN%٥=ݳ.珔Ce+~DUDQuĹ5G(3Z?O}ś3\s3Z)뛌7L*PM\VI+s.?3N,3.ULk.1.AKfI?S\B\"zTۻ4K'O|ϭ1ޑVgZ(Yn#׮sQ:T^t)q|KF=1/*TvJKBo*^nnҳ^JҒzÚny.]L5IunbYqo[rFJaJBF7Yw\s}UUS< keiuíTn9+_KQk05.|ok*e@t(oK `LS+4Tr囚# " _`w 5¸|lk΋j7t*R֎pEۇ<<3&y6"ַ?՞\JG5QQS4T-&%aušVU"7(Ȑt7ŽA:%DR;0kTwY롷m햸Ʋ[b'. ߧh˅qenzA58SQcػZM]\jEJ4^[jZ.2ɦi4EM=Ә7QEMJCswZO"){[ķ.VT:mZ bl jkd7YR:'ma'ayt0S nunJ{eH>3^YzZF~g&7&8mv\CcN.rE*7'MśG6*9u^|IXoC+< 喴E&G;K/ڍ;Mg 4UQ Ym&j0kv9w47Gu nƅNO$Ͷ=B=R9RRU4xFSuUWp-&'}XAEZk/`UhձZ W,S{y.k p&r^ĦNXU*D5[]b9=l5dD(VJ[VRZ2ij+V۳F ^u6J|^Wؿ{%rXb)EJj|r\ꑯa^XX1iٸ}COFK3^DD ׯVꬫWm~VoEdr؍V=2TT;GS,xɢ(rqsZ2"msٱLX|0ū$2VZufAVF5ǖ.Jgvv+^ ӱ6q؝s\JV.IQ?̢&Yl.͎G&v; Pq9Ԕ^5ǚֵfecԝܥK.5\)ky<:;;LMDU^:Wb$Y^hFJQ{?ͻ q`׸ ܬ85Ɵ |~m1`ȶC\nR4N9'~fi]'ҊNI;y,mBċ̢]J2gU?z)7Ο` o+.b%nY0=J+JSQ$sy3Xz3waꒉRLV- )2mPZJYQwnV㔽K2+]/{mɋp!&8MbG}3UTR47G}kѷImm0ߍqܳ>6#d8mrLrDU?<ц#CHDG䭏2lTEn=T~sFkXZHjkbӎHdy/zTc2+umy9)$zx85WsM(h%%+.Uٷ 2ad(P FD6""o`iVއuJU""n3ҊDTTT]C89b^Yb<8M,Z!9=/BZ &Y++ԉFEkm[Еwso5FKs`5SDi}cV"#aT᧭ ֹU>c5n['FH{a2O%*|mTڜ&5TuFss]n6nvky8œML*{V?Fkj~W˻o>nlKL'sKmԏꉯ"*Tk]3ayi%(If/tnx}ryk?Vǭ0=2e`eNBq1\t[V/9}(_Y7X8MB`Xz_L9U\-&$;Rm>Y?m,g4e|}']m_c~m%-/ywoY}"eį@ UmImm#wݴ;ޛ)fuw౉]?}}c=#N8 "]ylGLyٸ]Ej*uʈɨ\CQҪ!.mEzo2f#7~Wf[bfk2Qj"5"'i r9xiSZ$"φV6RiezU:䈫)c&2%Am25:+U]=adM*sS)\ֶ:#n}7+hQ~F-&}'ʣ#To{BYqP6 393 VK|gDz]XSU\yˑzpڶxC*pD*ΒKWssCTMF+Oj.Yyp!KFdX1!b׵S4T]T1۪ñx_YپY)eʲ~vva-a,GjW9{H2%RM~Fƒ^XUePޙQ7rddG9rVyc>9cU[MqS˨|(/_Ň+ | a_ׅ[V|HiעzXƢQ7쾍7X?Ye*'*ŬV] 33Q#Q뵰٣X""jbkܷ 9PUnp%=Si$Y%Q9 G{I%~ĀnN+Wt엑]t^L3 ݽnm_as]msPiȩZzY=;z*/0Pݒu^2RM?Qz?'H b^](DƞjMŒ`Hu"Uɋ ixo8c.Z2cfy~G9QDUɭɨUj)aU*M,\KVm糃#!oNt[}|kbBDג1ҹxRymYu-EzYQQ Y܏ / bJQȎ+fuFWr \DQjuwƍ33kU]b_TJIsnbt)-(TQ[%|&.}%l.`9*okH u:ܼ봩G[VF4jEv&|^Ro v \X ؍G&J\,5MLi9ָsN dhtgCm1FVO%=y.O}'zD.Fx2VΕIZQ1Gƛ?N@ c["7Iz=%0Pߛ6֦꫞˺`O0&'SVB:,$Tc_W4Uj$2IN"Lj.NcY%ۓGDN^Tv?IHhz"W49>#ǁ- c26&n{܍kSx$.[r`Rȉyq#UO I9%f9te:n7w%%Vf[f6*"mXnTn{E*>^D?ػOu$:.nNٱQu}b қFLrV-_,ſVb;][] UW=r.ܷtt坬޿7¼|[W p- B*U "\i\g1 iWͻ=$h()%8N{LW5Zإ]ӷBz$Tc6HfMJ5sرQs? ޹@,|E Z׭J`MV+xMW/ཫfXBu<kZ74[$MI'f԰ڭIgwNjS1j5jZ#UVa~8aN3H,^;b>4"$Dbg5nKiLkDqmj=u^*A7=4ɌGNɥNޖ3qGV:M4pfRP+ G7Od6"ECZ-~c*59+^ Pй$-| _$5{TUx`5)dB)y4ك]ZUefn:fn )*u6^$Uɐ`jr"5W4bbl=4cja~ՁU N-SЗAթw˜AX88TEAWej4]g 9Weʆx.-ZO\A\M4),9[eKSZ(%ȃN2PKS f@Ole7RO3Ec{Z~y8%[{c)i'gn.aEJ-֝)TKJnd$GDz;Ur\,)qn7>?&qZQ~R:~Pv:~-$I2&Ůhf{JKZm_&\F~GqkiyY%o#):Ps3zFELn*LD^Hk*Ca%3 ™ *lr6RU9Tĭ%q~Rf~qϽ f!E\s,wQjglsM#"l:ZV4\ ̳:݂1e`U%cTpc]98"G˯۪\MƦML ^nR֯Q-aԳO6J<:a,q 1H nKnsͿWlWK&"JQrkQD]T4[ҊVщ?%)%)X9;=X9Qr]֪dʸ1gcT')pYz_3Fc2X*McֹQɾˍqwq޿LEdBbULv1wk\3tJܸ^YޭM&ZԪaT$=GrEMU{dD'$twCW'8Ujec9&hlV/<>).dd/#XUkWO69X3U5Me\M{$~HG^鱌4Edk cFUi?jF>k\xΆOB\Z8~v3~hTIj{{y2]^:j~urncff_Qg4͘HJ Mj2dsg^T̪fΔ=3T5 gtYGZyN]uelj^{iu\z$W3+xKHa%p)>MXpUhyάf2N@6֘ x [ܞ|K<.3HG7Pz45iufU&ZJݦÎhd6Gk5mO:SJ*aMg^m{H6̉46Ѻ[ gzT7r:&kԆ^$TDEsLGH-$D땊OR[*W,]oe4 `uqGL>I R"=!Ѝq`H]uzt\'&\X Uj#u\Mת5l{L;s)wIT>y-ެ/R8zSFQ 5]3,e b;'FEjnM?)M]T+ڟsk<wTP:)%/%T7P]15nF12YF cvGRy&,ԜYٖӚwQT'7tbU%MiJ5_#\'S[Q>j}~GlXT"Rf̾9yv5S3-:aS+BvP{tBOHGW+ܑcFr:bEbd\֦[\kDזi\[J#dyZ4%M@kyD{n{vJT6ᘆYTi7R^U5k^5[1jԧw%OkzAқ.W4֯.wMqQKU)Pe&646c4r/u5%S)T5Z|r|6.fem"/Ggtع=#:Qxrϸun?"AeiRu}Q_ɴ\iRu}Q_ɴhӫȽݿ\D ^ixgiE,/DԦ]Zd"&]S$ڨLU ff^NZ,fA#ݓXƦj]DLEL2.(*WäLz1w*_U?mΖ~R"[KLػ\Qا⏟ȟDɡEh7bG)XHn]fJ[?-7ű[jܳoė-H(ڜiSYEj@cVh&-tKukJ/\^/˵Αyb%8< PiÈC=z7]Wֹ67RR'jEc7ϲITd{"žm Pj:psNyc/ Y9.$V=UC4kj斸-i1iS윧GlV:D|'Uȭj"f_oRR'_cEVK^Xrz<=Uno=].il={uܥ[dަz)lχYu Yj\Y)"RQ/^›Xh󘊙Dc^T!̳ůMm+jSYPzqLcec*63ZEwҌ8{݋u돩jZ#<\ѻ1 ²d瑺p32̜MYL"ƪk7 -ΎF3YIfnb2C6"6ʢ~ & _6wJmSbn_ +QE%|,{WkU3Uk}Pa^4gpAJݝN'#B6 EjvY {JVڎ$կ-R\zx::Z4ɛIxoΗ+3j1YvzUUș#Uȉ-)_I,7=[5'Ozš\wn-Y{Nwձ*Rs'2'풧Q\*9]q*uvц}]r<վv_O=҇\9}iy/*{ ]$,?kC;O{7't2{ٽ?,Cu!"|aW^i^k3~Y̨1v~Lߖi)s*; ]$c҇*#O8 cnjV":FF<*"C$bnk6j+=NF9~H޺geG_$iL9cD{WUs\wiw`Uq*Z 9xWjÅ 1Lt ܃B%Nxڨ͹E;'B!2< !>Fͪ90orUo7W^{,ʅb}y8ݼӬEJէ=V%ttg$iV47Cr孷cC)ӼroC)Ӽro54@NtZ{Y:sH]I^Z- bS? KI̬)| 2,#蹥ȹ' Yili Gg^{ܲɾ&wXII9`yax"k49 TZZ8V""v.}h|g&ISSZ(%} 訽'ǖ Rn^+slHoL; >9SMkO5s^FhՋz=^Sqf(stTܛ ܫw575iK?H6C$JC~ި#]/6v1#]]* _םr˺N#[0VokjsY^-:UԿ?*k[2* ˎGĸ$K?.Ol h쭤_L3G~3gJ>_q_`,wa2~>JX?xK6+a2~>JX?xK6uy;˖~ș@1+_w27eYAW.K"d4ʉ|ԩPs-lﶶyZ#I.I&%vudǯMņMp˺w~$>g|Ԃ?gkޱy5&FՒW;;pfJfz2+vnf"`CbSyX zLjsTMi(} 0;^Z곤.UU6D ^hăun7=Q5CTMgk3un4lq/*u7⌵ž,ryNRT=Yc@CKrSaFw^|r|6-rrIhSM8hT]tiqEcwv+g(zDL )EHs!3z XhcKK6sUUV;W\WZȃtꖿ|zr˃7Ȗcne-ѽ8db##ge8t]wjF2rEtrEDgVCUX'5.JEB*d)Eq^9")hJLDW˽6Ar͊+6'M4{˨5== Y댸8HMϫY a\g9D{aIDy"nl]RJ8;v/U"ݶukEՅ9^$kg ac9TUK&mhrtg $v*'k8˹܅]>FT-ysO76u)5Gnt3lOQk1v#%e/ȰF8qg/a킱&K *"z]Ofna{b4+ۊj4o,E#jn"WWSvhSl .@|Gϟ{SVtۻ{!Vjqg-k5{0O5o?/%eOakd"j/M+-rO9(%gE$4?kC;d6ll '/iao"C1^_smk,)rVuWV^:&*vuG/Ci-f1h-?T|'cpK3dHbVv IQlĂS>vQj;CqjR~fE%YdHUQ9t7*nȊ53E;T3#u} -.qETd^&ș5]vI JpZpIB ؞~I'|. ]%REi7w>~J.ڜN{D_1QnsLQG6UbnjVbUj-opR^_4!W.JDUDDk[\?r*ʥ^.I\~nb5T (Pd,k!j5jdD؈5[U, W填F,¢zQ`Ef\W;f %]-^G2+3EMv={sLjgG1HصMl\S괥r󙿊tl<Ljiҗ=iP#=2q"6$r^u:轋8Z+b*JפS UU^YJ[7M}'lZddeAJΫX׍ 75&؆Zk^8 qO6yliydkZutE;N#j*ʹ"'mNEAq-Gj:]2P35鈰ܛd&"/:W"*e5dFKMI(Xvmۯv t`,VeJE}/"CDsztHUkuW5<+zR#+B#'-[y=,I&nngyQ{խBBO{7't2yl@\zU'tn$?$OvuSùꪬw9#PGok2$*~l;ҖzøpQ;NG7s!{e+GxD;7Ar#cGtUF,DjrԈG5NHC~)`=mnκ3n/rTݒm:TQO=I۽y[[5++[idwsu|(u;kl[蛍5W$gcj6$Hτs᬴A  Sy%ٞz⌅.dpoZ eNeLM\Y+ѭ+v{t @(ht5^̣dx>) q#]uc7I$+QY9hpZٯ 7YzTTTC~8g]VBZN{+\ŔrslFGb&䨏znͰH)wU;yd||S?31K ΤqgpzZSr ӱ 6꣧姢݉1 _T\:Z-v@%UKaMWÓ&.͋0k+j<;>mݖѮZ^/jGipsltHNCIku]ȉUbS<ϬXJ:[姽TX e֫9OZM1Ҽ Gou[&&k$Y$M[VxojsX=X^%NeOakd"k/G҉,TT-?iC 4(Ι98N҆ѓ{Wå^RpQ[&š? rDUvo^1uBzsI6nX'5롽2\JʈėDtbԱ*RyIqDž{ו#a͝K4\AHԩ bslF93k{J0]ѾtY v6mD\کLyUS?DlV7cNIi=^RNьV_^my2dx쭤_L3G~3gJ>_q_`,wa2~>JX?xK6+a2~>JX?xK6uy;˖~ș@1+#Z$t኱cYr.9;^j̓yb+5NV,.w) f~ qwB<Pn/ȱD|D'mc]]4cmj5q_.{hd/GR,O/;Dֵj""&H | , 铘*.Oa&",ZZsove*b`;iWC])86Sj-VFͯO sci+XFkTdIl>#A1 &!2,8{s\Pl+^v94ESkm*h%.2R6.fem"/Ggtع=#:Qxrϸun?"AeiRu}Q_ɴ\iRu}Q_ɴhӫȽݿ\D ^KꑲmJQTwF.x1;rSJ.sy%.BK\mZ)4?DZ{ . Wl8I9\iib[Wc9j%ܐ}Yi CY6y8ܓl7c-.ajM$Q#v2k?e-1SMNYs_/1bC{UjS2-WhvӒ|yh躷PM2Z nӮZcjR̙Qɚ{QL*wڳ"9:$H=62]sܛĬ~,g}EFtqN~EWy+?ĥjib+?ĥji,_W{,g쉔(8T>§U˶:7JsXsLUv"+DOBs2[t9An.`noyR>rx<%y^|wqΐ+8P[ykH22zm>]%e!2LLQ֧q'.O;A- *3\ U69D~#;G#"lXOGk*spWɤL\dkA~ň;(LÇԺ={n3z>E^8IW,I"nP-kKO5"N(ع=5bhV/|G7>ո?ԋ!L=WK1G &sL=WK1G &YN"'vYr(&%zuUxINFNXF?40aspw;",B$ٺd"vw>Cŝ(p c1")tUֹQ] ]WtPH.w`}y5/6YP+mޓc]V!6$I6⧢&MR+3S{5zL9wV */4R}:γV~ïA|£^^ҽ͙&>'&eO=z  l(i?1+5vV D|qrfKk4~~. K& bhV/|M[H 3W/wVR/P 0_?I%,W[W%K0_?I%,W[W%If:hae?dL7Bv=vjDFDb'T>*TTKbY z_JmEr2Z Q2|Zhׁ)Lj1QѵW{d(lOe[3 Y;p%4^.ք$}e$Nshr:9}LcګuZzat7aډ QF鐓q%QvѭjqO81|"8Ƞ۠=KdN[/gH`ع=5bhV/|G7>ո?ԋ!L=WK1G &sL=WK1G &YN"'vYr(&%zDI>ffRY'/7wkSIJG^uYAƏqFnV ]ȉq Ea8{ >engSJm)52?ME&%DIiR 9Yxilj5PԤܞ75iSbVnFU Ko"~I(Af8m%2@747mQlO/ڮd ^&*Z=6SI}3q+ZSFYvn_wM0Qfb/; [яԯ[=k8;v&:BXjg/qJY1&21u ]MEY)edܘVPܑg-N:.k72{F/I2Ў&) K n4ȵzvU!wT躼IU#$6/5?j⫰E޲fgqsl8__3t+.ϵڍZE)2B[Q .ܫG-i koHWSmA/+yƓoVK>Ibןbb]mv9Z|8o DOx} ogM}05J+}Fh/S-Į4[1\i':]s4zbWES-Į4ek讁ێ9= x{1+"򃩖bWE qW@e=ƑyAx{1+"YZ8+vxA2JHe=ƑyBz|};q0@Lc/(:o|%q^P+_Etq| ^[1\iLc/(O@un8>h/S-Į4[1\i':]s4zbWES-Į4ek讁ێ9= x{1+"򃩖bWE qW@e=ƑyAx{1+"YZ8+vxA2JHe=ƑyBz|};q0@Lc/(:o|%q^P+_Etq| ^[1\iLc/(O@un8>h/S-Į4[1\i':]s4zbWES-Į4ek讁ێ9= x{1+"򃩖bWE qW@e=ƑyAx{1+"YZ8+vxA2JHe=ƑyBz|};q0@Lc/(:o|%q^P+_Etq| ^[1\iLc/(O@un8>h/S-Į4[1\i':]s4zbWES-Į4ek讁ێ9= x{1+"򃩖bWE qW@e=ƑyAx{1+"YZ8+vxA2JHe=ƑyBz|};q0@Lc/(:o|%q^P+_Etq| ^[1\iLc/(O@un8>h/S-Į4[1\i':]s4zbWES-Į4ek讁ێ9= x{1+"򃩖bWE qW@e=ƑyAx{1+"YZ8+vxA2JHe=ƑyBz|};q0@Lc/(:o|%q^P+_Etq| ^[1\iLc/(O@un8>h/S-Į4[1\i':]s4zbWES-Į4ek讁ێ9= x{1+"򃩖bWE qW@e=ƑyAx{1+"YZ8+vxA2JHe=ƑyBz|};q0@Lc/(:o|%q^P+_Etq| ^[1\iLc/(O@un8>h/S-Į4[1\i':]s4zbWES-Į4ek讁ێ9= x{1+"򃩖bWE qW@e=ƑyAx{1+"YZ8+vxA2JHe=ƑyBz|};q40ع=5bhV/|G7>ո?ԋ!L=WK1G &sL=WK1G &YN"'vYr(&%z`WUms= ߋ;?ԑΟ3U|5V}ؙn]s["L!pՄ^Oٽ7UO!M?ˎ(n->=ZQbď35Bbnr5;Y-?-McY?Y,K6,ij zETz"#Xa%UX}7%'y湻gRR-{5^#ZUW$D&u4,欇Jz6#"TW&޵JZ!'4Wk-m9=HLXcL*4IK2ؓZ0ט].~VԼk7KָyӰTљWqLdrmVbMT5'3-Ufku. &cr͢v'\Rea.ˠsћz(^.JmKŪ/:#vb~4HLb8M/OQEI3D . Sġ|kZyNZuV J\-rQabw5 nͫoBٷ͝t87%rӫ3%5k&{Z7 7+q\Z$yOm &gVF/5JS=4fIɅT''W<):YGDKHkj¹-zŻt:ēۊذE}M䄤U]ztԫۭQ_nv-MK%4=W[a_q_`,wa2~>JX?xK6+a2~>JX?xK6uy;˖~ș@1+$df'zxOCkkڼd%zT^{YW_NKv_ 8^ӹ˲B',쎦Ʀu{>ºO惡~7-{q⹓@,!yү  S3+7LSV3(YU:mTUTXaXxYsؒ2\m\']JHF/Y &ֵ5UU؈UYӺۦbZ?q*\$ZdόTf_S"*Q1I?(u,3ՏTV*&bf%LѰa(ٶ%Ԗ, 2Qk$u? ^UM;ѡ%KkY/+F-ml>?^~7:"?xQriAXP$i{Z`Se=fHfiva~ԧC[TY*d4ɰe`~DM]Hօ%B [I%FqjyQ'{@1IIebIT% nH1+WbC5fmz9<+ʽV5"ښ6*uՊ^idpV l8S[Xs]8TbPcDrBiP]3hnj3Uձ yoT -p[*2qrq V檢h{^J$f/3 "CzwQv0gbzEnبDX[\"\ar+0Ԫw>N^Dn=Blbz: ہj8Vfb-s`QvE؋/3r1W4FMKkMKc򧱯*6 WPU(4ƝjgFݚ\hye.-zqՙ! Rps4{+iz?>k쭤_Lҏ×n};qB (]{L+bM{L+bME~^E4N埲&PLJI'\e\V\OUV!' ?Q(,<-wɵ|jоEиQe9%V|_[=)\O6V%^JS&kUڔ>% Ѧf(0u{4,q7[jEl6_'DDL6&i&4sTE2Zۍm$&͕,t.._E\Uv4KA/]Wtٵ Qt]K_o-f߉ZbF7E(Tn+z ÆU*&h~nƳ$zXSƉ_ầ'k%X"9srYWS5\˾$:=oe:tyxhpڛȈ{ +hoa ^Y>M"&3RW+llCb%i}%bj,3,?m]4;1Dؑ 5TH&"lzDkٖgHpCt(=5ɚ9u7b=9noƞJ4{K/z2SqmG1Vٗ, RJؑ;l7䊰1rt7i\ll?%0䏋0ս%On5VsTj9s\fY]brXq-{ʍ:X\ #7US-fY9L7;ծumz?y.-\ij,6i]QQyMmډkPbDFi|d}O 䠘 erlUn*o4X5Q ;S)x6nbhV/|M[H 3W/wVR/P 0_?I%,W[W%K0_?I%,W[W%If:hae?dLJj]?bQ?_zɷr˜ҥ!AjҿK<=RB>=%)hWR^șYM+ƒZpv4<>vbUM*<XW[gUsDˮHy6iM}Y]t "Kѿ6wYqͩVbS]걪,Hu9r7=9v&]刨XZ0|Q[e[`]=K՞~O ZUrtD +kvjduGQ6ڝfmrF1`Ӆ*tQZK$\sıKZOy3a~/`5"A)sƛ]rÍ#6kUȊY@4ɞ;{i8pZisQnI\Ҋ E(t{c$-$GȪdɮUTcGۢNZlƶ.|"EdHnTJ446Ѳ]#**V۹ͷy?5H M2u3Uև5t5ktsE,YS^,,j*(\ʵ1LBdļVE=G5T7QS|4Ve vQݖ^)sigkn/'. ~C7BF/S΀ P@s4{+iz?>k쭤_Lҏ×n};qB (]{L+bM{L+bME~^E4N埲&PLJ=a}yT:;n{81RO*GmcԾ;V\OėYM?th9&5' r#[3FLs5b5誈J&H t(xO ]rQ3ՋMDj,G5#zeDEQܣNkݻS5F5Fsϕ†/Cm׫n]4{oZ^+5p0jJjbq*f-4Db.s^ڪjH7U*ʽy9Iljv^*oO؈:79rT\MAEmfPļC [[0dj 4IYij$795Uќ\FKjI1`BE9$X DTT|ϔiY Xl-y=IOԥn;jQ"bnMAZvRu8 O̐ J !Ziܱ#%LUdW=檮ؙ"ndٚj0Rbۗ}n?PUn\=Wo#urZ4o굽ImYzp`Sa0mlFL1&ɨE\wY\'ƪʍYnn,c"I@"꣞rg嚙c% 8'a ْMNEVOC{ȎUɏb&ҥx[QRy"VoH9{Fm"-> Xj刱#Yo5ۈZ}(Z]BtIJz2yjy#*rg&'R̒soow=ʸ:bHF:UHb;ae{{ d~2K%1&kvrK=yg7-NU4&X%#QȈfTjlh؂K1YRɇlV{RK==107mb%5jݔNHM&]ֽ7;P}ө:3U)֚ڙ`2]"W䰣*aQ-ۙW:ڒr{?U^iyxSrǁ ,7#93G5SbX.kfxgm ~P EMMTT"9LF)d V<U,;Z\֖].l4wb7&LW$=3Oa~5WۿNV&ʪƼVi^DcbC{\"9j抋WIo=KQ]'=~k6?kC@1}U?#3l\E^Ϛs4{+iz?>FtqN~EWy+?ĥjib+?ĥji,_W{,g쉔i_brU۞ǩ}!-+ TSʡs/}D*o՗$Ao=S%FDVsxY%X` 5*8rbTVefb_cQ؍GzUi# ԹfT;%nҲW,X,g66+6֦YvT,Yu^b,jQk᫼-b5lq\B?Eؿߒz&BtJTW3݂3F ~$#FRw?Ubf.juUrEUv0+=kү^[K[`8y `5Csxw<#^ӝ:/v{'tNݞb6>n_q~ik@I,}5L?I9ز)'DDs\vUTLYK35`uʲBJ*+6ؽ6`wSV~bۺfɮFϹf"-Տ!Z#?[0 Vj;/XQ}oI,tv^BsmѫU`J\w1РEb*.S5SiZm@h.wHaHOŀlM9Mj=޽-y%ibk j,޵Y~RhANjN̾]wVj5*F3g+9\yUSUTvKI-%n^o@?tBH5' sDQU˒4rLdtR«]_; O$jZ|A9#15>r5X2pUv 5w6*Z"p ק= ZrNu· WsZ$Z6[r>h,#%F^ڟ*]k;flj:cl8".HDE-lK~|ŰPZ5ƿ+Ukۖ[fR1;0خ.!$rS4LtԬ)a隌{𜝝d9kg9U3ۈ噒{W|Kz¶ofZf>1U~k&EFn]X- lJnKf0dK¨+!Ds+*$4[Z d{1-i:nbۭǟl/*lݷ4硩z5M9ʈRb>8ţSU 5>b"“Q3v#ξ#7ZܛRYG.K(;dry_@sb&Lk-=gس y~3`yiuF+)NQO{Eֹ 3-vNwFvZՠ݅*mK/joב k{~iH^6w>Ry7 DME\ػ˱S4TSc>H5ZS7JI@ IM.jZ\8d=͊@4M]*7,XBc+[\/rΫa=ȹ*6$gvdzL,42{ew{Z4`o3ͤI^mFY])0̮M%I]"5υq5xqae>ԪN%[@„^9!#UUjJ$?5pNK-xlvRu ;{sʩ:/i\{@bs@Y7! {&?wZĽU ‰7"9,]Tq3US+=gwԆd4}rxe<6\jԡ)8MS ۾$~0@'{>jXvefls]% $>xU]YS< =TCe)HU#Å.c\֮ƻ$ګNT-g~hB]쥱7^~bo~,\.3+),t*\vk&VmY8_}ckn Ѧ&=XxwgK\F$YxؒRF6#2LQWfmSSڪG/=A>Y*~NY+Or3#OG*v-#r˕k\0 NS ݋1 $̛z$DdT@j\^T4D٬pf; .Bf[ؗ-9M NRmqr,Ft*.֣ܩ 2T]~/ jUFjǕ!C}_ UgMKJ {K?QӠC&/ \wzb~n*>a獕fLMg""U툹5-=|qj]UT!wV-%e/>`9IeF'5aIh}7g\vljm<1t}aRѯLc"sGCAEB~ֽY'8I_jSn ^BZ/:E9{NM=omtƽh8kSjgH!k[$|7&Cz&㚨MpFѻ UZ%Kg#~gAǕkR4dHqkG"T] t,k5n*+٘i)1=a$t4k^F\n9w fqh|bڵz]wU gl$!^t(MHՒʕ.Í!b*93f*%tnj|O#EՒed詪g4>ފ`T׭*r T\agCgir]۸pf;.ΕԶrwA f'R\I&QٙCld]5th~m">bB-nY0JUj5s)ZJr4`w6gѳKl\Ad2H3ʉd\U2]"ك嬈~%\5ݽr4+%lavگi3k>,=TsQy]ld&-mls-DO.p[Z5ƀsIaӣMvx<6]Ы?TNX-Jyb)Zg qih+$=-c(vvr}Dyr"4\Ϗ+)Z9sD6otƌDe%! zƃ|V+Z*sTUTW#v¨wIXn^3H$)LhK3 :Z齍z^D`9[#N-?J0%ۯrNMg:EVmW$BNKFYxSZ&3 ̤GLW=eڮɞy КͅxQ۽ɴߓ2W}7nKJ4_f"a|Y{x몮bfnfpl6X-"ryfXP!}Xl$#Hlo 0+V"YGagE~ݛU[7R0cDN>Nndyw9t'ͣSaYMn0Y\:J;YuLЏK*_Doʁd~2K1A3?H4 AX%,d٠Ob ,eK&ͥظsIm,,jŁ8u F$FIS\6Z艞r+[zƓji!}jF _U=3:ݒkfH &K6h{.Ԗ|11r:B`lļB%m9(rrmUkm\pz2iFQE%VCsa5ƦnM]DUb*&Y1Mw#ZOQc)^lb]Fۼ+th(q_FkQ3!Y\ 5v!%y!uOM-g,#Vq' IsA;F߭g*+vyf\DX) :[5bTj[͛rg.h:ucJY5 mݰ.2S2Fk !ClM=3M{W /QitU"M6\Yeבx9)vdrՙpA͊nA2IX!vU%fFTV"7j9M?Naͭ"Abl<`=Qs3BK|Yqň:ĵ]Z,sٙvDc#Q5Wkk*uk"'0ZUrYlٯ_ \ h޶]6t6n:Wv9߂S8NMTkZkލ8q`])>2՝5Ew;(]LjUku in⽥3j\PrlO%fGDb{i.H;Uon:z3nH3lDrjf˛Mf*{s"?漝GwO*5| G Et_}tEsigjn3D;t߅S3:/@쭤_L3G~3gJ>_q_`,wa2~>JX?xK6+a2~>JX?xK6uy;˖~ș@1+*])PzP>ҿK<=RBYrOpo?^d@g75,V O,:!^sL̖,U 4_\$DM=DEsuUJzɦ^2>+_&Ò*I=teO†*ns"K/EiN"k\K- >;w]g o'1UTM[jW2!QwUWUru럲v?c!֢#Z"vޕ(СE$ؒ؊bܮq.%kz1`y `5Csxw<#^ӝ:/v{'tNݞb6>n_q~ikcmVn'DRT9v6<}VS]3]p4[R7pU(2|3v >h|MMQ/^o¥?)EMU&!Kb"dƫ!˸>c0CI oIv=ݶÆ"vtK&_+Vgpv Z4,D䪶2ͥr]^UϢV%ϬWol;٫[y*E7j_6:hZt+ܐm&JS&5nʞ\UUS) %Í+U!j9jJQSxwM.(]M؎]~P=7Rw4z<5#0O voQ!⹓LjeΡEMnvƻYj=9MȶL|xNXQ_e)8WJoܵ9lGJmUToZMeT`UXsNf"*f".eΗFRiqy=m"2{;gɴ\|`^f jsrBmMeG*~ ):s@Z3z k"ƫ k Z#XO$,ըIg6/)g$$irPi82Hp`Ab26&kZjyiL\4L7Hg6ֻdH͆T~Г 1:/-Z*c̚sUʩgw:I w;Vb/JAg'h>k]l`RujsEE53-FZG-*FIk|֤Z,Nl5T|KF^f+XhF5UV縎wlouBo]w fںu)^%Q{^ݭj(b0}¯+yEOZ[8Q)h yKܗ 3VvQ* ͆k۟šgOrݒԚXrX\{ڿ!P5.&45igS&,̔XLڍTMg5utژ| SZvߕˏPҊPN3po.y>tyCŜUu.xsU%E\/kkZS_32RRt8|)iYhm &#Y LD؈dRMdľnz&.5\QxN%iJ؟[7`K^c󘖗)7hBsLQSbO e0jt T.uKk*4g 6'qlFqtܣJCek#SFĄo22'cqN͜^kʵ& ;{sʩ:V9TOiu~ 1S9~Ǭ녚Aҧ({/=01Rj[7b&HoC<vuc\3wFDžVHl;! ;rE{UQ3U\}-VH&o>"3Ǡz㫃{FڼM/[kj43FL״v w, 0z;fL&ȄQH499O V2$7&Nj**Ҩ ):T:N^R`j檍j"&j!7uչjEY^cuĨnB|dPgڽ{QEFCvHvlgo_k`\lK V c]j F&C~JzmV؅5i r=_?-R$jD]ULMUnG/OvmcOSUh_䖾'̻ŘNE[%fʙM1J-~jm %) ƱUU]TtU-[/ u;~rBfi"\3ڍ/#QU o2ĮڒҥR٭jo/R\ WpS9lrs#FrCGsQmp[1>VWcGuFHZɹ5nzʛf$,$˱U5[-D\~CI{|:= z*cQԌ Qz-q}GTυkQ\ٶ%#'MOJAbB LjlDDC5mkƣXkSq2Bi~T:VKTK$3=\2_s3Jic٘0/ s@-_u'mr旱h]Wb8|8g]/&*1UTUMV"Ɯ۷CJU MftiXP #QtY.k鿥 ̖+W؝Ksyg'p- Q4ن^CeALFub"@TV9=i @+a'\>]YX湰|(?!^F.W]F|Ixw#\r9a"TE$- -k3u{n!.$53%V,F93ٹ9Qg5lrn9[blՖ)uʓY>I%^Eú=cɸ澌t3W &3w|!xãC{}3`';tQ ?&nGMNh]l-dx O!#ci/EMLjte7sM6kseU>j1{:a3oYۦd.l7^+sn";Ej1Tgb\Ј5IdHyT/iW?vmBf'b1a9wխTO~'uIaz :u{^nlCWRɚ5jB 7A/Db|2qjQy4rŚ383H Qۺ\ۭU:17b$ȑZ͑Q;>Bi'#[DHn2q%-yWj1uHzz1<^ypl%©:33vXiig9XMUUUfjg{x~$ax#EUIB\k~|kڬ{Qrd*{NB٤rZnB֧qD%wULRj.[>Lഄ=3xTcb/:"W1'llQȈu̻QY`UjQ7k&Kf:jŪzw+NHKz Uz] ٣'X@|+~C4sm Z:qIw@ e3q- &6NŁ̏] EnzT]Lb6j5"&!Pl9k5>}ƶ:[-NnWJO$*M߰UT.nW5%="&m ɓ)ʫF>bmJN{l"Ö\/uvQNe᩟12T^.)ΣC{b1E)M~ùx Q}{B$.$ ij}6&g!֢g8Э'/Rkgzl,zjMnmdlRkɚybsB?,_}*4#ʗ.q[C;C$@Yʓ/ˇXgq^Py:I-自Cs=9Ob ,eLΛ3OLUf) v+=:*>MY}Xvcũ%jb,YEtJV#,&*+]W<]ӦrrH20d652FfHr I+3<0SQ| ^~pk5]k5r$n]~P>̙iVW1p+%ˇW1Kݣ]Iv9!? cTVn{-|!)Zl>Ԭ2/XNӑU;K"׭VwzԸ*,w⣢=wY/Zw5*s[j~cjr#Z_*Ц~cڎOܨQ>;+PG}`In[ ly1`·̍+UcZ9lX;~B)n-PM1ѬSlXJv^rfȊfUHN*&4X"xš#OiN"OQg5s[S4b;wcJTӌiunmRtr>ikHvv e:졽$n*^ֺ^e3Nx֮ԆW*g檪Ufsx/^ H҃e,JK‘ 5[Pz:.͈ɫ.hYw6.0fJg1w?ȹ]T][D^{eaiE􆎐IjI( +緞s({K?OΛSq !۠}*}6.fem"/Ggtع=#:Qxrϸun?"AeiRu}Q_ɴ\iRu}Q_ɴhӫȽݿ\D ^1RO*GmcԾ*])PzP>7G}j˒ۃ}#"+9F4ͻXmYk?W̻d( &y.I؊w[[ջzNM$ z>e%-Zrczj%/E-U/V*!K\ʞb"9]5OV\8)u6V V:4E~G[*LUWfnY 7XmRp-h-!e s7'̶/"̭zv_T\ͱyC_y `5Csxw<#^ӝ:/v{'t6Ҙs;~J4ܽ*;ăQXnnHebX?y$6~`'{N'(e7-8*WJ`j^νnE)fe'!IË$%c""K[N2d&c: $z%-[2Z;'6 "s3U{rNjB<+Lr)y呧AbDX=uWbU舫uUSjgtjU[v} $iITϝÍ/i+U{oih7!ڷdZJe'1*tIhֱʭUMf+Xښs9[5k*UIR>o|k,VhF6"!iQZ`=f"5Ӑ!An{^ZܓMg| S QG5-^D%鱛WDk?+,b iG[u"׷cPԙ"*2_1ZLٯ 27!qwNryk=:voo&9f"oaq-n_#d3t}(9F_MrNR/=oms\֕AKhP&e-q+W`"2g bi{Ecc^Ǣrf21X&BU-A9ZgڿwurnXsTzGK'gIc1Ƴ~H1!9Z޴Ut |9 Mc#+XrÉ~cMrsMZjF:OTuIkζ@TpU~Rv]mEJJLv| >vat[ m-}@WƔP]<.9[5R`7^ rMI1ΪXZbf8\<}f_[QZWrYr,:f:p HӋ=Id|d㡹ȩ\1Wc#/-jTD3!KAwg9?2'=ⷖ|.wY˵\|YYTMֶ3Q5_ѯk䟬Q)isaK+WrSj#~\Ѥ.:,UdRe&5ZJV18u(,#(S1ZĮR€؈lU rLĔtXҳԙ̛1 VQrڈ]X:r^ %;sYۧԖz4 'Z g-5͖3O *R׭&}lXUOHka=:*YRu ETvo۹nz]3Zagf9۫4HZ톟{5QWUrEV^uw&mԳ3/n9TH+BNB?~^?cw~HfC@.'S0@Y7!zJ[X%lhu~PPI>Qѭo;kr]eMiFʾ!mI@_}P<8ɑ|뤴`K+VQD*M_#5uUw5wkSc04tW!7j|¦3vݨC ,÷Uo,g%tDŽT_:OY #̵sHjfBeYMciH==eDž$۞u[o#"KDJmsc5Ⱥ5%>ն~2vCo['8EG&F9臎1-nikHӲ5{65ꚮڪZU=:oUAe+;g&閦ߵRb"1պ{#}|Iw*wR+5Kutکڵj*IHn^Md*n*!ֿ̫4|ĦA,N243(s0zجc{jdw謩b=|Ro7sIk3@.J GQLj$h+{mu۞re2:Dת  W4Oq"lb5.~}ǜ5* Y;7Z yu01ʏwҕlduX9\nkpU% b_6=Р5:wdEDs S2Z/u4|󄸓ֽRYdႶd|I)tɔk"˵!}Ff`<]\IgiSQ5j4,55*ÈQQ2T؊— OIyO)ѲF\"©}%Bu-iӓm8=y5WX0VKO\5կ^'H+ ׍nW5L=KBUHk ׹ʺ˞#S2fGq+;i]Re,m${-"X%pqrҮDHrX d'Q_U߾n?yѪK1g&yXfd(nQ7cnH1&NyE#O3x/*AqZJy䡶*/=cS$XDDTEVEtZU6$j[NV gFSS\Z޻Z4O˞j}]2]f7MF7R nn+]Eng3`^W۔5"=\zeǑ7T V$UԂ&j.GC+ !S-'='验Vn>j&J}"Yj6G^},|t"VDLdc7ѩf;Y{j({j"~Q!,7Bs"ʊɂg3jbWmzz>c۪pD-,ӊ˚K: c*3ʄyXN,XگFUNҮK"%,5_nkF$aMēWLAnWOC"w_挢6g˱1gxI4.v<'䐥:jvxB`7ak#R~IM%;aȿ) nٖ:UB4UDĎƪ*3 .h[|Dl UHL]ױHoTՇ?Bb=IĻ*d֕͗AHb5Q'ukW-]sV.$hъLUUz[lFK9r\f'lTڊnֲ;0 = qυg^Bw>K;~y 3 [-NKӢϐG|RSp9̴TީNLl^n$_7!7.j3*R6(1rHWEvXVvY=viL4W+)& Eӏ|_2ʫ=Ts+4DY"12 6&Mc5DDCjYm0KJrP̚\el] ٣g-R_R)] ٣g-R_R(K\E&9ofS?]N|K>e᩟Pd?e%UlF}\Mvx<k}Giw:_tzox*Sȴ;EO"8|KOA]t1'o#Euӳğsu#gxpǴ%1JNAGn,FBsD*3֎[;mgWmWDH~SdQ se4i_33XEg.+իFV'v:V+>4U_sX9-;j̤ZIJ\DlhhUg.뜻\w͵Zpo%+ ŷ<V:e6Ć㚮LysBPt:ZQLs+1!*莄5S>9dNl1ҕ\vG^伙dJ7(.kq͋X 6j3@IE1a-^nCH(`\9dU йBrnVaFY΢"Uɺ.hQv.JsۡVw7y)o.o?m"OȰtWB hI.=_[HVw~~e;KE-R9_̷}I}H4ǻucb[ٲS-{xjgv~[{ogBF!uUjk|F9"#_IogBgqJQV=v{eb 9&YQr&/8ɘ=)ش*UyFYž,z=z ܲ2+_=:alEso)4,y]2$bOj TiG{sTv96.Y"Kd3RV#Ws3i{Eb+[RsےqY垶[2~lR5Iͅ-';qs~E9OjĪbuW㹨䯘k?y?cΗ/ĺ<\: 5B U-35L͵ʉaCb9rEU\fvob"Y|>/-3ꊰ2assLrT].DD0ZMzk{:TxTRz+~ë@XR%~ThG%/\ ܡ_F٠Ob ,eMK z\nJ{rl8_ C@Yʖ.OSiuvLJNA|xOLڭsW>cu.VDe~핷~-v3aøi% 8sb13@b&]X}8oI$V\o\isDTبX<3Ӻl v)'!Oj1t6l QX]dUFKx\jPŰ#(۫c55m_p`=ygק,%(47rޔKjβ!JTjqfqVHc#Jrgq"4tW)knZ(~о0_Z= 3;Lwf?v^hwnLD;91= O0P<"S3B[XM?'ZD Gd fq`g+HofYf{vңMUӪӲ۫&Ć9EEOAiEjwJSQ|zk\-3 zCEaUQ\2L7M2f]#F'e^:KWscԵ*vJI)驪"TfsPajƪ*fDUEۖG+3=aAey8,yyzZBީo aޖܰ#A[aB|Xq;oerhEZ$JŇIaFFGVCXi wrdG9rə/OeFX[TV^.Y rRV)Bv#tОrdow8Gˢua7iMG~JN9Q=]zOn9㔘S,Y^iGz26e֦5޵rs*X\%IySٱ)r/sp3L.9uw6*Rn)hGDniaSxvdL,yaEK~漍k^FZJ5^`xQ]'=~k/QEt_}tڛ8QT΀ P s4{+iz?>k쭤_Lҏ×n};qB (]{L+bM{L+bME~^E4N埲&PLJ=a}yT:;n{81RO*GmcԾ;V\OėY(N>撲xKN<.s;SUa.ݨm2EBiq4UIF9ӑQr۟;j9*_\䴕Iڳ1Fz釦zu&:˾n4kՖ9pG8߽/2Լ^n]ݽMK܉9kQDDLy+`X)YDl YXF=TUELUbũ-A8?K%ˎ%㒿&sfwc$VΠ%㒿̽~wE.׮ts15.n{4G#`"fʄl":ig9')Q!P3-0B3LjTTE+U:$ZteeȒvj>vIXqGdiQ=l|aV)z#7 72Pu' L7ķm* Ii̽|iQV$E띺7=mf}8wU̲D;z-a]U{fs%RF=S5ɐL@N4ؐj#Λ;X2=TUۖiO-%WIYeH;O T Z.Oj^x|8KkU]EBbn:aQ&*UX喦Gk&"eZ囗b~uis7%A梵[-,8nsEGkz*lMC0Z9@Rk9~Y+&y7N׬mm50z󊛓%= L;f]r+be-v,P꽝EVn2O"M?'U*yliHQEo{̤fŮ]Z8\]/ZT,9͙in5(]^ܳ5{ |0DT1ծ{3|5m<2lٴՒH:3\z꫚sfCcի:u*0#_:NTiH޹e+b.׵Wf[A5VKЙœSEoZ3hi)cNv-M.LȬtMpN3#W.I\5.w&| ba͗QllG$H9L*˗mM :/C꫒عH_n?yF+hcLtd_ܧDo`gb7WЅ;(j|:* Uv#F+5.Mv7GArlU'P3bXtKYqZU:'HsF^5zZƣS5DM'hnO6 30+bsTe!MqDc\LǦY=wFBKjE%>"2UjT3n #aDjCWuy6e,6DW̕^ǦO]Do *75+Ri柔wm,Z *!D+V9WfI.yHssZ^[=t1]Zr2엚l&˚Q =Gu#uIn-y &Afzj53"`D8Z:="7E=ۺzݒ9{d\ggb%U`~"IGƠLLHSy:83WvjTL2Mtի:%VRmAw7yOFKԨ3sX0 ]XqRt$ot^Ylǃg7m}6({K?OΗN{y/>:mM~[(nb~Fg@(ع=5bhV/|G7>ո?ԋ!L=WK1G &sL=WK1G &YN"'vYr(&%zҿK<=RCZWwܧC_y@T.In zK،im]$p.$yfnT\ltVj>.[PYUĮYrQ\>*MS"^ոZJS7;fYSug;XFfkJLDgtºMjNuԜf"*{n?I°8=+ M%[Ϳ+*2J)Ez2 F@zҷolgnYOi9պ1ȾsSx'q ]nG<ֵ? c,YRS`>$IDv{Uc5*… $Jbuʜv#^K?35U\U2m\SJ,^Q$¢Okp ]OZTZ˒+ nkP.Xo]2 Nyr#SjrT 38~9s[ֵkV( j?8iAlPanIh[tثhV 9ڦVNլCדĕLu蹷yFvpi4LxkwEȳTj6HרӐ)hsr6E=;EOaS9WK nkP|rO,775iI-M"V~M2\ky,ÞEt_}tEsigjn3D;t߅S3:/@쭤_L3G~3gJ>_q_`,wa2~>JX?xK6+a2~>JX?xK6uy;˖~ș@1+*])PzP>ҿK<=RBYrOpo?^d@g7Xv:^9紿\7]opً©ؠ@bhV/|M[H 3W/wVR/P 0_?I%,W[W%K0_?I%,W[W%If:hae?dL{J.tv=K(Qqi_brU۞ǩ}!Sw֬' }7Ꟊ/b2  jR[~:lFyIKvo'=|'gڄ`Rdh4*2BÕņƣZ8һ/>a-rlbN^閪B`-a=(RS[rZʣt[qEki[s&&~5Wa6F:b\T*dEtF76yoAa(V&Ņ" 3b`1rtYD^I0528H+f: BzN#-S2<&GbCz#^ҢLqy= +緞s({K?OΛSq !۠}*}6.fem"/Ggtع=#:Qxrϸun?"AeiRu}Q_ɴ\iRu}Q_ɴhӫȽݿ\D ^1RO*GmcԾ*])PzP>7G}j˒ۃ}#"+9kwp3՗Rj/jZUQ;lZs4-̥JQjT*c _|Y檣JU"}sXF˞D]W{E_m1V^ԵĦ9b2EHlFeKZO;{Vg'jiN Зz5MYPZ̎wWX"[hYgjjy_[A.8ErZ,+ [ "E˷ƖoEDӬ7ZAsE,\ڹk["74!QOH({K?OΗN{y/>:mM~[(nb~Fg@(ع=5bhV/|G7>ո?ԋ!L=WK1G &sL=WK1G &YN"'vYr(&%zҿK<=RCZWwܧC_y@T.In zK،ImẪ>Vّ6=^vN:Q;(% uz.o!@v73QDUCr[ǝZBO6DpJO[Ys^YؕUj]\~ \2|=7QrT{vL$G.zn*Tbܞ7zF+k͒2^.iI9R2sr{3$T!:ث{*od=ZDzv O֎blPb\w<[w+Q Ǧ9qң׏Lri'^!&%߯B |Q Ǧ9rT/J_סTbB\t|*v /vɉwЇE}*1?!}x.:TbB\; җ;dĻC"zc*1?!}x.JvxKb]!_JO_^=1ˎzc%@;N<1.zHF'/JO_^=1˒`zRl~|$Wң׏LrF'/PӰO)|CLK^>+Q Ǧ9qң׏Lri'^!&%߯B |Q Ǧ9rT/J_סTbB\t|*v /vɉwЇE}*1?!}x.:TbB\; җ;dĻC"zc*1?!}x.JvxKb]!_JO_^=1ˎzc%@;N<1.zHF'/JO_^=1˒`zRl~|$Wң׏LrF'/PӰO)|CLK^>+Q Ǧ9qң׏Lri'^!&%߯B |Q Ǧ9rT/J_סTbB\t|*v /vɉwЇE}*1?!}x.:TbB\; җ;dĻC"zc*1?!}x.JvxKb]!_JO_^=1ˎzc%@;N<1.zHF'/JO_^=1˒`zRl~|$Wң׏LrF'/PӰO)|CLK^>+Q Ǧ9qң׏Lri'^!&%߯B |Q Ǧ9rT/J_סTbB\t|*v /vɉwЇE}*1?!}x.:TbB\; җ;dĻC"zc*1?!}x.JvxKb]!_JO_^=1ˎzc%@;N<1.zHF'/JO_^=1˒`zRl~|$Wң׏LrF'/PӰO)|CLK^>+Q Ǧ9qң׏Lri'^!&%߯B |Q Ǧ9rT/J_סTbB\t|*v /vɉwЇE}*1?!}x.:TbB\; җ;dĻC"zc*1?!}x.JvxKb]!_JO_^=1ˎzc%@;N<1.zHF'/JO_^=1˒`zRl~|$Wң׏LrF'/PӰO)|CLK^>+Q Ǧ9qң׏Lri'^!&%߯B |Q Ǧ9rT/J_סTbB\t|*v /vɉwЇE}*1?!}x.:TbB\; җ;dĻC"zc*1?!}x.JvxKb]!_JO_^=1ˎzc%@;N<1.zHF'/JO_^=1˒`zRl~|$Wң׏LrF'/PӰO)|CLK^>+Q Ǧ9qң׏Lri'^!&%߯B |Q Ǧ9rT/J_סTbB\t|*v /vɉwЇE}*1?!}x.:TbB\; җ;dĻC"zc*1?!}x.JvxKb]!_JO_^=1ˎzc%@;N<1.zHF'/JO_^=1˒`zRl~|$Wң׏LrF'/PӰO)|CLK^>+Q Ǧ9qң׏Lri'^!&%߯B |Q Ǧ9rT/J_סTbB\t|*v /vɉwЇE}*1?!}x.:TbB\; җ;dĻC"zc*1?!}x.JvxKb]!_JO_^=1ˎzc%@;N<1.zHF'/JO_^=1˒`zRl~|$Wң׏LrF'/PӰO)|CLK^>+Q Ǧ9qң׏Lri'^!&%߯B |Q Ǧ9rT/J_סTbB\t|*v /vɉwЇE}*1?!}x.:TbB\; җ;dĻC"zc?+r Ji%x-VW"k LC3GkRr؝b=sXSaz{il-)6瑪Bz~ "[wH_ >zZ!-S%<1G5R_wdw׫엨{^4ׯW#4ɰWIo=KQ]'=~k6?kC@1}U?#3l\E^Ϛs4{+iz?>FtqN~EWy+?ĥjib+?ĥji,_W{,g쉔i_brU۞ǩ}!-+ TSʡs/}D*o՗$Ao=S%FDVsxxeDp1^v5sD5n%O7y]9ۮb=|Ri_u]Ŗ<΀_fѢ6ҭb7ՌEs}n3hFyoڹSӥӠzKfhe&(=?INy^Dm6噈7ndc'=/+Fޒy/󋌌10qQ:ۚ 5Ih_lr"檺^em>cѡPnČ# 2DDL&X)`̕w>ϻu9;]#js=w'j-]"FC,07W`K(5 Fj.!R,VU5([#KEEͫmVߕ2\6t\Q*?歝Xס-ƿu~Z<5^5/zPar9t(]eOFEG+f OܰnkN"MsUcjj,oYL8id/ZJjˤGB`EՉ W}Zss=i&</tTi\R#lR-qָ G(֑4pF#5dI&]R2e`4U]KI~y˷=W:KP+qWIo=KQ]'=~k6?kC@1}U?#3l\E^Ϛs4{+iz?>FtqN~EWy+?ĥjib+?ĥji,_W{,g쉔i_brU۞ǩ}!-+ TSʡs/}D*o՗$Ao=S%FDVsxwWՋPY}yJ^kLöںIeԂ5'm[֛ܺ!QЦj~`DF5"&fw$h}RO4noG?f^@4 OY4(q(֣3͎*("`$Hpa4WXƫDM)ޏQ&+t˖O16yψJt67ؽ55~mmQP\y?R%5"**d橣 lGf7G}j˒ۃ}#"+9s@uNu+Wy6qOGn3$KW4bݮ7r^&:q@UfVN¹fڪ9?Bh*ECFDW|e,v(v4|?w[<[S1RFkG^X.WLpiI%(dڍ5;odiLr8ȣ ?ѹdDrwF4SGO_K{E +緞s({K?OΛSq !۠}*}6.fem"/Ggtع=#:Qxrϸun?"AeiRu}Q_ɴ\iRu}Q_ɴhӫȽݿ\D ^1RO*GmcԾ*])PzP>7G}j˒ۃ}#"+9iœxZO%O+MZ_q_`,wa2~>JX?xK6+a2~>JX?xK6uy;˖~ș@1+*])PzP>ҿK<=RBYrOpo?^d@g7S6=h Ք7SJk֏>YCu-~e$v!95k8WInD_,cKSgy^%|͉t*}fci,$nCֵ\""&j?@P6^V#+&|3qyR|ĉ 7EEsqm>▭gc6^N%+)9˖s=f%@9"B/8[R,vvU=0c6G5EMЧK|֣} HKC%"--L"3ӣe"{cAЙ8KOlH=3wY9tEt_}tEsigjn3D;t߅S3:/@쭤_L3G~3gJ>_q_`,wa2~>JX?xK6+a2~>JX?xK6uy;˖~ș@1+*])PzP>ҿK<=RBYrOpo?^d@g7S6=h Ք7SJk֏>YCu-~e$v!95k8WInD_,cKSgy^%|͉t*}fci,$nC4$t*}&< Mp9&FY17dU , < "c{^ ki85)Gz3d}%K=:/%Pt- +خO#%vM,YϪ&vښL6+RrXع93?5rtW,%n]lTb7z9 VcW7GZn[3˜i mb^XFmeR,`w Q5W95n ޶ko&kZU,Og.O8={qʃqe ̾b,%l\y5rMs?ܦ,ibCsS$",я\~}d%{֩sЫQ)A6m Xv؝zTc6g?bd#57[XSőLK DV52v.y扞{sf!iLtIۍd8ވՊ&y WUj&l$7xn#өBTrymd4ñ*Qj+',qq%#@W])<*zFuVCE,}/qx;_=λWѻK~Mz o]^6C[ĮWW'-j}gT}::`~l٢H:`~l٢Hnwܺ,IՇ"iG?@^XYmw~+}tŖxSͣio"3c5--IHtiKd>Sȯ}tou1{/&gNei?)VNw:>G/@Q]'=~k/QEt_}tڛ8QT΀ P s4{+iz?>k쭤_Lҏ×n};qB (]{L+bM{L+bME~^E4N埲&PLJ=a}yT:;n{81RO*GmcԾ;V\OėYMZ\~zρ4zu?R@r垪DrwQrTWjbT,'S'])>Êܓ5N!x(B"Y>TI}=s98M^k,{b5#4T횆-Z(K қvkU?75w6;JlSok5eޚi.V*038Pic&IU2NKZ,fO%NYkY5٪'FKC kh0رߛa׻b.}P;/4۷ wURMW\S-0F|-,k"Gя\~}d%'я\~}d%ihrm}>\{JObW"Wn4թHq!DFsŎVU**.J(^vM='Ћn{ZkjkπnF Rb˄ֱaչgQyԼW'NJ1;[ȋT1Cj'>v m/]5Y? J;5Zn".gED{!+`I[d fYT3,G']_?*"={y{y1 OO F{rש-'g<1[jueO=k.-ZA锽 ʚ硤_MlL壎W7dKl7z2U\ܑٻzmQٮ[Ԋ4$n{nYSYQ-ouOEފUhJ =n2i%jK?6ithC{6SN-zWLmG^/ٹstnRFJ;ujDw,))T\4LڻM_7U|DLN*Jb$u^uN'q]f_I =aN Qvkd췗YʟP y:IfkYɹ޹>#U3OU<7uo4ѨByÓg%/ҕ ,3YDܵ䯌';[]ߥO6dzt͏sUDLv4şoae aũ5#9o:cYl]7d!MU5V|7rV\+4n;WBdi Y`,g9uUZٞ'/+0og=rڗ/I6זYmX QޭypK=YkҲer~_nkQb&^*tbU ÚҢ^/sFDؽkc{4ֱ˧߶; ,5tW756軩b床y4ТYO%moͭgExui-iy</u2$ P+r,j@j9<纛UsE\.Oil~ޭT$#8= 2:5Qڪ?R%1jb-"+0*?4?\lI3&ܷc3;Zwa8}$kѨފk57qzXCFdg$\CXW(B3aœt⽭EVDDW*Y=,I )3t^=L.k^aJyk^ܶn^cçV\ಌ➭~==kYt=FN,vVʹ'y0mdѮ-ttIV,l7#B҂VU DNC(ljxX% d[h՘~;N&BE_.HcUIZPb9EvY.}GsEvKϺVmmXa%˸˺fAhTɷ97j^ק,SRJRԲFOni 1nJŏL ;F ]TVqQQQrTNn]9[Vfq"=q|.R^bqb,(-k;"fOa疅Nn!w9 d=hYSí!7ó/[eeӊcWIHZ.fNYiО~lrwQ;{KQ OƁ^$;*f׷=&{qQɷ,ս'U9HTxA#_߁Qȫ#MgbCtrԌUdV/1YÕ֍W:5k> ˑյzx\xRPM,SO5봥Ao39j͞=S[-\ȷ>77">miFtOf7@-օDpQ)&uh֎M:(y)K>W/9ь.7yZ o7"ɷ28ˋ3bIT;*uؽ͇N,:ܝr ̴ʫm𜨉sm~kQe(4YVMs}ʻUwTŅ <' !j{QZ*.(T+ϫq?>˚{eNSmŖڗ&OW~R$)JH]EbɿyX-/cJOMI8lܫSg;5ͩD-\n?"shU\SѢJoRrydKZthӫp&k%5ƍ>FmP`ŬFŊfKC_[%]22 (RG.N~TnjiiHXPqv閻_CĆpLRŠZl+훆ɥ؍ JYwǷysEۿa SZ]Wp6`YϋOo owkANuryfˏȖkgVca9[t:v\%3=UMYRv|jjfNYu|$,MsUUkS57&ҟsEs|WSjyY] Vis5i5F`Nݴi4><{9Q E@4_V))n6lՊ`t_s- eiX"{cAЙ8G7?]ާܳ +緞s({K?OΛSq !۠}*}6.fem"/Ggtع=#:Qxrϸun?"AeiRu}Q_ɴ\iRu}Q_ɴhӫȽݿ\D ^1RO*GmcԾ*])PzP>7G}j˒ۃ}#"+9iGV~V]ygk6/k&Wջ ɬYƗo`7bKw"g;];݉-߈lM;S?3Oc&or@P˲ 3мbוCK.4Bfw_z>g]ُݗKMޟO>_l%j`THɱMG|.Rbo?q c7G_L UEHrj-bt!/4V|4}ctrys< fst1]DMEn)i_w2MѓH=2b/u w䭧tOIR|W9ֲ,γ'uuM^/=8ĘG[,XɞRD_nI!ǧ`64uj59{s`0q*TӚ{?fRSGkG|8 VnGu|\_ϲ*yE򅜅 Ld8mF1LD ǥϲ*yEJ,Z ߝ#-yخ_f>!ZP>mxzO /Bm?0KsWKFxΏw+ߞiBjÇVDzĐ"V7R"|kj3}JK7ڏ0N aVG%͠]r웵2dj|7"*,7"S]Z*%pU#9is\^+X+6 ܣ y{ydkHw K.\_w+}Q ۝w>1'[wBR(6#Et|A:bk[]à}/,N v,ſJmK}ٛ7d]NeS=X/k6'R.77"}ѾGվr/N!)p%aCb"dDO{O=;[YPJ .#_TmͷjX0{sis sٟ)iYf\^$YdjL6F(HJ"mV櫒U] YqC >ĕSڄG,&g?k^cڎk%ELS}c:%irtˇ4ϜK]kR*.y4Ul*.ZÖ+2,xhf;{:|'X[mLBtQH&x9k7Lw1 G*7FD*+Yb?lWu[+UXa%I?Y`LIv.hsH^]E#t^ J]VJ\:^9紿\7]opً©ؠ@bhV/|M[H 3W/wVR/P 0_?I%,W[W%K0_?I%,W[W%If:hae?dL{J.tv=K(Qqi_brU۞ǩ}!Sw֬' }7Ꟊ/b2 )֛]ygiGV~soex[5Piv +v$~"/yaqC)X ؒY>314?f7!U , < -y ZX[ՠGl[)XG9Ȫ7hiN_Ý[HW1ss&,Sm7hƆ1XױVLQwQLenH6M$>u ܎\D2^Œ)Q1|$=x]խOd&mXUVHr  ksMZ&|MB>QU7>*!&bX\MT妡@IuY8؏j*+vjʙٗhlNc۲;Uќs*""\#CXc2]J-kׯfkȟ:D*W,m!VJzjۓٚ+>Ȩ|-MOU%IBt(i'QW=vIᳵ|۫cL#C ġuq'g4`cW/߳Y `׬["-,ihq_4"5ֵ2 a,(QO#]SqKMFݓmI"J>%pUΘ; #V+z"BE4۵-[0:`j,Y< cVjSo(ŧ˄ti:^:4.G[ϸ]/o^{FFLD纐9ELE$ͪEMm+4q۪j%BZ j*=dU5EDtr_[N>M|)tv'?kj&Y[ _zo8>u(5sH1S25F%3Bv\n"zdEkw,6k*[7h:#[틜Q|';[]ߥLe6"W!j&y jƄjW%ܹ훝oZ r4Ij|$ ќDr"g"8F ug\UKyS<ڞûƭop[[M垭Z[O/;flvADa6蝶~BI^X;{5\$jtusn{&i.J0Ml^\x=G=2|%]=:vhfw缭 דZMl:toI; Vk=O5+m)V2Nf:K#@rj93dvOM_BN{Jc*z&Yqd#Y.M\_hn2&suJ+;PCu8-%/ҽ^Pڒ?;OMUFZ]%nHg?<|XQ^*jXNٞs4k', jFK|Z4TlU抨֦7Qh[Sޭd["|:]7 j)ko7b'f T!Z5feޟO_*#!hQ#ֹStH(*9e%NFT]۱{dM/K2C{^LAV?%'e %DE1[seN)K7ї1-=*ly7{WN|6BxoEĆsȽzZDD$COm-gBE$"ws;󸩶My(V,bS`+gTz'Tƿ4Z(LfL(3MMV|jL2+ULԥe`z'5٬DvJ,T-|0=1ALEl6DtF=X֪Mcr]<4645u75Ζ\.Cc;Yg3yfMZZ"/)x.|8K!2L+ULy&czBJ'Q5-Ȱc1Ƿcx\qIֵ%_B+44?*6§ExOte/3NnU3EM=GI*X)2ok{YFNj,&Rs sbbUw5;DRfs&Y u"eoxeeJ0cI>tɍƉuYN2frh) X~cl$L#$s/ԙ"lU]DgOKHZ 1jѩP$& tfG5Zs5sUڹ_0ү1ZOcǘYW6n,75kQ lM kT:fOEqe܎.4èq*knk篇 E"{F\U vB2NJ1rUL`Xյ:Vgfyjɣͮ uRi8Y2P7|K> &,UՆjg&G-))<)>DqL\W>To[+˛Ws%x'9')P%?+ f^3UaEb=jv*LZ>g4uh,IqV|4v%}HLBjC=-MDv_IvI- Cw2MQ*'?E<|.,UU/oiETQ;hVvQ[b=/Q-I%h!7q7:mi'~e%tt+>{ǩ=~.BROR碵|ļ8F"siۺ2+f'"5sD ;+…&A 55b"!!c0J-W%dwN4a0k,|;2DA jN߁=L ;QИU6kg[ ;I)&jSTbIr~Y?y0>gbjSȉnR.ߒ"&*+Wb"gy ƈ=VRCnDc*iSRoRj[zjJIIZQك7VsU!kAX0Zz'eDPܬV$Dt7EHޘԣz3kUw˚'uP&Y[qUgR2 ;| kݙN٤DU|Gv^ꪞGc8:ˌi罏nZ|wmjRRk-=3Ԓ^Vf@5--IHtiKd>Sȯ}tou1{/&gNei?)VNw:>G/@Q]'=~k/QEt_}tڛ8QT΀ P s4{+iz?>k쭤_Lҏ×n};qB (]{L+bM{L+bME~^E4N埲&PLJ=a}yT:;n{81RO*GmcԾ;V\OėYMZ_~O3G~3l\E^ϑ(9}S} UiO)bھ(/XiO)bھ(/K4W^D .Y"eį@CWwܧC_y@sJ.tv=K(Q e?[TI{Nτ+?VPM+MZSȯ}tou1{/&gNei?)VNw:>G/@Q]'=~k/QEt_}tڛ8QT΀ P s4{+iz?>k쭤_Lҏ×n};qB (]{L+bM{L+bME~^E4N埲&PLJ=a}yT:;n{81RO*GmcԾ;V\OėYMZ_~O3G~3l\E^ϑ(9}S} UiO)bھ(/XiO)bھ(/K4W^D .Y"eį@CWwܧC_y@sJ.tv=K(Q e?[TI{Nτ+?VPM+MZFtqN~EWy+?ĥjib+?ĥji,_W{,g쉔i_brU۞ǩ}!-+ TSʡs/}D*o՗$Ao=S%FDVsx:k֏>YCu46=h Ք7Rm^Lwbq{XX1.buTI~P#st+{r"徙U6pVt*F7OWs[NX GWrDV=&*vح^R%%NaڇJt.GkWY ^y*{Hq=${J맺waOSc_ =֮[Uw\9w6L/iFYTˋzs3X1o^Z_K l"># k=ҳ4Y3:!瓞fwUwZc7G}j˒ۃ}#"+9iGV~V]ygk6/k&Wջ ɬ0%!L-"nR54Տ Sj5*s"U6s\s8趺gUV'.k\ie@b Z~0[46*6Kg=f[9l_Fy寺ni^r3megS, ՊbP1aC cClHq{v**.~ z:$_C.Ot:{}cJuX7~S(0C])I𼼇m16 %-Rk~!74MHXk7yѤu],]Kt\:KsOɽ~>m=tSnzKOhx_JtC(XW{>hGTp5SZ(@tҟ:Gr gvRmzKOhx_JtC,+4~!z~5KQ/?uI})ӭw >P|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NP|a.y Gև;%NFtqN~EWy+?ĥjib+?ĥji,_W{,g쉔i_brU۞ǩ}!-+ TSʡs/}D*o՗$Ao=S%FDVsx:k֏>YCu46=h Ք7Rm^Lwbq{XXBf~,= _L֜#YŸdsS~ nYsigWIo=M>_~O3G~3l\E^ϑ(9}S} UiO)bھ(/XiO)bھ(/K4W^D .Y"eį@CWwܧC_y@sJ.tv=K(Q e?[TI{Nτ+?VPM+MZ_q_`,wa2~>JX?xK6+a2~>JX?xK6uy;˖~ș@1+*])PzP>ҿK<=RBYrOpo?^d@g7S6=h Ք7SJk֏>YCu-~e$v!951RJgh9nȭoҠ&# ;+AVꣵSyg"cѭ7*̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{0ټ-x/8tkf:^32 7Fo h? ټ-x/8W|̃ ѭ7Fo h? U_33 tkfѭ7{1J3 JXLfODwS>hOlH=3 `Bf~+';wz#Sr΀({K?OΗN{y/>:mM~[(nb~Fg@(ع=5bhV/|G7>ո?ԋ!L=WK1G &sL=WK1G &YN"'vYr(&%zҿK<=RCZWwܧC_y@T.In zK، ug|!YiZmvz(n콯^7V>'&b.{زoݕ*T?EF#/Z]wQWBU$|˟Tj+{I˚Kl5~9hXj[DODg>oEpN yQ*Y%_$}oL;Z/l6$6CLfʑXw|"UN֣|dj3dF&_&p}qGlW0:t)žy)?YdZwT׾$<D, f?OeZ8zO=]q%dD GZ-8B7zS|c%2GWlXD=JX)پ1,x 񚞜HK8;71ԥq/b3SӗI)`f?:S|c%2C,_jzr",R pvocļHvŋON]$CԥpRN8رˤzS|cJX)پ1!/5=9tR pvocéK8;71^$;bf.!RN8u)`f?KdlXD=JX)پ1,x 񚞜HK8;71ԥq/b3SӗI)`f?:S|c%2C,_jzr",R pvocļHvŋON]$CԥpRN8رˤzS|cJX)پ1!/5=9tR pvocéK8;71^$;bf.!RN8u)`f?KdlXD=JX)پ1,x 񚞜HK8;71ԥq/b3SӗI)`f?:S|c%2C,_jzr",R pvocļHvŋON]$CԥpRN8رˤzS|cJX)پ1!/5=9tR pvocéK8;71^$;bf.!RN8u)`f?KdlXD=JX)پ1,x 񚞜HK8;71ԥq/b3SӗI)`f?:S|c%2C,_jzr",R pvocļHvŋON]$CԥpRN8رˤzS|cJX)پ1!/5=9tR pvocéK8;71^$;bf.!RN8u)`f?KdlXD=JX)پ1,x 񚞜HK8;71ԥq/b3SӗI)`f?:S|c%2C,_jzr",R pvocļHvŋON]$CԥpRN8رˤzS|cJX)پ1!/5=9tR pvocéK8;71^$;bf.!RN8u)`f?KdlXD=JX)پ1,x 񚞜HK8;71ԥq/b3SӗI)`f?:S|c%2C,_jzr",R pvocļHvŋON]$CԥpRN8رˤzS|cJX)پ1!/5=9tR pvocéK8;71^$;bf.!RN8u)`f?KdlXD=JX)پ1,x 񚞜HK8;71ԥq/b3SӗI)`f?:S|c%2C,_jzr",R pvocļHvŋON]$CԥpRN8رˤzS|cJX)پ1!/5=9tR pvocéK8;71^$;bf.!RN8u)`f?KdlXD=JX)پ1,x 񚞜HK8;71ԥq/b3SӗI)`f?:S|c%2C,_jzr",R pvocļHvŋON]$CԥpRN8رˤzS|cJX)پ1!/5=9tM0Yɴ/m)፡E k;\u/1I >6\oABLrFqGMDzu/=0HKyu#'e^K莋&@pvvωUYBŲ'xwi9=FoIf; qF䁉YvZtCz&Y;&*$cV=2TTJ,vfڶX,O'bC\]`L9{I *5!%Y{kŪZNxYȦ!b.WiБ5h++uS[62jIj]=gN\U4㫞1, :r;w,&Շ8s6jfϵ']Z\Tԥg bZ3bzvUE:kiîlruk\_$i{/&gNei?)VNw:>G/@Q]'=~k/QEt_}tڛ8QT΀ P s4{+iz?>k쭤_Lҏ×n};qB (]{L+bM{L+bME~^E4N埲&PLJ=a}yT:;n{81RO*GmcԾ;V\OėYMZ:mM~[(nb~Fg@(ع=5bhV/|G7>ո?ԋ!L=WK1G &sL=WK1G &YN"'vYr(&%zҿK<=RCZWwܧC_y@T.In zK، ug|!YiZmvz(n콯^7V>'&zFB!1Tz26"fsb" 9I䶟XB|x6MkSj!Z/Lpv9?Se*cFCEIuNNw[\Ex^RT $sQZVr.1{M]9U~ֵh6]Zݶe[.rSG*wd>9qGb.#\,pN"!v5NU&Zb;jU>UUo rW^NR{[=@8FX4۾~jb[W|VMUț*7jn[ՅZBV`P!QnDdE6R&*u{\M\b^[Xo_RrSJfv۸DT- ՞%QC7G}j˒ۃ}#"+9iGV~V]ygk6/k&Wջ ɬA33Bj$G55UU؈USwŴXcAjԧYWo'^ƦOo:78SU:*\\YJT\kIC2ݻ$Y6j\rWjNf}J8ZZfq\ =yM&B0d!(0!7Ujo"OXJNmO6cSR҃]dзm[1~rKp{'&M'׫>$5JTm>딉q;!KjDl5V'"䩸qV4M.}F A8OTÏ$#~ɮ-)/(/U:p>HWj pݞNƈ}-]ܝs!j"]ঙu'iBwO(''y%\0Aw߶-/7wW aM=atFDQ 6%WJթ );[Q!3TTPvʅXUK=aȐ͇Qs\Nfz>x;8I'7oZ5.JT'd} ]1`ֹٮJEq2eByg {;)I-kʓɋJOp]o娺s+h19Xe23z0n"XUr^ZNq P '#r֢sGi:t^7VNRŏ"17UM~L_q_`,wa2~>JX?xK6+a2~>JX?xK6uy;˖~ș@1+*])PzP>ҿK<=RBYrOpo?^d@g7S6=h Ք7SJk֏>YCu-~e$v!95tʝ#%6{Gl7upL/#W{';2Y> S+܍.Ɖz5Tj'mPxm.9:uSN4܆g3j"&7#fcJޜ+ 淋.Epa3 {~MYmOͣrt§\rMMNfZ [үY)<t(F}/͓$#J>W =ۺ? h+Y 6R.V| [N#ʿz^':jHDuV%$Y쇟J˯-,V!]͑閮z]3k$nKֻwzhՍWzm7FY%]uVq^q9O-y$M0)BY䶽O'z3.i mIvG/ȎS,PFVquIYt׌Ίl5rk$\w3S9;VO.:y N3%FrEny"*d&{.t&[I7*ՖZif]w9jFo]VEcE5I߶-fӋJ\Fo] GO3*]VZZ 5a=u"k~jv؇.Y F;i^*T՚ן MlgD@EELsE?sXr5}W#Т.8rE+mE+(8;4alb/q\f3B)Z MBTʗ&kHr;yhX#kU1瞼Zze>#F/ "6&n{܍kSؙH«N5Sg?s9FU67=DEU]M1oH+MtZaꯇ%yܬ3UF14Mg.k5UY:+[+Tl-kg m.R%c,BB13pC aDkͮj抝RaJ$7PDTe:䈝oƥk_W;'Er98sjn= *涣6&UjN?rהXUDLrD5>hjI]tW*BnQ!9stELqOMن_缃ϲ*FjylL "+#OyH. &&n|G#Z߅Wb#Gl@Skq%Em^4Mxnd6\DnSR=$n8hrp_䤛QuWYw$ߓRV$I,̼7ҷm4\p9GffJSkh9UħAE zjUQWE؈ԉncs}&9av8lYIb$7tX~Bf; =r&Ĉ݊nMrɞlcCgi ʵ˩\[Y;B:Gl3mZ놡 bs"9$=Z\QvlU]^[Ȍm-sKe̗ ~Ek&:^GII(n؏m_ʇwZusgb9rFKNŠ^Mrױ1?k33uhuHpv[帙GD ^JܣXVu͈Xcsob&7 Pjd\;ۑbs4-V _@5)V} zbhL Ȭ޹\U\QZl]BɵoD<0z&[UjI(lqj?W+YsibIiآ,pg j)Ƌ&qf 8m{Qȿ7~ѿ$~qEXBϳD/ ty85x%~I> ./#KӥaDnTQH>74#^LF-ܗ3K2⭛1oLg!)*c.YUsV;K!Jp3#UciJ9O,^,x5U 7Q\**"拸>+8$ՙ.nVʪbD{X5\9rFno:ŷb&)OX<]ꋖ:wfgIF . Q_JObowZhsdb5rVLÄ=T b*sn륣*'R[*b ڤi9 Sc71)2+vk\^S^pZvV?feOU]vU;,O%p.}moUk|_KٙA7c"C}wE.ya*lHثmDؤ}'aḘ.VR\p5> jȊ}+JUT 2O/fo,\,{B,Y*9t\|eDD|HQ˸F'[kߒz Z+% }'%>={_^m>7,mm^w~k%vy>4y*s[(*.DHBL\EfV?BbGީDXqڛ詛"nڛʛ|zh^+ ΢kyYYXح2zLحTc17U7;J.Kd`#e g.ڨvEW"dsjnYN#٪M<ū՛50j2)w;&[ݙrmn[VmjD:W_]!W/.[6݆T4ݝ)bl<Ǭ%b5mgQ/|oJ^HII&;q r_4:vўbXQʤ6# CE5Us6歁,$ŬbT}R2c܊=T\w#ˁ /C%qhʅ'=kIg"^uN*@ݔi9W;t̔fN/$9/)K+YaIM-#Y|y3IyK76ԫ%p&3#?M抛yOaMy'd 4eUy2 "Ê.HSbf^R &à Dr5N9݃ؒ+t$376fgyf]#gW"2frI"*6,x/#Sav.[wms>7sZVn0Yfzë2c:26,dذң[kffćbױɛ\;QꦈNt4w:]uy5UM>ī.dyfF՞Mk6LkᯭwfKA/JXMjN<WdtLV]:9VV.Ɉh6{\{T5Jr7Nk&MyPzq?:ɡgcJb&*x_6j_{rEy4woխKJ]$*jfrE5Qωy uOcUrUL<=Qe]3yjN=rMv3N7ρ~j,F0Vߵ" bc@Ȑޙr**w}87r91$"՝# &m w-egKetզ]Z.TUDZU7sW1fH^S=Zɭ|ַ$FBc{X"r䈉#=# ڭ5o(r]H.KMb.f :5wQs cv ;zDl94׉?GW&nHNmV|'R[Q"ɥ|_K =~яmkSPSV+R͇TXhFUGif$1 6oq"һJ1kk]e(M'S38؆\oz"b鯡 Y=n /f(_IM1f=c\P‘jRkJ6jlXwUb'/qS鼍cc_sKg'f9Z v,HOvkʜi;‰6zB k$ve[p;Z ƚqW&3֦}6jƈؽJt4'6f$F{C09*Wt *)U'MѠX3Tt%hݽn]W\J"eSYV9L{e#zAw:?R=QSђp>#zr5uUr?&k v[.dS7a I\pԼc15 JdQ"EYeQ&h'4Z-Y3QPn{y-oVŴ}2jSs˞GmnTCN:tʹ$9yQ{rEW/c{_.My"\U)w#Ej䬇P"1$8H5rfjwC\ DN;, Nuj~φ:ZH2!NM'"W,:o]٦p^֮Y.SDԥRSm4-oc>ek?wdFaW}FRad]eg=#c* $bC%kSuQ[u=D~s ~7Q VVSVa٦ܿ NfIʫe_tk$w\)7+?)~Fbyim,7#LTEEEE?Sai{/&gNei?)VNw:>G/@Q]'=~k/QEt_}tڛ8QT΀ P s4{+iz?>k쭤_Lҏ×n};qB (]{L+bM{L+bME~^E4N埲&PLJ=a}yT:;n{81RO*GmcԾ;V\OėYMZ'[wZab?R>AjXEتaKa ~WKpj*"nwYqkKTfm5|Z++rTY W%ELc9 J\Oo( T*]3NG.رGDX\ɮU&~BbocksgyJ'oWH[i))8專譼.zsپϙ7#,)jp, n8afՙܿ{]K8q-W|(od9HsUU˵ͷ?KW>"[ <_jIFMg\k˭u[tܨxz)w@ksMNW%N!9bb="-&b?R+~j)t҇ u>cdH%As΋ UUwu$=z7aeQo^Ql% k1,KO_ءZCw??Rt&6juC1rb'mr9m(QY+ v{ 0;4_:YyTt)MipIAjEƌ/m\&}ڵiTZ9Ieo2ȓ촻[;!0),IwuUt-V _@')!q9șt.} odڷbphmf9Er|.p5P4cQws8 ?]N ]G=mz׬3Ѥ2 "]QQsjnOr[, _9h:of7ѡZ5[y,,\4~Şa4賈yUY ¿;?+<6ahےf Ĵva%Y^WM+g c&ܔΥNJlijSf[bn?k^kL6*Mf#XF)[ j\)r軎RVzXםIgIɲφکmU}7ŏJcldȧBثwrvjc4JY0UDpd7X"/i2ڈ6>=ZRe(rmݤYn{VXE*MF꓂i5˅ [|o;iL:ZݒՋ42X:jv7TܞCcDr5Esw|$H6j,$LHNi .ig)o;SW sm q.=}Dm%}OÐbM>Cl"%ݫfB曳e>B]vW"+i1WW"||yЇO1YZLEsQvb?Di-Dd(8oMb'()ĉ[ti3ȿk:-OHeaJl(0aU"7$Fiyu2Kq GtA43m@e%iؑNl9FJ}AXsVˆjUwU"M*~ VMhqχU>DoE.Y_IQWwLNQT]fSÝE]șFq֯'uw^IKв$Dĺ$Q8v͙ -:ɯ$?D酼-[q;s̷b]m؄1jeUc^G;L:SHӨTZ5&UrP&&H52DGHmV]1Aa3/elj^S}3:uI%e%^E͘sDnu*h'3a4 R^)'ɽoڑ%;oz$f3=WޤB:!O L &1:Kb.^90e[ӋU OɐLY|h$(-jVuɨ!?&*xSYxn#K 7M-si+OkKpwR?48>vsX H1jbfLO\- 3O_=pGhXk&N8}㷶urלe˴moa sPN_+ ,HuتS5ge_{hN[5rJ"5DDM 577HZ+K75tDN=KJ$X76f_tl$LeӶKRo_/VfUHPه&J#LE:HDuk.[ЯgoN j#e%?+ٞD_bo4$k۲ԋT&ez7?<Ζ=)ظiۉQcTETg]Jv'̤΀htf'@ʶ^JB `AowUwUUT}sI9޶T5m)߸=VV4OYՈiIOE%VUf`1yDUM#N?Bn];@>6em )j^I/Vh^kf$^w1RԦwr*!Y4[\K5Uli |7+bEG#a5S}5[fOza"'Z%U|5mc^q]%Y8z}xv\]Q)6Rl'kڬ{Qrd*v:mǦr<]tk^TUgc WRYe%doT^\!_?B* LAɹ]1:.Jv&nD]vƢ+K7a IW 0ã9^:<, ,䎭aWJ2^F2)5>^J)'+ !A Cbn""UJNMO6g.v_iUhwg), 빯,MvbxeŻ6dRlh.3MeT|%]UkSEfY*v&[-ۚUτ Fedb.qQJ蟈֫ېrSۚSVe\1]!Ӌ3 _UU)=mձi籗Ǭ^YJV,)Ոnz3.9Z !Ft)wQj%b?^ID3\"U>0ƅ8LtJk^< u" rˮ?m :@SuyQrT]D2@PIN&#-Py+ÝsDn{ȩWe Xi#Qv:R Rmk6,y(Q7:7Uw/CS:4~02'C{Q_Իwb۸S鬕LԒਾ&5%p0'? ӄD3?Zpgpo λO)g@WIo=KQ]'=~k6?kC@1}U?#3l\E^Ϛs4{+iz?>FtqN~EWy+?ĥjib+?ĥji,_W{,g쉔i_brU۞ǩ}!-+ TSʡs/}D*o՗$Ao=S%FDVsx:k֏>YCu46=h Ք7Rm^Lwbq{XX+;FŬo0j]U[mW=kn㚉RQf)#$xd65\/V`N\P.^AVuh5+ ʌb/i\߁>6f ؏ ~y;ˑ2#QDDLxz(F}/͓$#J>W =ۺ? h+Y 6ҙں_WmkfmA&!Ҥɶ+zCb5\V:w? H^h>7ZHRY96tx_)m#jt<&*R1̙vWˇtO~4຺n95*"#QATj纨D_|WGp nq(%O7Ntz)+RiL# "ŗzfS}5UMEMpà 1cֵ2Fn"&ܤT!aO$kT)lHv+\v檦iqK1M^Ή$V- TdE_!i8&%ҹT$֧y Z,\HtXkW9\7UT!W \pJZB31r݆Vˑ.>+KƠFKTwÆ}76w"#曮4"+ȭa֯/F@+6$]r8aЌ 9Z j)o/+_Γ}K+3 cc4sU2T^%!mfv\t bhߩfU\s|5Vjbd- UnEi<*JޣHЩ%i`5.H{NԲY_%_U7&ļ$P)-6=*0, %qC%e%5 V 6/Eسrl_OamԢ|+fŗ4Dc["4#C/+r `kXh4b-׉vbvOCTXRCl8|QS5Us]91sbq2ylڹ Ug;gY8(.gWWu%v9GÍ$(Okh뎍;@UUc%wnSD;e+I7̩^2˚fl7V`ØG*":,mf'mUU!oz^'vCd&9*6W6b&{iNJJk6uVvJ*B|L SW5TTNZFcV/eR5&,YkjktݤCjS`NGvO65W4 R&ElYYhDtGQ6[EJ#FwF ijԖo>\Vs^~ 4h\ݬWFͫꢢNKK<>RXmmb4МF:"nZj*q5Ѵipd5KΛ\.RRUl^'`bFhoj9"( i5\:s-5k4XnUԋUsTb*9溫ب NšUΥY9u 3M;eJ=3$ֵmd6 /n z^:S`nwƧ4-V _@4X=WvJ$ÚVIjk+GercQ24-V _@>3YUEl$$yY.d.p44cQws8 ?]N ]E;c} >6[FC$حH"+E-l7ëv uI-f]ıB7pEA-kO4[@T$|}aᖕ[l:GtvN9fP>ȅWЬjڔTb:;4nZfdIe'qLIiiN]5# jKGdꋩ Y"*VHyPFYɍUԖ#ZܚS=yjGC*wu%jK^|kԸG, u fMtt·qM1Mrw(xTd'%HsrU7&&{3/ͭl,~F١K$ I w}\sUUwT4(Xh9-J%Ab]t^Ӳr&g{=9ZqO7˟?"6<ؑi]TWaxỺ 9)ONEEJ!5׋k+:Dr.rE#jsG&=&G!Dj=b5S4TTEC/4E{ʽk'sfߞüDǷYm^s(QOBRvGbskͮEEEE*X#RUZI-GUBnn&5JثZN\kNn)QP!2Í,Z䨭Tj{ד':YJ9ʵi,ψSzm۬Q/[m۟ˆ7^+bLJ *OB4!AG>.\Mh}r5 tCz[Afk&ِߍT zĻaƋg'dm)Llvuf 9Dr#bvX_ƻE\ti^>@|vg.Kv.;D|%1ϩUh9O[73L)J[-n>K6)РT7M*9+mF46Tok3U7iھݼ̈́ښBRO^ET]%q qj@I u"n4Vŕ^GD~nsnjSc-V4nJ;OC qN[Zoeue"M,yCOX榮Uwus549F>Zoey>&ЇO/[ǯ[N/ Wq"9UDM2V!fvi}YPa.-rŜˢTIY.WEk=jt8MMFZ+VI,()Uߪܽv{&GoZRH[bC7 6vș&HW$ڪ|u2Wx\ޖ-e)(ek.Tύdr]-Ni8躼c>.ԁ UU;Ve/hVٷ!ɻ2mS&6,8PڽW*ٟm ."c95iܚcU% M o2k$x4cD/#bqt|mkkgɛB[~3[$7$(p`?-sz|($,:ɯ$N;2Fԧ,Idkߖgyxޞë N9K$FqN85+E%ȲK,óeCg?4pk|o!6-һK*h%*h'3b4Ht?BM#bn9ع&zq?:ɡgcJb&*x_6j_{rE|,N;gKEgNjnJMlo6N*zFP jw}WQHQJ=-NOmF3A3օYӏЯ%ۡb KN?Bn];@>6ez?yy=-q*?17 rgn"E! yj]SbrHZ񠪹GD_oty ;BAu xٹj˒lkXJM* 2lf5kڻɳY˳j**-O3D*Q?3If*i^֔˧!kڬvMO&ИőSZI΅a74Wv\E>B1V=G/.k{˞~u/] Js]:"QK7`gf]vP$iw%-܉jUMhEښTj羙ݍ?7~0:trތ 94iqCv.7U={1@tЅ7o}SM}s/vT^C2ž4XqUhe)^Z)iC ѐ>Mf*=|{~#w2srkQ51 oF ,6j\ܧ\ԘZ#%[mjZSyQL({bŦBd)UJ?qVaf݊i63Mie,Zf2 PՈ/*hb [\IYêSoSMgM~]Ctֹg VБ9sN}7N˶Db":Ot/L*\$MgVicFHl3XZ؛ݳ$##ګȨ%M|q.݄+'D%O?ɷJnT"ЧwhgH`c>@րPP&%#"Rlj%5=bEEr9=&zn.6X ʩKFDJ<8U>wa'խiNYr2kypf^Յ4ԘdDڛv}͹PœGITܳT9qeflHO;'74XM%Z͟2VB4Hնo,xXzď3PI3(rЕzd["i'cഫT^Gdzג4֬Z*Aj:uUYH3 v#%`HAfxm 2DH~i8n!pQhUH)R~1wT_n@*Q\qWOٓtbEr*w˭Ejoa _Sicc\krnO_*,}uN-{߂meyPBf~,= _L֜#YŸdsS~ nYsigWIo=M>_~O3G~3l\E^ϑ(9}S} UiO)bھ(/XiO)bھ(/K4W^D .Y"eį@CWwܧC_y@sJ.tv=K(Q e?[TI{Nτ+?VPM+MZֳMqF׬9I('JDֈsU<=U3D56ܜîX{!AcR9ʂ%Hz&mUg־t6Me1SyVk$W@G*:StL|vƏK;[Rfyګ5sBd#/ iP}&"lq Z$b^$Y8 Lt8V:ܗ5E\wny`Ì;FY,H8j[GkZgG7QƤgo)rPxu~9C"J~!kފLڨ.*) )Eu/_sGR9THv񪞜H*d[F+q[֟*2NW@阚VZثH7Ջj44,z^#(Ό$U.Eǂ+\֦->l-ff[Fwny&G9Q6 ֽV]O=g{6#?s&6ib#'&a#߾VDFW5]AR<.0'F|UcqUroff$zӍe(brm. IGNGwLzi–sHtWuUTbչ8f|Ϫ[Tp|i!h"%j"O*'ot9GLjHn,W,6gpIkS)GͰj.YLKGnX$7iZ8(":Ɗ3Vt{RNr#[qW"|WDl'UoETOɠoQ\/fK j%kHHDX퇯S]SfVVni9Ie қօn^?hS,R]*{&!;:WT)8W.O찋lSî?I+JjNԦڒrkS_1pވ91bAJW>w }uf6Xp4Kb.&몣WE$}5l{ԪMo^?8QSQY( VF>F~ T"Xy~nV?W'L@u'{ֵe ϕ u-TvϾYOvDUL{=zm,D^4L%%8|,-X575w<##yVY:dyG͑NA(Cs0]Z,s-f^GVo׵XQS4T#[G, f?9iÔHEt9{j*3>jI`5k}sc-IA^%bHIƢ}ulś!eCKF׶n{\$y{uZS\M񝓖Iǐ LtSYEELjlU?`rcqy? mܭoZ~9΁15Z^˺#syq{%;ZmŬv$DzP}1t=uk{sU7s6j*&%Z%e6(632 ѹ>vYG9ɾfZ+ԫ')=m|jؐ&ޔ?LR[&4w=Fz,údj=INRn<[5k ,lHI$(IQ,Lh+_W_l7<ᖶmCAӤ<|MMg=U˛w̘9JtUIO5=z؞rL)tԯdg:-w3]TTQQMR,0 _-@϶oFEɎMXѼ+ۚBIFړi>UcKo oΛmYXnLj ef*+D sVl`83+mvTWb7%ȸV P"bz+w? X_\JKÁj6#Z"&C$-]Tߕ17MAhs!tұb篇r=s9xY8Cxu=g[ޗA_u(r&Mq90EB7)[ / v[rU$b+Xb.b&NoȨF#$*h}ʉ4rg &d1B;jҊRysl!v5#"LR3蟁1u- u{G+}-iMq6&<?YzaUnnNn=vIV7ĆDcj9EG"hY鳺.iMyg킷K b'G/@Q]'=~k/QEt_}tڛ8QT΀ P s4{+iz?>k쭤_Lҏ×n};qB (]{L+bM{L+bME~^E4N埲&PLJ=a}yT:;n{81RO*GmcԾ;V\OėYMZ0@b-UPIHI0;vz~-+gQ(ޏil-8qOi´Jo=ٞ"<(Ѳ9UVpsь*W/Ur p[ u!uu.7Kb^EO4`1#*W:@ll+۩ \-RuhBmGdhl1B+=*30IeqBڕy' i(%@rb@&GKWVZ[3,x*ë"2"zAEtbLN";lO<'D}Z}M' JĀ0{/&gNei?)VNw:>G/@Q]'=~k/QEt_}tڛ8QT΀ P s4{+iz?>k쭤_Lҏ×n};qB (]{L+bM{L+bME~^E4N埲&PLJ=a}yT:;n{81RO*GmcԾ;V\OėYMZEzOe7x OϘr$WBG4ΰn[g~"Obyyy091 i_~O3G~3l\E^ϑ(9}S} UiO)bھ(/XiO)bھ(/K4W^D .Y"eį@CWwܧC_y@sJ.tv=K(Q e?[TI{Nτ+?VPM+MZ:mM~[(nb~Fg@(ع=5bhV/|G7>ո?ԋ!L=WK1G &sL=WK1G &YN"'vYr(&%zҿK<=RCZWwܧC_y@T.In zK، ug|!YiZmvz(n콯^7V>'&iM X?hJU7`+ý`[L̏v-O; հm m'l1w[`ry!ҷ_%< gx:[4Vħa`bTOKy&zQ$}ģ?-3.:@'? ӄD3?Zpgpo λO)g@WIo=KQ]'=~k6?kC@1}U?#3l\E^Ϛs4{+iz?>FtqN~EWy+?ĥjib+?ĥji,_W{,g쉔i_brU۞ǩ}!-+ TSʡs/}D*o՗$Ao=S%FDVsx:k֏>YCu46=h Ք7Rm^Lwbq{XXxwC%R*қް~ -K߇G ;jQض?6ض9<<5&ۥL+ddر#GDMAJrQͽjNHM^G+NM=[bf~ <fձyS)Kz*"床ٖ `/錁;їȄ]*{_"\Y.k*/,|mgIKyi=!4lT\<1p6㫱{\܌t8isKV[7&%9XV%,)''d=i?wݱGB%L6g =>\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAu0ٜ*|z$:l]= &;Y<}}#\[ÿWAFTAeݭTniCzƴ1nu/[ۗk]"tOcO>O *)X+?ؕI}ҷ_%< gx:[4ډ#%ĩipb= _L֜%'? ӄK;{Su|^M:N{y/>:^9紿\7]opً©ؠ@bhV/|M[H 3W/wVR/P 0_?I%,W[W%K0_?I%,W[W%If:hae?dL{J.tv=K(Qqi_brU۞ǩ}!Sw֬' }7Ꟊ/b2 )֛]ygiGV~soex[U7`+ý`*V݀am2X?tm?2<Qض?6VŽŶw)gaŰnޗ1SDti: *:0&檽Ri?V_Aȶ=KYkEy竞Q| 7T)IIxp AbC cQ2Fb"&I%0`r 'pR;!R!Dj5۪\}587}ӦI]sQ6$vK\69p1fRm`NQ!U#I%W>" iѺuʽXN8zr.>{<$J=ZFI#$YI`=? j9J.qɠD:Vħa`bTOKy&J•~,Jo$[j$eQD3?Zp{/&gN, Osw?qz7,9紿\z+緞sguˆvf/ ft_b [H ]6.fem"/GgΔ}^s)[HY@*4e|}']m_c~m,W4e|}']m_c~m%+/ywo,2bW!+ TSʡs/}D9ťa}yT:;n{MQZ-g~$Ȁ oZmvz(ng|!YZ;IunC/krkV݀dEZSv;!`ѷaGb;M[ ;{z^',W"[xisVR42:Av~#bz2 0& xbSSnR߃- U5$=n,E»~"kL1XWr'o5cѦ,nFo#݇^j>VOBHp_+TTJzA('lg1Ri3椨u4GXvuIܫb ZD"e ;^"zPNIȐnc9%Ղ]qKNlFM.LzE;r_;0t)WO.YĨM+*)X+?ؕIIq(%OLˀBf~,= _L֜#YŸdsS~ nYsigWIo=M>_~O3G~3l\E^ϑ(9}S} UiO)bھ(/XiO)bھ(/K4W^D .Y"eį@CWwܧC_y@sJ.tv=K(Q e?[TI{Nτ+?VPM+MZ*)X+?ؕI}ҷ_%< gx:[4ډ#%ĩipb= _L֜%'? ӄK;{Su|^M:N{y/>:^9紿\7]opً©ؠ@bhV/|M[H 3W/wVR/P 0_?I%,W[W%K0_?I%,W[W%If:hae?dL{J.tv=K(Qqi_brU۞ǩ}!Sw֬' }7Ꟊ/b2 )֛]ygiGV~soex[U7`+ý`*V݀am2X?tm?2<Qض?6VŽŶw)gaŰnޗiG6?%R*ҏmkpľֿrP<#m{w&H?eE:k\qYrd0I eZ$?226ױWy@hdmrP<"PyYrhPV1G(Ԋ#)9.`r1US=E6Yir墶$lHon㚩*|(E;5(Sn;RiwTF1H4|g;ΐa{y(+ÎQ h]+~UQag_t)WO.YĨM+*)X+?ؕIIq(%OLˀBf~,= _L֜#YŸdsS~ nYsigWIo=M>_~O3G~3l\E^ϑ(9}S} UiO)bھ(/XiO)bھ(/K4W^D .Y"eį@CWwܧC_y@sJ.tv=K(Q e?[TI{Nτ+?VPM+MZI *V}n/Γ\`ֵH~ddmck+y42E(/a_~;RȚC Dr%ߒf't a5uDTZ̚*.sNTmVP߹'mwgY噰ֽ;4Sx'iF\0W?ym$qrEU\AI  mUa: NަvcJz/C1pĦߗ}aKɽVΖXQa»y9Xq;zUG/O}m_t[U&MBEՈElHNWvrhF85\=e($MfUEX)]›PRzB2^6*. x5ibQ-gš3LIWnvާpjܔސ)nUZq\T^ceS U)YƙjSuMv@"***.⡎\&"ªoMMC9PHS[鼗9{&ߐ!VUX)u9IƷu*2qyFP{,G̴LMC j|* d+ЍnSjT[EfEEDT\wX*&j?Ƽmx/C1Б ̴̜JLB ޵ގj VQ &sKʊS./!'DtiN|2ZM5gZ_l?.61@17}'N]sԉ; ϵJNI30bDSao[E~簵95-dUFʈUN3a^VTÈ|$wW9YI)1%5b ʛVQa$ߑRN-/*?`u4NLbET%Gh*2O$sJtSMrFg&%5P-1l%dG*k&37#E[4zJnv U"B\EvOUdbrRV$#JtSMr|Yrd0I eZ$?226ױWy@hdmrP<"PyYrhP(5ԟi~J `C<5'k}=͏ޗ_}&_ n ϥZ~yFq%7lsؚuj`ȸe DUSV"} oKmsR_Eo~לhq./=ۓ-4Jձ#QE\lDTr'iX𸟊LWZr)JnG6%j<Ý!=sy4JGjToԏf9cEk,^B鏕J͗cN5 Wq\sL,XK咻Z򌛩Gs>*<_!L#םEͺsRﵭkQDDD7T*q)QzZL[dTk9I~D Mw`#쿧sM-ٺr^Wy˱Yww ;qޥ?R/iՙtʤ9 ޅ =..yV&,(Q>xm#Uc4sU2TT6iȱ!6<~}s>DVx= }ͺryJ-˅5qi[B T2K͟'| =*h^R[oOUs,04W+~h^RK\s#: Rc mJK3[vշ˅9,VGtg14Vk咮&DXP蹦%"DL׷&)iJ£ԯY-tusk!5~sX h-)%&Qy%s垬3CYaTrY'g"\. MFJ^f~R96؍]qr]7);>JV )z򎊱[PگVqڨ]11nyTqI>')I{Vx^,7%ƣqa1N'R'uRu&Bn#_-\K!11cֵ2DDDCaR ש9T{eY?[|sJ8F8z)6b͆2f(!UWCEMeY/rL@7- cbC{W4sU3EN⢔HN aCA.Iz<0ڵ7:Olrf߯2_GdҲ| ,HHb6 jw h{x:[4E%rU-W:7[Y Dwʈ'Dti)Uܫ7|f[OJ6yrdbofuiѢ.SUDUD)*V}(.^/:J毋皻^n&oNHejJ3fjH֯q\!B˓tF2خk&5DrwWY{'&~ţ[M g6ug̖Z[װh K k˝%i迊I7GgxK>n5z+0b 9FrkBУ7Ls} hlI8mZ3 Z,DUɗšk[kYWV 4ۖ|M ;NB)fz[qy0cbu #×SY9辶+W%ټ*ȅ-tL$? V/]#;8LcU+o,|ϟ3E<t,5K<\W"ի} ,%:29r*|Zq[kTISau$?֬g\nT4|A{rbsR4Kv_.YJד~|!Agnf_y&<_rٜfqʃu_lEٯy&NJ*23ʑ*b^ʋPٴ9E L:qs땻bErwuQ𗮝NSeaJHPaCLƢlDB YW{whEgo,d쩩WR|ydSx#cBJN䙫eJZ2ZWөRF^V^Fk]WF;f#w7%8~ᗐzM<\I\CLK9B9K-i4O>6JƋ2Z!iCعtBp&WD0l陘qT ,BuRֈŭE0IF.".zݻ2\4&)4)j–֢(>&,s\M9۵aʙ5䝦=2+PifUږM|&7Cpz],RY7I&2f-G[47ZB:9ssQQ;B-(%n֟b=5\¥RhZE2]%$6 mkZ':{JUI{e&[9uO]]o_;zqT˞MKX5ub-@'hqܐ4wI*9UmLm9**f1"ؓq2q$!58,65W#+K'Pfǃҏs5^tcUW}Uj:!sF w-ɮMi-g֘a(cVުohB=VИgZs /?GDE|;=V5b9Q3UELi[= n6BWAUT̪,uMti+My!T%UV$&\ j]v*'9RO+AF?6#uY,\KOHWyFtqN~EWy+?ĥjib+?ĥji,_W{,g쉔i_brU۞ǩ}!-+ TSʡs/}D*o՗$Ao=S%FDVsx:k֏>YCu46=h Ք7Rm^Lwbq{XXxwC%R*қް~ -K߇G ;jQض?6ض9<"(_'$EZQ :OpN-wZ!RC$cmcZȽM˕@:Av熤KPk?.[l~6FZ2 kpAZZVaKT99e 7EbK2rr։+j'UwjIqkMo#-#˵Uu?ʪm2+<:VSR^LNWdA𪷸nnj\S,6ٯo/^G+)1T~{eoZJ4 aWnt\4ٚf}sH5KLr_N{ܹ#\k5>R=\fՕiIZKpp*KQJ-&oߠ "?//(c&c7=jT#\,f=\ڪԻ`O@,,^kCt o8P5:O,:..rcBr"*.[6Ke4/)hʹ%C(s46UuՖo:ltjF&S12y)53T刌o:Vo35V5yCKε?Z0KJTq{ϾoSC;ae8\jU埞HT 'F˒1Lܫ"&n${ƋbTskE؟&eF:^lsNQqq-QQI>~_ %='VLy,YθjHITX_0 CL !FqۆMsR$*Y*˷_!tdw;{q"@Dn^2^PtG% Sb]LbjD_Y(^9 룡6uƭ;2I?1㭦%J5({W +Z&+sfbq"5#IoF'+.z$z{&K殺_Y*fô6*R{ZMqgxq3Ĭe:PeƟ\nNOҾƿ^e2;ֆ,7);>Jd ᘝ Oeȿ4}%Gҧ\@ [(ҧ\@ [(\j/y扱콗*"/P:ԗTUCuZ6aw-hXЈk]R"E[5u%ǔl#Z77 jլL~Uvj1wX= P:W;+qt?ܖMX᲍jR vXqdݳԹ:'JC7 W2G^kVև:'Mku#W l5*XU9K'ʣii"JciMr/ucDCH _Q=gKVVt†E5ni% .hR5qdb-r*= F關*XI|BbJ3˷s2ֵ֜^MΌ)REh[B3jK4sMEixyC.GCcmsU2TTXQP`zNuFV-jޟ3*&QVi+&1o@3C*T$ړ^\Yp$`1z=V5kEe><[(U ._]j 06Z_")K%,Y}!){p>ɀb %ZI܋Ȟw^L?).c[ GCLdտ"TAk~.ONXFpX4MSS~&HFʮpQ/;J.)So(sFHpߊz&pz} ( \-LJU"9UܫZȭVk9{HG/旘'ΚdOB(,k "=({?._?hbB_E _7l陱~}p׾]b.~&&erVBև0ErˤMHviZ_DsJiU^DbXIMFM5ScUS.5]iImAmp|ɞ%aw ):ZO漗&hz# r "?//(\X1LBdxFmsU3EEEC˭z⧤ ?oKS辒T0ћ1TӑZbDlG#sUU5],[[ R.|I)MGzˆsEت 5ӻRRM|rKcYuګJO>6KpĪKiոofl˻rLڻ-DTwh|2QkRpYr/"v˒gE1uZƧ}~8.RjTlpg>b]$.AvjHsr]TNcmKo}^5Hr*uݳ]EN[Lv D[Cl(SZ r'DWf8}K1  e}AB؏q˸ELyrR%VqWj֚[m2_J :%oK\Egc{^I j._lw璈XR\ 8y%Ҿ_\oPJ•~,Jo$Ҿ[R]Q<-ymDT̸1{/&gNei?)VNw:>G/@Q]'=~k/QEt_}tڛ8QT΀ P s4{+iz?>k쭤_Lҏ×n};qB (]{L+bM{L+bME~^E4N埲&PLJ=a}yT:;n{81RO*GmcԾ;V\OėYMZ"cx 𻌤(EjgKN;&S)xM$lGjsؑbw.g:[Qogw}P^ޱmг~]aS+WN!VB0+IUc7l&&I.Zg~ h9-vߐس%)Ȩrn>*hMjlEUsؓ.F5'! %6SQq2¼*:6ٟͶWbiV{5mˉdY㚎Ek2T] )6`FƭR%bEkˆЮr~[&yJߑJ[Ha7=URE47A !k4r.*oV}ªwLW.nY(΂.|K>Wxe ؽƚMjfB6=-}ikK=nEd4h ^6_ҥdh r1+DTWfٱ E.wªOt_nV)mlTXZ{U>LUw4ncg{g̱=> \3l*h^RѕsKe4/)Ӟ叵>I{K "RoL$lVF{0s &YdghM|ViԠLeabq\T_0 O6W9)$<4{j:5aTwGK\ՒnFlC`Օ*Mu\ Z'=w0[ʴqzNR8Ro,_&[KDҩTXXѦ4G*dn{SjՅp*R_KcDTTX~*"]\slo[-oz2<5Ecb2"hHhF$D2vVX3UNqؖŖ[rKnѰxn Iמ{3of;Lr0nKvj a *'fjvT%ؖCO*"8n#_ ջk?#(b݅3eRn4TceE2iuAt:fΟd|,s3D_J^طnLU&8&^G*'iUPh~aasgji&qSR4<|wFu|BQMgYwKznZro]naSZ~R[벻zXPfg"MGbF"#Qg? >xSGnz6#wr;fz IHKRFV I!Bb1N"lC7+s\),Q[85QZ)Ʊ^i=:=|1MމW&FT u%2L2g; ]nyU7qM~l?jn[AN:,OEG*dn6)\^9EBYzzeƎpzêFNu3ɤ֒ׯ>&mXDʏOd "v(;QDz|?m\F2`$OEG"dǢnv噟)v&ZE45 s[YUWj<3n1DI˃Sq6m,&koΓ-9fq, \QZ*&iSLQS=3.;_ d[̵)VIilQb7'=WNoȨsz6B{VykY=sqqJxڸYyt.(j-JLm5ܻjjj퐋nYhs1회kS&*e۳%$0lH8M['W6G "2M-,LF*w#)[F0zK5/j2[9ŵ5&j~\wMZj05d7^5c^/qSyP:'LEׄ{TXd&H EUDɏjnv噴JJKJAKl(LWUL3]vhLӖJ:y,Ki&ǭhCzHef0OifنD`n=>MUMNHX7x1-6(t(GWq]_Q cZu~+?,; j/u6CQe3u^Z=l;+zg>z% Ki,w-j":,tzﮣJ}TF9"#־fu{k?ֆ;>MtH4DURSz$g™GS?W|\C t)t96VipvA*,+T^*!$EHH,o kZ"r`19P`΍(@s}UU*hmJ-2*8Na{k f{Rf%mgVܶ76F7&ׅEۣHک<{f*/mQmRZ5jP#"k=ڪ&ګk6 ፵XTT6TTTG/oF6"& 1[iζ)_9e[.dG*j9o1S);JIHj9UUvpSt"BEڕIxmIh7Yч]Kݔz&IVED$F=LÅޮ-SEl)TO"K+ʜj2疴-[b:5qmNR1I<9,ַZɥA퉄{UyF\;QhF8QcW1Bd8MVmW7빹34;jXjnSOرiSEDz>7U]KS(Pz|/Y4c&qkV'_~O3G~3l\E^ϑ(9}S} UiO)bھ(/XiO)bھ(/K4W^D .Y"eį@CWwܧC_y@sJ.tv=K(Q e?[TI{Nτ+?VPM+MZI *V}n/Γ\`ֵH~ddmck+y42E(/a_~;\M Z2"Ɯ}^Rd',lvUʮU2H֖W0жMW#\dDomVeRʼFĔ$R2ny3Q =mORFQS\M$c1q<)ɧƛy1\ 8y%z}qJ!aM }ٯʿ*2, r>*)X+?ؕI}ҷ_%< gx:[4ډ#%ĩipb= _L֜%'? ӄK;{Su|^M:N{y/>:^9紿\7]opً©ؠ@bhV/|M[H 3W/wVR/P 0_?I%,W[W%K0_?I%,W[W%If:hae?dL{J.tv=K(Qqi_brU۞ǩ}!Sw֬' }7Ꟊ/b2 )֛]ygiGV~soex[U7`+ý`*V݀am2X?tm?2<Qض?6VŽŶw)gaŰnޗiG6?%R*ҏmkpľֿrP<#m{w&H?eE:k\qYrd0I eZ$?226ױWy@hdmrP<"PyYrhPeÎQ W!9кWݚ+*#[R]Q<-W+{ US˰V*'O=m>QJF eiX"{cAЙ8G7?]ާܳ +緞s({K?OΛSq !۠}*}6.fem"/Ggtع=#:Qxrϸun?"AeiRu}Q_ɴ\iRu}Q_ɴhӫȽݿ\D ^1RO*GmcԾ*])PzP>7G}j˒ۃ}#"+9iGV~V]ygk6/k&Wջ ɬEZSv;!iM X?h%F#lSi5l([gxvI[ ]{X@VHIkJqTJ ]2𛚲Zu!dEPz% ROb̏eQmR#ɡ56I=TI ފlo q8>׬AYrmGQ7}8+MhNm635?4FҙoILsU+HR^t)WO.YĨM+*)X+?ؕIIq(%OLˀBf~,= _L֜#YŸdsS~ nYsigWIo=M>_~O3G~3l\E^ϑ(9}S} UiO)bھ(/XiO)bھ(/K4W^D .Y"eį@CWwܧC_y@sJ.tv=K(Q e?[TI{Nτ+?VPM+MZ:mM~[(nb~Fg@(ع=5bhV/|G7>ո?ԋ!L=WK1G &sL=WK1G &YN"'vYr(&%zҿK<=RCZWwܧC_y@T.In zK، ug|!YiZmvz(n콯^7V>'&iM X?hJU7`+ý`[L̏v-O; հm m'l1w[`ryq qB˙Rf29o\j5v{! ǹc$7]Tj_^bY師Tt"Lc ~~[,9 ʕ겡 .u{[.v*.dh9S\%Kq*uFo%yOd$p<B|X R}"͓ɄVݎs3کnyW}7 *|?:^9紿\7]opً©ؠ@bhV/|M[H 3W/wVR/P 0_?I%,W[W%K0_?I%,W[W%If:hae?dL{J.tv=K(Qqi_brU۞ǩ}!Sw֬' }7Ꟊ/b2 )֛]ygiGV~soex[U7`+ý`*V݀am2X?tm?2<Qض?6VŽŶw)gaŰnޗCm|JԪsشk[%ɦVn#5>TTۜg!Akv=n W1驸2__,sZs\**fG ŮRyqʶ{զ5:tZKbz~억ɮ>B)&ƻ`- 摌8Ƚz=%3c9emZ̘jqHv/]M1̘НZ$KN' EQ!>n|$ԅ5WU75Hk z,{P pW7h!qSRtH\TԸ9c;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?caj .*{\N z,>P uѫ4q:rä.*{\NC36;Fo:B⧾q:rzGXx=@ z,:B⧾;/?cajyj+3Q#Dk*Foɷ- n(_\yw@>HʟSPÔI랥_#*5wyI/(6/bg=kSR}$ocݥ)<%+9u⮬9Z\5{\DT_V ߸7%ڐ*"軿vEgZ|{~B Qއֹ\i>!Ncy_>^3Fe˿fyitmG%',H0 17:G'6'`t)WO.YĨM+*)X+?ؕIIq(%OLˀBf~,= _L֜#YŸdsS~ nYsigWIo=M>_~O3G~3l\E^ϑ(9}S} UiO)bھ(/XiO)bھ(/K4W^D .Y"eį@CWwܧC_y@sJ.tv=K(Q e?[TI{Nτ+?VPM+MZl3G8%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`:0/6NrCqd$3C%J`崔#!_p%&!t4ۨ7t9!u"KC|OO=|2oќW-{QD:Vħa`bTOKy&J•~,Jo$[j$eQD3?Zp{/&gN, Osw?qz7,9紿\z+緞sguˆvf/ ft_b [H ]6.fem"/GgΔ}^s)[HY@*4e|}']m_c~m,W4e|}']m_c~m%+/ywo,2bW!+ TSʡs/}D9ťa}yT:;n{MQZ-g~$Ȁ oZmvz(ng|!YZ;IunC/krkV݀dEZSv;!`ѷaGb;M[ ;{z^'AjUFjzmOM'if,b(ﶶyV4(G})lH\MgR"nj |]+WyD.[_mR)Zڋ^"f(Y*/˯Faˈv_&b*+UIi&/bJnӏ/~WxEuz;*vV4‚XϴN<a:aKشwYMeSoõ5^V|6ycXui%Ğ.DK2IJ!VCkSy)Q>= 'L=inqs#w\teB= 'LkC\;'@_(_ |q|/4ִ́;̇]wNQ8:!D^ihw;0|q|/4teB6Z2wq߾va:!D^hyl.d:|teB= 'LkC\u~لy{/N/晰:ևx뻎 = 'G@_(_ 3`us!wf{/N/掁Q8fZC;'@_(_ |q|/4ִ́;̇]wNQ8:!D^ihw;0|q|/4teB6Z2wq߾va:!D^hyl.d:|teB= 'LkC\u~لy{/N/晰:ևx뻎 = 'G@_(_ 3`us!wf{/N/掁Q8fZC;'@_(_ |q|/4ִ́;̇]wNQ8:!D^ihw;0|q|/4teB6Z2wq߾va:!D^hyl.d:|teB= 'LkC\u~لy{/N/晰:ևx뻎 = 'G@_(_ 3`us!wf{/N/掁Q8fZC;'@_(_ |q|/4ִ́;̇]wNQ8:!D^ihw;0|q|/4teB6Z2wq߾va:!D^hyl.d:|teB= 'LkC\u~لy{/N/晰:ևx뻎 = 'G@_(_ 3`us!wf{/N/掁Q8fZC;'@_(_ |q|/4ִ́;̇]wNQ8:!D^ihw;0|q|/4teB6Z2wq߾va:!D^hyl.d:|teB= 'LkC\u~لy{/N/晰:ևx뻎 = 'G@_(_ 3`us!wf{/N/掁Q8fZC;'@_(_ |q|/4ִ́;̇]wNQ8:!D^ihw;0|q|/4teB6Z2wq߾va:!D^hyl.d:|teB= 'LkC\u~لy{/N/晰:ևx뻎 = 'G@_(_ 3`us!wf{/N/掁Q8fZC;'@_(_ |q|/4ִ́;̇]wNQ8:!D^ihw;0|q|/4teB6Z2wq߾va:!D^hyl.d:|teB= 'LkC\u~لy{/N/晰:ևx뻎 = 'G@_(_ 3`us!wf{/N/掁Q8fZC;'@_(_ |q|/4ִ́;̇]wNQ8:!D^ihw;0|q|/4teB6Z2wq߾va:!D^hyl.d:|teB= 'LkC\u~لy{/N/晰:ևx뻎 = 'G@_(_ 3`us!wf{/N/掁Q8fZC;'@_(_ |q|/4ִ́;̇]wNQ8:!D^ihw;0|q|/4teB6Z2wq߾va:!D^hyl.d:|cMX6W3[huWF3e75g]գz(}J$IJ[8φk .:\?WWYdm֧qMr)r-kFz_>63PdQw3|a呯ȵZHpV<o+.Bh%x{Ko8z"=}m=% ogԥ"W1*:̽q'sQ &vy7aWsU2]o\G"9mYtkKj&+êaY(K\dIy<=iǀ[R]Q<-W+{ US˰V*'O=m>QJF eiX"{cAЙ8G7?]ާܳ +緞s({K?OΛSq !۠}*}6.fem"/Ggtع=#:Qxrϸun?"AeiRu}Q_ɴ\iRu}Q_ɴhӫȽݿ\D ^1RO*GmcԾ*])PzP>7G}j˒ۃ}#"+9iGV~V]ygk6/k&Wջ ɬEZSv;!iM X?h%F#lSi5l([gxvI[ ]{X@1uHa,W랻cS}rQ;j6bX ^nm3=u!-9w\W/\NR񞴊#R]{Nw r~=֒d8pCl(LkFL7xzFoY/(9q0=D벩RiM5wr.SP+fܑ6|xXO DUYڈ&5ksս9. aD2sEFjvԇ~RR_f{חW B^r۩/'ȉ^wIdmB].|*uף6JgoRT,\Mj`t)WO.YĨM+*)X+?ؕIIq(%OLˀBf~,= _L֜#YŸdsS~ nYsigWIo=M>_~O3G~3l\E^ϑ(9}S} UiO)bھ(/XiO)bھ(/K4W^D .Y"eį@CWwܧC_y@sJ.tv=K(Q e?[TI{Nτ+?VPM+MZh Gxeln::;MhQ\MoٖƫBIuEy@e`㚎Ek2T]4 ں,Y/KmJ ۩;S6q?\@Q[EҢ[ o9ժ+P'&zFOH XBO-쟞Qo0CoaJ|Jyv %Dti_t)WO.YĨM:mM~[(nb~Fg@(ع=5bhV/|G7>ո?ԋ!L=WK1G &sL=WK1G &YN"'vYr(&%zҿK<=RCZWwܧC_y@T.In zK، ug|!YiZmvz(n콯^7V>'&iM X?hJU7`+ý`[L̏v-O; հm m'l1w[`ryEzP=ۓWߓpIPq~t=[C#jBd ^drjwPʘk+y42EF_q_`,wa2~>JX?xK6+a2~>JX?xK6uy;˖~ș@1+*])PzP>ҿK<=RBYrOpo?^d@g7S6=h Ք7SJk֏>YCu-~e$v!95JnWzD2U")^d~~dx0m m&lSi=a_/k*ҏmkJU`ۋ2uk+y42F69J(M e~_~O3G~3l\E^ϑ(9}S} UiO)bھ(/XiO)bھ(/K4W^D .Y"eį@CWwܧC_y@sJ.tv=K(Q e?[TI{Nτ+?VPM+MZI *V}n/Γ\`ֵH~ddmck+y42E(/a_~;5 NY,2@OӣÏ64G9vyL_xpئڛ)2>,>{]5.֢,R7j׭G_|Yl\g8]KS4mmyf55obn<|Xwx!ެ\욀mkM ^,|\UuQ32R3U9HS32ڏ {WqQSb7J2NKjZGdՌYE-O:a{y(+ÎQ h]+~UQag_t)WO.YĨM+*)X+?ؕIIq(%OLˀBf~,= _L֜#YŸdsS~ nYsigWIo=M>_~O3G~3l\E^ϑ(9}S} UiO)bھ(/XiO)bھ(/K4W^D .Y"eį@CWwܧC_y@sJ.tv=K(Q e?[TI{Nτ+?VPM+MZI *V}n/Γ\`ֵH~ddmck+y42E(/a_~;> jO jԎioqk/j$z#ݫ~We`ǘqgʹ̈S4N3ػ+l֪oÍ W?2_?QI`ZeTZ\^N2iUN1kjyIx-, F\4g :#a-c~'^̵nej$Շ4o$f۹W=Fd1+2jky]&)D"z:ʻސb7BZ؋ Lm>/ihM*9Г֟jfylk/*ciԢ׊חcnYi|]( XG'ٶ6`v]c;.yhdnq#*o.\DCF~bة /jesasuF2mg\No!(PƓIUuy2JplEɒ-t^ujkRiͥ{9;8ˉF :ſ]^zSU#BI8j9:Qv9b{8*3POVri,Z\^ӶZmǨAd%1yGUMf=K3EMwJIJ;(jh]S-Dr0$#DF9qU7m<[oism ebQs{Q7\瞥RT6|k=kRzt&{F*b? RutI EHk5UQv'ZChW싪Uڤ6,Yƪ=&ҢTCdޘLt8q YD]d&EئJqŃIњU[թ\Igh,b*=ilԳo.-pû-u Ųltk+3|~W_rNQCI8rnmbo0aVJ%\t㡺*CYXnj'=v7esFe^s[kZ=VxW/ހXĀu"` a='|A&!;-D!dMo6NNKdvǟ!I):YIԾ6=ѫqUhjo$0ab?y&skr/a_s߶u^d\a9\ީ!A"+!EL".ZEʋ%1ae-h6zp|B;z2IϰPvʶY\]OHs^{k4T\PﵾE4yr{(po5@4fÙ ֜}̩ekб*"7[WWuSnywt-u棛f,ϛ{KռYy.7 s^6ś&5jZBFsfnX")LiG"~7<"7뜋5/ac-h6zm0K'>^k]0$.MTk NCX*Ujs7kyo{m55ƚ~ugqe>s^ǏZ &c2(mW=r5jnD= 孞dy#aZ? (-1Q{',VET?:NYT3~tܪsȟ 1y{vuTVfcw滢sJIJҧ0e ć {V|6*=?#!kfHN:Y=:g_(te(wvZ>w=uW cZ˱չ%b̀#9;hƢ.]DvbYs$u~]c8}h\Mญ:J\\BU)A1&i/+5ۿ- Y=j &p|hXܩY&iI eXF"jlqp)+ьYckbe(%|BЙ׵KdYk-@UVRx m-P &wK{_{Q#[c+<űB?YȮc*f*vHY\ayâ/ ߐ~ &թ%)gYe;׸^!Vҕ88,=i>A+x7X^q7b]R]QcSsМ=uUo'I nyJZ\ qWMoqkIէeg |@ߊ5?tV)3?Ė6k+s͍TMS3m\koͬ̌ʽ!XnfzVr"]O j*Emk5ʶ/;+TUAO'p3(49x1&#;V&޹g"fz[[fWVW1*Jg^O'|PEOI,| EZZQuWWs=m#*rj˹Ϻ5 !ZAJ}M\C?{V0y}I%C4GÉ 潫S~]4hôɀ >Ŭ?RqAdSl4T+X쎛66^ֽēonJ\1RF|qXۆwT:}2n+C|=ʹ#ZDkWfH;\OyJ|=NR޿_)t,3=98կuM(՜2/Z/p#N DO н-kϬܬ{ *dE{vm$h\JId}iqFqKi=gu N!G!] {Z\EN}%V3e4fL ުTUS[n1w⭋bT ˥&cI1%ZFj۸m^M,̄޿:mWֹSj0U j*O8fmfŷffnѳC{J-eY]%i3۰˽Ŕ7o,#ۓٙ|ѓZr06+=0Qa|TiVQǂ?џS1Jꐷ_u16n]>[Uj*# sv\*"c)N+ATOcZ1u)Ό:0geÎQ W!9кWݚ+*#[R]Q<-W+{ US˰V*'O=m>QJF eiX"{cAЙ8G7?]ާܳ +緞s({K?OΛSq !۠}*}6.fem"/Ggtع=#:Qxrϸun?"AeiRu}Q_ɴ\iRu}Q_ɴhӫȽݿ\D ^1RO*GmcԾ*])PzP>7G}j˒ۃ}#"+9iGV~V]ygk6/k&Wջ ɬEZSv;!iM X?h%F#lSi5l([gxvI[ ]{X@V}n/Γ\U"(_''a;k^)]ɡ1ױWy@hd-CQ^æew@ m-P &@[xAMH"G=ڷ~~Z"0=)?sq~IY;~^FʼnOz-W7mq4ķU8klL|E"anJ־}y[ KPdY"\Ei&nWl 9(N"5;IPu;|ގ(,vjeUrkf]uI{~,N,g+FB&y&M؈sglWQs\`W0&L%]ETLz/kVӛTi埓<$כlYQj]TR/.Ym%%0ÆP Xj'qR6heޭG$4Z*j#QvgTYHEVV UNd8Hb9L1JNA9ͷ̵q2'U<[Q\ydzì/r33maK2&JkZ'^YuS4jMtl{m{YU$, [mB <'&6mXQj~Cvy I$m^K_~7XKӳ[I$7fE3L K%Rt!"@rMHrE\Y,/h56E5:n]56gk5֩2UD "KO@|T&[;(=5db>iVŤ6ؚY>Sڭ hYyJ9籬O53m,*N5eOZyzO-M5_biقoB.J4pt١m;O呈뺕? h~,3~,3߬hآD+ݓ#*?׆X oQrw}vEnɑ ZAˑ~i/{,KgKd.PduKsUVȽb!W{ty)- :DGfM^biCE:YuQY>tY9'5MmϤs N jc6jfwnJun5_!Fj]gounfs2 Q={rO=,Nz7ҎZf[ s/9ԝg9u`A%ϕwWyWxrw7+ $HZ Wj,nhn.mzŭb)Yy9>=HHk2IRjf!ÆG._"[iyĩQ=!$ƦHQQn݋\%;As xuF#b\vzb$Ąnq rf]$U;g5m(Bi7¸m9´H)^AsiI8p>Yek6'͸1PBH.o.r﹪uQ{8*QioF497e2|2Upܫ<υY1qRñ=j-\O6?8GZBv?6[0/EY0ϼ}N6(:qm nPvٌWbR9'gRocn9IC8&0 B5ynWҎFFꪢ*mj.M#:Uvui*QR7ծ+ a]Nj,ZiO_K7؅I]vF"!Cli7!Ƚ_1^k՝ e,%\ڨUrQY[iiU .^ !Bnu:,hc\9 rB\T!k'ɭVMμSmZIDJ$I/[߻U`4[F*AWXTUD~[%6s9#E[z#PjS.6fE^*g棕lCû ki 557A29{"艗vNVD"u2/`(`MF8lSK<[_k2S:8ΌW')M&ȶŸeqbᙀlH57DcyȨ(1Q!nڲf+sdz6So +smjY5n#EM <艞dԓgWz? S?bQÊ jġQCk9Č'D7b_ 6od{^c=/ĩϩ҃*)lZaH_]=2z:Ii1Z, ^ڢyn*$E=vZ4JPmϗz/;wLH٤Ԝ)*EtܼffiZֵDDLq[} (TgK|&mz*M1hUެE%\Zֿ9ChʆWKfUֆDTlhKs\w3M.Α럮(+%A +"đO\&~kħ3 e;Mq{z&<'G:{ԥ-.N)>Y0=}h/C{)Ef7Ak{y|H10L, 93F']S}7Z PDbO<' 3y_QO!RxN:SQO2O/)<.' UVprk.;k?!X8{u-7g܍k5B3Uu\$$Bi]mB#z'-2\=x!waT ˲+Pךk㵯rN1ߐzna4;X"[Fufyfz۵pH{waBK(mV{6>,krvY'"bW+q6=3EتY| ȸj9*)́8sT"䛚*/uIyaׇRԻ=PZ(tE]tkQ?gCARWi5s5~DgTHke= jO4-|i\ᅪ] ]_Sץo,c/;(^]b]WSeVeL <|ɟp1Aj &-zNvJ)V;,XFק*ҸC&S^ɹ:Umi E>F 3ҫ5uUK/+"ZUʟ h̳Fy$nj;9{]$vd4پNtZ=>ߤQ)pRJ`bo5{LՎ'{U딛{xrɮa/U)u*KTbi.&$86Ũ^Ry m-P &sK{_{Q#[c?-nh]aJڕ@›{bFor&Hc6b éZVܢy(嵾.>"ehN!b5n 2k,ܳԒ.)_TF1p^H8u;jd릣BXkU2LYm7qM՗4F뛉A4kyώ+sIK|B`˱sIK|B`˱`knc?dm>LkbE=?ޱ~uKE'Nʶ~'qyJ5mOξWl K~U-,-m}46NaF*گUl hH<-I"mY=Z)%BN×Vӑb9k߁_)xkRjy.ϑX4b1NYG>͙"d6uU#efr>} -%:\jL6$=H3\69f3MmKva)(uF!Rb3 srCCp[$i7˙w ֫k4.ZEzZtI)Ok:s[G5羥)Dt,Y՘{~E_FԹ*^}0Y왼+2rT$`fziQDΓԭp:rqeõg֌S`ڑRzIY6EnyD]Ƶ׵]oU-ߢsbMWYڮII/q"[u $ 7Բ?#jiu KЎSKiuq݉[!Uˎ锧Yhۊ,(bUmTG+Hڍ%̷Br*Sͤď=VO\ctڥѷ'8MKՙ[vz7ZT$O^FjϤWX15Ҳ,jvhɹvO.U1cZ$昙[D^Jb.Z*=Kyyxӭպ%&Y)C[W>}ұBUvshCrv%=DRB<&Gt8Gɸ)IR.\Pa"#] U7K{aEt{ݎ](V T4ZᲖq-\|ӨӻR:R\ٳ<69._lw璈XR\ 8y%Ҿ_\oPJ•~,Jo$Ҿ[R]Q<-ymDT̸1{/&gNei?)VNw:>G/@Q]'=~k/QEt_}tڛ8QT΀ P s4{+iz?>k쭤_Lҏ×n};qB (]{L+bM{L+bME~^E4N埲&PLJ=a}yT:;n{81RO*GmcԾ;V\OėYMZ'&iM X?hJU7`+ý`[L̏v-O; հm m'l1w[`ryEZQ :OpITq~t=[C##m{w&H^)]ɡ,G{+u-BioZ5#[Zډjߕrֶn -v-zܥԟjarNeb+4Ez.HL *ÊċK> 8L67QSqHD_ E'ɢvհZ.84z5YS%YdzW\8K>]B4edجckSӱukܴDd?k*l{\wI8KmL֨Xbg0zGեV"z3;wqWs -ƥUŞ_$8v B~%Öm-FqH8r*:梧T/1RtR/w(N5n]WshsS- ᑫܒon!K)~\LVHqDXNEc%ME܌|I6! dϮM 蝭\mJD {gJJ+٭y#ZToUd'X(5W[%HՉw_I Ӟgb"B4uNviWxOn^xGsV+Rmnv䡻]9j.BWpZ,K_1>;:X]PKoYͫ(f]ri'W*b5QPC^y&8n5Q~Y ΚX4KaIJ heF$&֢ ntqF^UMJ^hsb1*b'@:+vL^&iMxIlV(t^3"DݭތDw'vEnɑ55s WkN=>\4l,ttN[\%:e*=]~TM)>&ÈkDo3~?>K {}Ss4< aMs.D]{/ks? a2R*#\q {w̪Z2"_ɺjwR4L18u!ni[VMbĄ_.繪eQSww$=,xJE- Y试TFEX# 6l*`r7dFk֪&NwX}M*oS\u q6m!wOEia)-IMԹϩY?:+57Śô?J]$;o¯F=}a[U*ve:nˡ6UȯF, h6AH1ײ L.H1ײ L5Υ呰w9_4Kf6[4l8 ν`z5\=mj*f3D H_b ՝EOϏ7cY}NJ)Q^\Y-Zkʝ۶#:%e{?';4%u{c(-{K-2#Yi,ذbsG5S4SlM)E晭%D'5zs /FKRnzflH:ӓQDHm^sOfΑBxަꜳlo;} TYo~gR9s,aC{)E_LR}n?qm^HFpsthY\Vn/j@MmXW{u8|5hq 8~ .\&|[Z/[zp%!CJBHk*Vk, R%c~&84e ͦ.ne7/]Y\G7TM^5FX;|:[xV3Ҝ{ k*o[gŠԉXᬟ7 )Wl;f䗝c DΡ˹M5ᘶ"δkѦԢ_:[W~/yFV',͎>&?ca*#6EL΅g +m Gy`H7u{&^pص jO$ jԎioqk/j$z#ݫ~WgD)9H1U&+ᣕQ{dM/l[f7~AO^/_9,jO~Ty=)M~ vs UDOԇ$Ӝiihb /}!iq'E ]H"*JLYLVDDrU5%KKc=NQyC,͏iCZֽ"a< ی{.KK3[7T HytDsZ\hJJ1o@piSMf$e[Nʶ~'qyJ5mOξWN7G/T{K;`wo@?.5g.Fk*)nW|Qm7uğn _sH"TtX3G\􊊙Iۋm}] \OX=|v<&HLk23S-eNӲO-<QYWuMחz埵>MgMi6%or5˨5(NvI<9%ؠFP1e:.rHq۬ɱMq6ob6F{-1LncܪZY=Z)3hv) gvyhͣaW>^}큱Vy{&iZ\ˆrdV;XySDXցpG&g S%b)ikDuНfMn3GȄE'k^8)^ ˓[0U"= 뿒ômQϖ.>ܹGD**m$ϘTDUUDDڪ5Z gHSn^J۴Qcهv߃O1)42Hj؊U{ӶTEDE, <4ӦMF%GEr{kWf*MgGדkx֕NM:^9紿\7]opً©ؠ@bhV/|M[H 3W/wVR/P 0_?I%,W[W%K0_?I%,W[W%If:hae?dL{J.tv=K(Qqi_brU۞ǩ}!Sw֬' }7Ꟊ/b2 )ƝKαGv ?ti¿pY X.wk^,[E29 ٫@EZSv;!iM X?h%F#lSi5l([gxvI[ ]{X@V}n/Γ\U"(_''a;k^)]ɡ1ױWy@hd-CQ^æew@ m-P & n/Rʥ{YgykOfkdpCW7,S[r~?QIagosזYٛ91^ԼwjOQM:T_6'E>s>B^U0H[ |GQirzy'"SNR)Y%VIkS,|U42fVdĜ5}uiPnv$^fHkMKZ4bDcv"jK ;ָg[Wʿц mGL=!1R.O=j'6+ۗmU>:EbI˅~IiHM_r|\ bd&^4 : OJ? <]. ou='a=?Ry* NJ7YTHCh~#fKFOW{2&Uj݌eS5}j޷i1'4L"j>r.EDعf"l|&k"CBnh]QrmfsYs>}o_Mn.֦QI䠗':[(^d킄(MYeƌncם~,.K|K> VI"='ltGV_.)#]q(;wuz[Ŗ-K=[x uW y{}  ?r>e,=$ܑc$FgEnHb`U1lVb5ʹz459jb#aRd&cbS+.Z䩞5%6RR,~='WgerKo=IMDL#lLE>dhz^e*NSf_@l=D"4GP%O'\Kj_\Kְɾ7uʁSC؏wf_$)h֏"Ek_6y/\۪o: "r٭#,X=C {B ɵ$zˀ%ֲM4QIŨsE2K~`TTo{3gehv}OIm-G`o{2~۳6j(Ȏ DOފL3ըӸ*UVqk& 0kTR5H +,.=&EtVC[U7ݰ忒m2T$ZBY%U'5S$1(z'ܳs.Q-Iz]R~UB*IK ( ~~u-6yN1m#VKW)G1vEeF]T#.OU5nRlўv)ڕ!PHOHQr+o%*E*L Cd0Ԫ9ykYpŴVVveYgg1W 6m8Y n9vfnM<_gi􃰴.(=UIgYd{|6Ũ~Ry7)eR},<528F!ثwyg?ap0YSo,mej^owgY$丸>Ƭ3\JRREiY'b9U2t5]refT) 'u}kfM]jڛ/IOBXf!dV#c)OW*:UW g i4nu0we^E> 4בG](=ezP.|孫5n}T-OwR]"m5Sus]j{jN'>i"g9Hm\y3ODnneʬ.v~SSKm.ZFޭ&~^BaZ~1YjE  `'AT丹̲Ԗ[_,y]oc{©if)rCgqvB)=og8Д-9ɦ-bf)rCS%IS3))d a%̲yy B.3sm>nj2M"liz|TR;j,F QrWj&- C[-{[qrW{5$%,[<|yD#M\ px;b-2,mÞGH:<={>Zq##+!'a6X/y/ `s _^MUmYfۛo$xvR˩zK<(%6\ 8y%z}qJ!aM }ٯʿ*2, r>*)X+?ؕI}ҷ_%< gx:[4ډ#%ĩipbEQ dF3t_<ȯs QsS4wbAO"~52hֲ'ۛG}E +緞s({K?OΛSq !۠}*}6.fem"/Ggtع=#:Qxrϸun?"AeiRu}Q_ɴ\iRu}Q_ɴhӫȽݿ\D ^E\w&IpXPQj_pYgWI{"gd7Th|r664ݦWCd1tZƤlP x7cJ[Y&m=\e8Q$Dv{z Cl4ϰjݏbx1(ok/Kq~tiG6?8{ k_ćFF9J(M RC$Yj"5k.W,}qJ!aJrd0EEB<{z cD[|*|A! o͡-Ţ"њ꒳wVLgsȋꞜPWk996={od&5KQ~lkЉr7Mv{FCIGSc"͊j*odȌBn5um{٩zQug+:5,7!kK[û3?EbYlaQQv*(;)TtFښ|wѮڦ [q3MixQ^OQi R-̊-ۚrMD&~T[MI|`raEZQ :OpITq~t=[C##m{w&H^)]ɡ,G{+u\ 8y%z}qJ!aM }ٯʿ*2, r>Γ>ʴ ]rP;m뚿=+5 . #l*v*zIdU8.RjSײ(2͟Ërgg"#aBsW?΋-y-{ګMMLBr>TDM@{a-, YUH!uCG8l7ָ۟VSy(4ft"m%؁Χt99mLwV$(2Hn-/ TH:^?Ϩ(ع=5bhV/|G7>ո?ԋ!J,M ‰IΒROH|Yh.{#7,Lwʯ a?ضOǤɻi+oS{z~<&Ey]j{}sSȟ"w^˟fd3Q?ƯDb Ix"XJlI eZ$?226ױWy@hdmrP<"PyYrhPeÎQ W!9кWݚ+*#b<X&f]Ƶ5_Ma7X\ݯhpޛ* Lt<ŝLG䙼k3rcO M6;;[)F0| - yh[ hynT{d7t' K9Xu%Nr'B*9JST1[0YV.ԧ`53|oԥ4Nש=>J>#r>:߻6pT9Qcd []YtSM:*sKGiW] ¡4;UQ7zk zYlMMM[hp/J-&rT6.fw]EFZ)YMkH9}- ?|Ft^St(bAd}H>&KEUe2 )11J} +JaD JrUtgY5nW9S^R6y$/9hߜ|JnK<6گ~@R>a~N`w̤]$8y(|֎&Zqi]H7Ϙtx.fK x߭MK^Je.fKn/T{*Sůs|<)46J%9.k-kd'ڲת/s)xbwKC]=2trmPycrUR;ZڳIt7XV=ktޥ$$kO3ňҥis\׵#4T?}gF9j_"GYJ;=pUbH%) Zם i@tf.Gk%F݈䛛.:tLPk&uBTΣ(ȱ?*&Q=!,ƃ b IxV&hdı;'b;Z۩nϑX;i=yl ?ޣ-22A@~ܚ5$hxJ}g6"N|"oH ObV%Ty-Hk\B]^jSP*:[3PAU$nSo,EZڝE>toi75)Ms6\?-PY):9eK W8MiSI ~݋7\{>e4Ti/quBTS0&L{k rke"M^ύڨsGoICRn&i/ll^{L%^"eVu]i\|3XTUM9˒:󼗜΅}@aꫩr0swSZ+W/lCZԕYܛouPLbsyH/Ƶ\H''#v"iYV%? -BBZٜ~VFjYwfp2Iֽu`b|]2ٕOtK:EYEyAr-ƍ,3 m¢ ΀b%G9sE[/DtVjqJFr5~VNMXGAALȉ.x!a-ޢe_zCn9awmj>O2k4{4SlLn\˙e)wQyWyu!7296o]oWJe:qqYP\. gnl6- ׋UȱE{?[Y<GfHp?sȝ)εKbHൎFeḐhqjm*} ~Gh\34\AkV]AٯS5jƊY/OkNkM>Djkܿ!Ҭ>8 si/ބ_2o%ؽ'-5^0b- B跅)sS2sN46~!?5 RMj|O5/c"1cZ3EEECsG{{Rn؞ Ć'20A0J<;h(iWU"Q!z[]u$h+ټ"<ܢyj2<7>r>8/M7/Yh1F^Y=Qyɟm!!e hS/BgVZUvY7.DD'urO3#]N8qZZkfMKElH1ؐLS}4u /&eܻ_.*C{{h+s#W|Zyv%e,:v׽zڟ#Y09<@%#HH&!F.S)rO4W,/pIR44fu^W#rvlQclaJjJM{Q=j*浿Dq&‹ <&G!j=jjhn {* Zߺy.6CY$֩}|``a汪֦j"'l6ݳT :߆鋂Paki2\{kJiVOFD=tGyMN⬫T&o6|cpJ_Aeإc EsHYN詻)jl3DX;&uMsD:%hq:jvZErmd+S=]g=23iqSFmV|5˝JC둊lUL?}KKKB0 1Cbd15y# 6\iOt`A]_jjT.2' te舛W4/5qLZՍH5M2d\xxe[ڿF\ga%59jO|Kyˉ<,kO$J*³־gWZ3M\T:^rm-|b}NӮP !.n.Eg@f'Z!0JU^e[vXlŞdӲ_@r zId7/3mQ7yĕ9F/U(gֹ hzyF%%#GU#X<0hJٝsrZB;A蕃^a56UGXi^] -ĵtTZ7WrN}2)u)É?[ʼnJ{iwqJ+MhCy9[GK{cPkYtw/R9m-v({!v\7D\GGT2R S_S~\{^V}{WtI( UBs5[vc.wS(k[ɞfT3\ekNޤkSyJ-5ʵv{^E镙PM1hćO^e9c]&/ DHrw\*7ϟB/ݺ6M&kSy ġt/b`B AzjCoG#bUlJ/U]sZ\KzFߍČ,HQeRݩ3 |B"J|-eVnKawqk}NQ_><~vj0wy+kSyz|7^zGSoytK^r[{ROU$+tZ.esYV.h93EJo5=6P@|>QXWPa ?7lOF\jUꪐ{[c'#ۼR3U`|?VT` w]V"{%P"zj9yQ *\$莃RrjÜCvgݻD&፽TV3UbOBOebEٛw$TEL:k?sfk NFz[a. d | ӅVXפ<) Z4؜neOOYz^f^n&#Ï+Q̉ }6* -q=VZSڹWcuk -Kk~յpSoJEc֧Qq-# uv#w?]M+Zny(mlwQQ)=+t2I뢿Xrk"u[kX3 EɲNQQQ"mۛUvfs 2ظavbj[UeiHmQQHڻ&5l""dQQ>7\e.R(NJUgr["Ⱦy_@k؁zSpѨݕG"Òh*d4w˦8/@5(Fu&BNv*~jDi)oVf[g:S['\΋sRPNu^lg=:^Q˭'lrkQ3a9DD$@yĮ'=%%KRx'i35) ZRz5EsDSX|sUu]vY͞aS۶>܈^͊CҒ%L«Q-Ɉpv󧽭"5=Wx8{eRƠtFIPid!;U9D'.n^ڹMM5X\0oVzr~e˓7~8;*bUQ[}~a5TDMHJk;aMCērԙ%Nڣ#w4i})Er>g%>aEaؑx/zăq\=p6Bwӝ$s%sDc)i/QqwR,eXEogV (ZSY(/Waƴ<ӛK?5s埜 tO*2PXIY8h^z2O!իed[֛ZT9729kޘɂxjs5VfjY*ĉ1:$ZȬG#U')z?@h(W4֖;1M z>_0U6_RsJw^z%\r^fN-Յ!ws(1/vRT<ѽm=k' anFq5ȾH@v[{\"fM)IleFveFQm>UYUlbLw%i5?IM3Wֺ2{5GEcbC{^Ǣ9j抋E/7[UeSOTbM}>a-ϥ/uЖ׺*qC?/jE3 X|Σ&^֗$+)T>h-Pm]#IuZ!&q^FTL"]"ꖝ~Y&)83pEbȋ*fwQ!Z%Myo-yBt*,ԖEr뚅O14JZG'˷otNu #ODӣ.(EGeZ\5SjO8&k4<&ucF8qjk)['rўI]WC/;Wn_md6[U^Qyk65CM Y:ēQV]UE7E#{ˎpC|&pSkֵUIVf&=sQÌEbuTG'3,Ny-ך.*)4/]=|+Vm67e*5K$c_33LuML7GgRIso՜fREVmOﷹiQU'.fJYdssdqF®ۓyTek*%; %jt4v˖‘ 43<&D{QZ*.Ts W rÒd'*\ER]k>U4GpuhΛ\YIw B{Wq[Q,8~e{?sϞ̊ 0ώ,8:8_D^q߂|xo>x<|W oN}!yã/N0}M'9o玦%@`_Fŗ'BG_ a >Or3M'9o珔Js;>3㣋/N0,8}7f?:Or3(}}w}|gG_ a Y|/q/8o>xQ+0ώ,8:8_D^q߂|xo>x<|W aY|/q/8tqe奔-1߂|xD;>:8_D^p}!yS~ [c㩿-1_.w7'Wч|tqeŗ'B󏾦S~ [c\oN}!yã/N0}M'9o玦%@`_Fŗ'BG_ a >Or3M'9o珔Js;>3㣋/N0,8}7f?:Or3(}}w}|gG_ a Y|/q/8o>xQ+0ώ,8:8_D^q߂|xo>x<|W aY|/q/8tqe奔-1߂|xD;>:8_D^p}!yS~ [c㩿-1_.w7'Wч|tqeŗ'B󏾦S~ [c\oN}!yã/N0}M'9o玦%@`_Fŗ'BG_ a >Or3M'9o珔Js;>3㣋/N0,8}7f?:Or3(}}w}|gG_ a Y|/q/8o>xQ+0ώ,8:8_D^q߂|xo>x<|W aY|/q/8tqe奔-1߂|xD;>:8_D^p}!yS~ [c㩿-1_.w7'Wч|tqeŗ'B󏾦S~ [c\oN}!yã/N0}M'9o玦%@`_Fŗ'BG_ a >Or3M'9o珔Js;>3㣋/N0,8}7f?:Or3(}}w}|gG_ a Y|/q/8o>xQ+0ώ,8:8_D^q߂|xo>x<|W aY|/q/8tqe奔-1߂|xD;>:8_D^p}!yS~ [c㩿-1_.w7'Wч|tqeŗ'B󏾦S~ [c\oN}!yã/N0}M'9o玦%@`_Fŗ'BG_ a >Or3M'9o珔Js;>3㣋/N0,8}7f?:Or3(}}w}|gG_ a Y|/q/8o>xQ+0ώ,8:8_D^q߂|xo>x<|W aY|/q/8tqe奔-1߂|xD;>:8_D^p}!yS~ [c㩿-1_.w7'Wч|tqeŗ'B󏾦S~ [c\oN}!yã/N0}M'9o玦%@`_Fŗ'BG_ a >Or3M'9o珔Js;>3㣋/N0,8}7f?:Or3(}}w}|gG_ a Y|/q/8o>xQ+0ώ,8:8_D^q߂|xo>x<|W aY|/q/8tqe奔-1߂|xD;>:8_D^p}!yS~ [c㩿-1_.w7'Wч|tqeŗ'B󏾦S~ [c\oN}!yã/N0}M'9o玦%@`_Fŗ'BG_ a >Or3M'9o珔Js;>3㣋/N0,8}7f?:Or3(}}w}|gG_ a Y|/q/8o>xQ+0ώ,8:8_D^q߂|xo>x<|W aY|/q/8tqe奔-1߂|xD;>:8_D^p}!yS~ [c㩿-1_.w7'Wч|tqeŗ'B󏾦S~ [c\oN}!yã/N0}M'9o玦%@`_Fŗ'BG_ a >Or3M'9o珔Js;>3㣋/N0,8}7f?:Or3(}}w}|gG_ a Y|/q/8o>xQ+0ώ,8:8_D^q߂|xo>x<|W aY|/q/8tqe奔-1߂|xD;>:8_D^p}!yS~ [c㩿-1_.w7'Wч|tqeŗ'B󏾦S~ [c\oN}!yã/N0}M'9o玦%@`_Fŗ'BG_ a >Or3M'9o珔Js;>3㣋/N0,8}7f?:Or3(}}w}|gG_ a Y|/q/8o>xQ+0 &a܎~hMTjOsR*UkĝRj1M!*yjo>ԣЬ s_?Seѵ-iT%Y98pU~j&g7YRz> KZY&Rd YrZ4JeW/\JɮMι ճ*&#ܗ+Tu1UQUW&l$D Z꣗\jG*n*oSQ6'&iyP)a& :Y+CR)+UM\;ho#iiZm,-[[FWzRL9^ysɩ1TJ|TV{UM 1Úٲ6u 9ԳuVꉯTDDM z̥BT\CvEYSb"&ƵI|[goҷ:.|ul7iLX8g?iͤR Mh&cun[=qi鳵{q z_JM%?}ĔֵF1ֵ2DD؈@/A`ךk2R,kf6$V>/뚂CjP&mXԥt}YSkefku{8ޢ蕇c0;[W0|g/[`x^KB[0q')Qct5wɭl r/ECLt9n~j3  ~5Sp 2췺T驊5Щg:iɓ\|dbvX!j^,NwC^w0T,ט k֗ fazK%Eb媑٬ZSd\ݧv5ZU3QDwrjt}ZKgUyI"6:C=#**uc˟_xH1VH-M@H̜XÈss%SysC*~yN' הIrNkJaVm*m)7D2sAfiq+3,5ak놑%]2fF2XQR*跍S.9NRd?r$(V3Ev*mEڅ /No]o9cPb~ۛ]dQ&Fb'5}c,"r`oi4{RX:ə; hjŜ;1YOt&is0S<3 rZYP*frO4{/e6BK)E~]x[^j LMcۺ&{Q{dq'|-Kk@k1ͱ617SjfkÌL1:ͽ3Fd٩8V&^ߡɱZ'R80ӧזo}EP}d56a@GXt<K}jIE{.MsѻQ"mvo׿[}'foR=Q=RrTYNB_""mb*ٸTjX 8ˋIά奞޲. _ZUwڊUO늸29HPި2'[lnwNR3P[KT.SJ]t\jOxOMvd <7)Z9J}ճy$V4R IkkȘ y֕fjSdcL=5OrȎQjU[NWկܝVn]kmo:Iz֎ŭr9y˙fˣֱ_ehlMA|9۪frrʼn? g--mil3 >Z*H0a2ed  Ont?!"ڕx1u@EM\)ǣ=iKsꔴ鈌W5юDR`k"ɗJ]c~n%/q*?q7kv9zn\h0H):)?$f.ErS%4-RtfSyIkZr"&jj(Xi U]b=Gn\{)lcVOM! ::Bj[7j.Qv:TEm/;Guc-zl]Vn=k"=zFhk3*1.;}ӕy2 .֦[5TT]T6Դi// bz1~Ug*>}4sl? NQEQQr\WCo7⼷mepl&5{ :+[ 8rFݢ1Tr'-QlبҠXv.͵%IʲRRLUrUrUUUڦ`:WtGdV+e°XU-(V@G  yݔ{Ҭ^uD=2\mW*'mW,7Q NG2ZmBeBN-KdGd5r"Kβ$31U\ibDO{DsIn:a=wԿTwlt$5#)SZ,5kWUҝ`(?4ϋdm. *\C jmwzo7Ze3_|$ ..Q{zHཞ+g<ގ\>XDcbC{\"9j抋ѳJ_{ا^J֪9jrXN\R,E8o UWLSTwQjg ֈO`N0[ZHP0b2uKO1UfN/2t[hzҬ nsKGּq]-X6*eJ%?jQ\'HdkZ2j̜LƆ,'">P^Oip@#z{oΧ$%N!8|yfY93Mf93̪ z\U Kkċot 5 v+h^¯i%] 0]Ϳ,j2S5SS5O^Zj2ڛzy,Uz >'լiCHlZ߁iU-Ye.xnYtDֆu}| (V*[T)E`Hpt(kVLQwQS|5k0Z&Y!">kg#c5S,cL++y(YmZC򚔕';- b^3U!Eb=jv*wLqeqKjO<\̗&AB`Go9S.WOڵ8]lYZ; .r[DP+<&Y;]wSJQ?32  ^-Y㹩ܓ=Y15W5tNo,ٙ9TnՙZ&ի֨ 6UW+UwQĬWpޕ,KLϫWUD_S|9hbxY&zbU1u2nV[^^sii_>"?ROi\_e7HY-"Hk<:$IH9Dڹ߅:c8m'EhᔒCT0$>dسOϊ㟸i.8E [C'jTۚlG'DC&Ǯ:iv ،I[Gz)>V›XS'1wat;kؐ"DIxlfCr Y蝮-nX^3)xȩH+r.rE]$ 唸.YHIƈ+l Q"5YeLɲ0[{cz.,v`S4ɗR (뎏;Aˤy) wr].q(lg:5RIښA\R۳>l+歹)]mDU]ȉ9?ݗ&2[j>0aƎksYvq ? 2پyoцLF/UGE|vETKQ"Z7lוp=im&oM¹& @oY%5 ֵviת=XFJ*IרsЧ$':Zi4/lX=BGDj]:1W,VG=s+TT׸!J*kS~d: O$VqLCTT׸!JuH=QS^☇+qLC<U/?Xz1WQS^☇+x!8_"EM{b:1W,VCpEaꊚ>)_uEM{bY:\~)d5)|Rxꊚ>)_uYS*kS~5)|Rygs+TT׸!J*kS~d: O$VqLCTT׸!JuH=QS^☇+qLC<U/?Xz1WQS^☇+x!8_"EM{b:1W,VCpEaꊚ>)_uEM{bY:\~)d5)|Rxꊚ>)_uYS*kS~5)|Rygs+TT׸!J*kS~d: O$VqLCTT׸!JuH=QS^☇+qLC<U/?Xz1WQS^☇+x!8_"EM{b:1W,VCpEaꊚ>)_uEM{bY:\~)d5)|Rxꊚ>)_uYS*kS~5)|Rygs+TT׸!J*kS~d: O$VqLCTT׸!JuH=QS^☇+qLC<U/?Xz1WQS^☇+x!8_"EM{b:1W,VCpEaꊚ>)_uEM{bY:\~)d5)|Rxꊚ>)_uYS*kS~5)|Rygs+TT׸!J*kS~d: O$VqLCTT׸!JuH=QS^☇+qLC<U/?Xz1WQS^☇+x!8_"EM{b:1W,VCpEaꊚ>)_uEM{bY:\~)d5)|Rxꊚ>)_uYS*kS~5)|Rygs+TT׸!J*kS~d: O$VqLCTT׸!JuH=QS^☇+qLC<U/?Xz1WQS^☇+x!8_"EM{b:1W,VCpEaꊚ>)_uEM{bY:\~)d5)|Rxꊚ>)_uYS*kS~5)|Rygs+TT׸!J*kS~d: O$VqLCTT׸!JuH=QS^☇+qLC<U/?Xz1WQS^☇+x!8_"EM{b:1W,VCpEaꊚ>)_uEM{bY:\~)d5)|Rxꊚ>)_uYS*kS~5)|Rygs+TT׸!J*kS~d: O$VqLCTT׸!JuH=QS^☇+qLC<U/?Xz1WQS^☇+x!8_"EM{b:1W,VCpEaꊚ>)_uEM{bY:\~)d /cTx8^.s?Y (UúWѴ^y+|KIQMRQ~/wUyej-bD*Djʅq:4ckMrKKWfh͉W#؉ nbQMOGj mp:{f"ZZEYUfl&|7xMx4RbޔO*V xN^Ȏkj*";)UBjmJ/4ƎRY3ɛC|CX=z«Ds{"eU.ȏjXNٖ=QsER]$ƞrLͦ(#JGD]H\nkTUjU[W5碽1e(U[j]nEIxȨ˞[S%\*.pՏm=9UQ//|V#(Xe^^>>] Tr#QPŬL.K[G ڮݔ:4kD{=G#W5vQS$Wz1wͯ S^Z,dGC1S5WGzGuNT8̥bﭪLWq3sc*,69w:krkUڹ$+LEmue*_2?_^ptBgԍr{Kn͹]t}4w%mNq3YDfQ9mrXU]sLn'uu')ű-HPiEB $`5{ekqSKHM6~tktZz8Y1Uu5 Nbr&TF=˖a=7Χul5kɤ&#' uR6#Q\\UUUUڤ+I/zEm?qvk/W'4S3`.nxӛ&emu[Պv$4+!&o?YQ6'.Ia#xeS/*]R=OMmTdR j[ԕ*q^M=5%Ecm7bߴfM)Q%{s]QQUEj.{c^gkSMpQS[l9#W )l!ƙ;qfkE  UV̵U,d&Cc1"C 潪Sb?-Y.KթՉ2mk&=j)EnK{!6[ɄdUךݲdMqnaSABPS)qLJlxmu 2:<~U*H[Iz>mf ?YO*oEئDƔ% 8d,(ˍIDϥNt)Zc&nvH2_.M[ҷ鶵!FH#%- 13۱Ț¥IUmN֕[PYB)$Y%t?] ӡ= ge#"mV9iL6%zY.UkSurW&hra(W4;*e~Ӗ$!98-Oz1 nI]!L&\)??BtE\π)-JGHS[ Dk=MMEClԖkaOjS)uM<{S[P{Qrd*vv|Ut3ܔSt̵+Tu"eW$̷T6*1Nۗ HV^JAlĤԻņبJ.\rT)J!X%!MJMtbYN%:^֣Npn-a3Uo56">W_c2LJwqɱ~.IXqC)+~-uaN!s>$$Ց9}scv]UMߡ(lU8y'筚Kt OyvBvkpH v%yKaL&j5j#6.wMr#*o֝HVM4MqWK k:pq~O1[H_gjP#7'>vƶ"Zj"{[ g[AO{z[6 f*id=vcSkLԇ+#4ٞItlꚳƵz䆋[e 5ZhswbuzSdXNH.YRmODd]rfĹRRФ@… ƦMkZ 3[RRڔ:b|EpgGsXn6kװ l'el/IJHP7EzkD=rLU؉VqRUkIRym6ͥT,tR*lWb¨vpRNrnF\Q}r3s8l ڮ'\ѐazFf1v"uQ.Ia6<%Y1VFr$x]g"9uuW5WnܗrEZ8' ~N%õR[nmn&s5c'ė\-dgmBRϧM_\/Л\v F(AS؊Q{{_Ԝ7oĵ ;(5KËB~몪9Ձ=c!»E]s>a=,bSSH_#"t"b;]ٖlbpЭ>as*W*ވ,lHgVd-Tvj5rȧP;n#|/sח1ʽdv<2my*Ԫmv9D%JKGb>h1{حViOXaT<9Y.]bOٕ8ꪐ&ݹ9^ڬ7Q$RAݻl ԙC*uEq6m\ՏM9dN|%qD^%Ő,ΒKɱڻEM\ܙ&ǡ2O|ҫygC]z?5.?>yr|,@@X. \% nJr*f׵EEڇJ*IK4Ϻu'Jj6ԓ54 Ⱦ"V0z4@TQݍ٫\*m^c.LSek4ZLJKElHQ=3k*ƫJ))9k 4kڻhxi~* =p`Vg7CLѢ;kۆwS'ɺr`qgU/na1$u2ا/'ĺ( uqЮletJ˽,5T_ܩ T SMji<>N@1CG,%HsrOT1rͫ3jdTLiraސIfn@y60wU^ݪֵ3ښLםСIi'P-Kg|1eQtnT<;+#ٵSmO&%ډ v]Sl<ϠyEX7*Ij+$bn=ܵʉ.jkH.k*mM(,h{̥Je&j5UQ35^ :9VF{ڼ0y$</Hw7c>/]>e&ػmҕjW%*1w#KDG"/iɺ*"%Ʒ%NNY5>O*s%%`7^,x8m˒"| ''?sr6H^+2 ǘ~sDr&jB%>Ѳʜ.l'$.x\j.]{Ռ>h!R*kH-Xe6S5QʚS6sXsҼ/Fﯪ|-r~ny/)?ϱ] .n>##:}ώVT"۸Gl٭' V"̓ljk,4Rh^h,yxe A阊!&ԂU\Ѫ]vHLte6O22^ZZaB $kDj"n"!+ni&t!=o>MK޳{7aT?t҇JxQᤜ;+ Dg_ &i13:U3N7-l6ZNel Ԙ%P6ŊV*"cv+Xs Vą7X#VbYrč1ˬG;5۷jy]5}%K֓>_qٖztl6ӧdl}KK ok^Z;ˬ1Wj7?5L""&t-mhQ :E,1Kfwr7oظ\wF8WS &o+W5!(3ܛXUF5뻫ljF;Nj嫽ÔϮT]HiUDU,V5UޞUWbE #dhzSݓ#Wkua$V.q{Z}B/m5wJkڣV ahZ^ZjmFĞ~س?n&kkScP@ eK-@@S;^ҲA̢ċ> <f{7@sScuSuW3j[NTGUziʍTdiM=LCe/qJnYGDMhnQwQM+Z;_N=!Vmv2-Co$W;n#\*^)W5RDdԌ$+74TEEE6.Tm=ں){/ʸ8ֽb?9JihS XqaEj9bJEب~4ɟQRɢ0.G \xe5f*H|yg.jw{g\{of݅ 21rlVz2aFǧiv.*JVDr+\|ط ŭY!PrKv'Ny"ݙ*j]^%% Z/JʲqJ^O7JKK)}"QYTg3CEbC}4r">ZAqڟq5VHVi; sRhW"JNg;b9>=2$G g^V{{Xu]wҦ">5.gQs|45֑l?V ![ZMa\ \w LϤfW/\]*L7J"^G) sp[ V+V;Ա\+*7򼦿^ě+v,S\kS=^B7ݝ~Sҩgr5YYxgq^r"_Ğgj/0ZWu:#I9GCE\&= E.*_*-z^{DvHlM޹MӁ576Ʒ2,sdx>' E&;ۮ >y*z6i˨5Jr#*o]YOA5 .l1Æ?BRa=\3r|*wPs5N\V+t{z^JS&Iͅ s1k= TUj\u$Sl6e8}>͑X8mam Z9*”Z#'ljZry2TVQY :@,vV4kbnn*CƧuwUw6DU> TmKk8o-lɽDkZrHR㶘WqMƌaMw'])MjNHڏ~kLӬG/\qtf,,Ч/jÜ".J&a^k658VNlY]#?-9vdYm7ˆ);cM>M7Zct=tn2Zsꇖ^^(e7 ƃȰdv<}RLe\rf类jZ)ST,"__wI<|?ZԀ<Uj%6fV+'' ѣƈ5DUOR5TDMG}u69l+$ԛd[ UFjHUwq;Ko}dDZJխ-PZ.=/dWhu &1 XLöˣ3&OvK95;nyBTwHO Hh6NOS%Ib20ڍcN⣩Qq0: w/o@=z>B!3KI@yx$(О1]j*.E9{X?VЋD79|UtW[ӯyӗj5Dͪ\T\ԣ zv"ڕ;"KըwKNJGLT"9EG5Ȋ楥UVɣ#q4ڟQM@5+bD{U3G"EM_ ߡ%ʻQ^sm*k: 25Q3#DMv2Ahm {湫9qQw};y.™8Άntx&^^̶;X^[+:OQ+#"Vkq^ \jHߌXo$Md GA^G,֎4p8t[VM닪 F-n~t|痛#{Yp9V} 6hXtĬۋEo.w'DjZVmج&aʉjН+w󗴏oKy2&#zH3k?l3v]ĭ\@˰n7zy"}JVd*~ZrZ'/UE=&[maJbmbNsjK";H"|1Tj:7ckDr׽zjb|btm+qYܯ۶.i!ƗZ_,Oڞ51荝dhzba::H6i&Ҝ.Z_K-q K坭XJ^ȥއcvO dK_4JI-0wGơ1K +Y%';qjԎaG-{Q x34fE1_x8j+A Dӛځ!蚔YjU=%&.WwЪ$f2=t)rEb$ASf`=-ۂvp)޽ĊS-i#xC}tdMHNMܻ\[Y}sR0_i{Z3#^{i.Hq,'z#h]EC/ В/i#+vsϧsT2k{f{j9h̍3Cr9jhQS|ظ~!KTm*[v+8?5K 8Jvԡt ^>S\DWU5Tww-=ե nTk]TtWyhaޙZQТTZXY'a6tmg cܔ2?eMJTYYxsЦ Fj$(G]T]( _}9eVUuI5 꽋#sV^4pkK+]No^_h~T+wٿY_*׭yR:R e{ԭikGJ;"=8$(W:SWbk=YWwZWEyQN#WM5jJ;cBzejgjnXtnQp{ l0zto|6+q<ܹ; acTS΍n6g=O:7ٞamI|n\1*OFb3 l0v7.hXx'{[S΍n6g[`;_4za< yyѽحFb3-/˚= l0zto|6+q<ܹ; acTS΍n6g=O:7ٞamI|n\1*OFb3 l0v7.hXx'{[S΍n6g[`;_4za< yyѽحFb3-/˚= l0zto|6+q<ܹ; acTS΍n6g=O:7ٞamI|n\1*OFb3 l0v7.hXx'{[S΍n6g[`;_4za< yyѽحFb3-/˚= l0zto|6+q<ܹ; acTS΍n6g=O:7ٞamI|n\1*OFb3 l0v7.hXx'{[S΍n6g[`;_4za< yyѽحFb3-/˚= l0zto|6+q<ܹ; acTS΍n6g=O:7ٞamI|n\1*OFb3 l0v7.hXx'{[S΍n6g[`;_4za< yyѽحFb3-/˚= l0zto|6+q<ܹ; acTS΍n6g=O:7ٞamI|n\1*OFb3 l0v7.hXx'{[S΍n6g[`;_4za< yyѽحFb3-/˚= l0zto|6+q<ܹ; acTS΍n6g=O:7ٞamI|n\1*OFb3 l0v7.hXx'{[luQf{yV?Aql$z^X-jyf ;16oϫW?.JOˣ,ER.[=Bi Ek`$Ds5) D⍣[J0bʀzؾ>'w$0c+ح֪U%9?k;(GIQzL堶SQb''lK-prƭ50soOEw;CG6dر6]_U=vyVV4ϟ/+uի 1y"}UDLԭӦa>7EXPtws0xEj".[+ri9;wf!3.3W<g Ë7ڔX2ȉ:/mjX} ˍ֐O{lfJ ~-eӪv+fդ}Ps+Na\_a&i4e&FQ@cȉP,`6šVK5HҪ[/{@FՋKh( o?M*PD|XjTUFF7V#3`7Ey+('EqmYuݗyV֯{Tlb>4gFQSjbZտ/hfhb?DwUf]39.{W5l&QY/ތ0XZ2ًAr_bz*jVCEUՆ:c5!U*糁qe=K5on)=~x Pmfs]ܴzzFivmr}(Qv)< hZb4鋂ƙ{kv^GMEs؉i{.#k69DIHY yI#zU]2+9۷|^Iq?.RڀXKdjyHՉbDcӺؤ`X5XFbZƩu(΋!wu_ li,6X'F^k?aʽW ƶf>ӫ0$*Fch䆗=y։ W$4sWw& 1 q^t&gȐ]Hk=sP1aC cClHoEkFlJW+LT(3{]z5{ҝp`3n3IqFzJ;G'Z^g#@t5V_U$ Y܈U.W8MRCkz5wls*+r+{h,r&JW 5K57 .CF~LUy-`0iV떙[dx{S4EV*f yRIk&4ր9ǽگ{j*䈝!M7G|+tIJ-UC]UQU= kM=!qVSf!ck7;ֵ79n @tH$Kzuk3cvMֽEp48 IYvǏ!ÆW9ʈRo4 UNěsl8vۦ2]dEl66_GzTjXuAZ|5<2M7jе?@Z*dYۮ^ꪩc=([#y-gHv씩aiwO.ZX}m;~]H!Ŋ޹Mޱ ͉6&KZw&ILDb.Ndn[ JF0Kȳ+; TGT|/@C/{!"=c\9rDDUS]Q\z~FFYcFvMNuWyj43¬Zz(,Y?]YZ: WcTTUuDC9} K4WC +[-4t/"䵞:Fl&Ζ+[r^OjB =ȊFWUMtÍ,Y =V4ulZVau(sȮMuXkQW$\aؘ7fXuo( aCL"kE#LrMD6_^U*E_5c6<JOg@x o}fctqbfQCu͈[+-GDUXr Rɣ W4*RY4i9;dI>}bξ=3Jcg,%vdhW&J-ސ.a6iwaU%Me~E暑2뙚7s$vNJ}XߡJecݖHy rc&-UUۨlaB+O+nM:.W ;]>zH+{AGLǁlTEڛ縕&[ RI¢iM=MrVҎҥ*3P[9h8TRYIҒ6[ȪwGK:Z^j$bw6=+[BO'TzuwwMlcBpv9^F^\kV1=O|(ӹs/?j4@s%qGDQ _T]Tjw_2eu3Qb=ߊF"R5XNkp4EbOk'Ȧ?v*J5>lJ8|n//"D%RzXK.i9Fby{jLhf֣kW홴\R{+nBNVҊ|8{/^ڋQ;ZѕFO ݕgʷ'?vV7Et@JWt巓V4,5Xݚe@5ӃHEuou"ʍFFU]]i}_ս&z{wum O2_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋ h O@EZXvosG\>_0T D_ujjV\4{΋  D4͘/q*}0]TtYC:'Ke_{P+:,*~inpss,*}g!Xh%mLڰiP[ STН"/nG\ﭩ-)Tnh#fhqEUo/tm)Ѯ' vQ3l[~C?i&]˕slcqSlkQ\DDLWy QϻذI@R!lD_ƯQc%vt9곖TN*;> N)&r|zDodK-Q?^YzucbէŇU`? WȄ/tD0nV,Y 5Ab)Ȑkv*#;bL"!cDb'ZNϷD7yii8-}k! \6Rn%?"ԟ&'hG8ᶍf}K}4YRFYƷjtiL& q!U_Oe$:jrִ]ʹkG|ZlXQgUT 7Y{clZrXz!aͨmN:1ӊKȲ5GZꤧ'&`v`1bà Dl8pڮ{ܹ#Q7UWyYGѥbF-ZXc&f1QLɯ10ɿk.kc2۫qQZǧ̥Ub>"Kh5"/Z\40>q{ts3ǮOZEIHnS-zzlEVOo;o.>Cw+\o1ڣrW>%NI}VGJ9#lHnt'9kbNe~ȎϮh]6Mӥz|&Pjd1DF2D= U:sl6mZABԒؿz=Uҫ٪-r+Qt I-<7&NcV2S(f3=+eNst?7V4P.[R޿ɹw3]XJiIkhsM$3Ha+ߖys3ED]k&Ӭ_6`T$bu n<9uIuH3J4L=Gk1W,ڹK Z/8?Qҝ.uc8Iy&n kpoESV>.aԾnTJ6] 5? ɪHxp,|LHYks!19=ٞU;YE.'+h/ δ7_Ķϫs2򘕖g;Y9M[[]_mݟnNfY %̃{`!wVKI}k'N/|s#ﲗ~0}Y<q|4t d>|8Z2I}k'N/:ևxL@O_Y<q|4ִ;̇e/4'`xD>h}p.d;)}>5'G@O_3us!K ?I>8:xD>ihw_xiO d>|5'LkC\vRO}&k'N/掁8gZC~0}Y<q|4t d>|8Z2I}k'N/:ևxL@O_Y<q|4ִ;̇e/4'`xD>h}p.d;)}>5'G@O_3us!K ?I>8:xD>ihw_xiO d>|5'LkC\vRO}&k'N/掁8gZC~0}Y<q|4t d>|8Z2I}k'N/:ևxL@O_Y<q|4ִ;̇e/4'`xD>h}p.d;)}>5'G@O_3us!K ?I>8:xD>ihw_xiO d>|5'LkC\vRO}&k'N/掁8gZC~0}Y<q|4t d>|8Z2I}k'N/:ևxL@O_Y<q|4ִ;̇e/4'`xD>h}p.d;)}>5'G@O_3us!K ?I>8:xD>ihw_xiO d>|5'LkC\vRO}&k'N/掁8gZC~0}Y<q|4t d>|8Z2I}k'N/:ևxL@O_Y<q|4ִ;̇e/4'`xD>h}p.d;)}>5'G@O_3us!K ?I>8:xD>ihw_xiO d>|5'LkC\vRO}&k'N/掁8gZC~0}Y<q|4t d>|8Z2I}k'N/:ևxL@O_Y<q|4ִ;̇e/4'`xD>h}p.d;)}>5'G@O_3us!K ?I>8:xD>ihw_xiO d>|5'LkC\vRO}&k'N/掁8gZC~0}Y<q|4t d>|8Z2I}k'N/:ևxL@O_Y<q|4ִ;̇e/4'`xD>h}p.d;)}>5'G@O_3us!K ?I>8:xD>ihw_xiO d>|5'LkC\vRO}&k'N/掁8gZC~0}Y<q|4t d>|8Z2I}k'N/:ևxL#l1[hS柫mQؤ%2[Q[ +m/I)j-Iu2p !QQY$ygRum0MlT.̵:U랿Ƨ\5MJ¥\`eZ bXlsQUuݱS&#-gC3{[د^%%;O+*3r~HC1riv:銔~V(j;֢1=j*$f 48&"Kܸ7;V ܑeyEժO{ EtWl\4yguw4t(}טI#"NYxjv&Jrl,I 1?6K'y\Uv/ˮ^Uհ my{7 I "_HXbDzTDUr䙩aM@iNkZRb˼˂'čQLjՈ_"Ƀ֏EMe%m:W&/o]-EDjjZgLCY"fVs=v&+x.mzu'y>pju3NrcvsDl9US({? sMShzM-##) !KKBl(PX5j"5y2"G^͠7~nEaVW/;;fB:A\8u5[M6.u[W8ȋu1S"RRVBV )yiv60d652kZ؈~JRO6mTiЂJ)EjIjIy'`:ō0W%[0[F2q&2f+SwIP[Ù|XSыg$`"6ݺ%uU]N"U/t]XJ6N DQqUW15#GQQv*).5S'WМWtYgY![CpԇnݲQf+>} fYYgF,bH7HS*q g̣sWe+h\ YF E,d6~/?bDja ҅uMUn)-s®?Q_ƃChd:bF7 <ڑ=bnXp㚟}c,Z~~8ԉ/S{Ǧ*Y͕}Iu{uHG61;չ-ێRELfSF|\FQ/4D*ѩBnXp5`ru<*&/RRFLnT@K6}B%,=`n $rcS[ ښv7\Xer~{հ䋙Y*ͯJQk$ VY$sVu7->fGV'\iohة4KF+uBݖt;lsa/u"O0GVSe;s)~;Q\=^['B_BFw;^)'n,IEL#oɶ9'WqbDT]DDݣX14\ q[q#1U7SU.OIei2p%`(M5DFq bTZ9]ϰ Ѡ5ש?,R УG t Og0rw5t.zsRsœDka!H7ۗVViZXUO gw3SDΘ'i;[Th.jVE #*ֈ+.0Yy*gڝXD!|e:)<&5;>;˚qMs<ꮊ{ XPvCdhN=ayMZ)ͧݢM2hS`$Ou( x-_2+yލ^띬SqAz %;Z#}wT7Bk~uk>t9֦W/GEhOu[WDWG׫|"^aU\l4?K'h\;tHrI~朮:\,nu ľ%uor*R/-SD V`&{o@rzg^&0B_ӐC=Ҋ߽15fT_"(Kqb&p'rjԲQFE4UeTLODcs18jSh}) %3B߂^tAA!,Dr2ϼL-8TPzZp ӜmR^|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R'Dr2ϼtAA!,UN>}9ڥ|R+}N3? mHgOBO f&Lv:;3_B)`m S(Jg?>/Qq]np/5I'>J8ؿS2J<46z> 9S-V?SU'C(zlBωiE_ݦg}-ĬSvmy^}Ni/rh8M~*C]& oBrUɬR*ȿ '0ZhxCeSͮJAT^(Pl(0 L֦HD:'r*+ٔoKK/Tbg,$*J\HVxNn&_M׋łSTDO:LJ㩍Teɒ"Ciټj/T^ҹXpq#JZGhlmxJ,5$ [w&e'ݗڢkĮVWv%e>ZIWYV8zҌabH}07 V K -J4 &P}VE\{K`P(Q'm6Hƿ=g*%\Qz$E=HV& K;cW&y.hwLLMff_ڐG9i:\P=`SZU$5keͻl2jƍﯲ$hl71]T]:*Wu9CZII)&ge.i5U]9oٙ\֤L%y;UMܻGczAMZrUs蛹doj‚`oP*1Q$Vd$W5Uɟ-dpj]G;I|%G#ɛKu7MERuf ɬ]--J_NKA ]Y]6*n@9ro0j!sV' M]Ⱥɬ7wR*I}Yi/:Eu,Mh~^Vj 2#Y="y 5[qrysDT0ۂQhh&ǎ]fTYGäS%鰜lj*e*litestar-2.16.0/docs/index.rst000066400000000000000000000366401500564371300162350ustar00rootroot00000000000000Litestar library documentation ============================== Litestar is a powerful, flexible, highly performant, and opinionated ASGI framework. The Litestar framework supports :doc:`/usage/plugins/index`, ships with :doc:`dependency injection `, :doc:`security primitives `, :doc:`OpenAPI schema generation `, `MessagePack `_, :doc:`middlewares `, a great :doc:`CLI ` experience, and much more. Installation ------------ .. code-block:: shell pip install litestar .. tip:: ``litestar[standard]`` includes commonly used extras like ``uvicorn`` and ``jinja2`` (for templating). .. dropdown:: Extras :icon: star `Pydantic `_ :code:`pip install litestar[pydantic]` `Attrs `_ :code:`pip install litestar[attrs]` :ref:`Brotli Compression Middleware `: :code:`pip install litestar[brotli]` :ref:`Cookie Based Sessions ` :code:`pip install litestar[cryptography]` :doc:`JWT ` :code:`pip install litestar[jwt]` :doc:`RedisStore ` :code:`pip install litestar[redis]` :ref:`Picologging ` :code:`pip install litestar[picologging]` :ref:`StructLog ` :code:`pip install litestar[structlog]` :doc:`Prometheus Instrumentation ` :code:`pip install litestar[prometheus]` :doc:`Open Telemetry Instrumentation ` :code:`pip install litestar[opentelemetry]` :doc:`SQLAlchemy ` :code:`pip install litestar[sqlalchemy]` :doc:`CLI ` .. deprecated:: 2.1.1 The ``litestar`` base installation now includes the CLI dependencies and this group is no longer required to use the CLI. If you need the optional CLI dependencies, install the ``standard`` group instead. **Will be removed in 3.0** :code:`pip install litestar[cli]` :doc:`Jinja Templating ` :code:`pip install litestar[jinja]` :doc:`Mako Templating ` :code:`pip install litestar[mako]` Standard Installation (includes Uvicorn and Jinja2 templating): :code:`pip install litestar[standard]` All Extras: :code:`pip install litestar[full]` .. note:: The full extras is not recommended because it will add a lot of unnecessary extras. .. _minimal_example: Minimal Example --------------- At a minimum, make sure you have installed ``litestar[standard]``, which includes uvicorn. First, create a file named ``app.py`` with the following contents: .. code-block:: python from litestar import Litestar, get @get("/") async def index() -> str: return "Hello, world!" @get("/books/{book_id:int}") async def get_book(book_id: int) -> dict[str, int]: return {"book_id": book_id} app = Litestar([index, get_book]) Then, run the following command: .. code-block:: shell litestar run # Or you can run Uvicorn directly: uvicorn app:app --reload You can now visit ``http://localhost:8000/`` and ``http://localhost:8000/books/1`` in your browser and you should see the responses of your two endpoints: .. code-block:: text "Hello, world!" and .. code-block:: json {"book_id": 1} .. tip:: You can also check out the automatically generated OpenAPI-based documentation at: * ``http://localhost:8000/schema`` (for `ReDoc `_), * ``http://localhost:8000/schema/swagger`` (for `Swagger UI `_), * ``http://localhost:8000/schema/elements`` (for `Stoplight Elements `_) * ``http://localhost:8000/schema/rapidoc`` (for `RapiDoc `_) You can check out a more in-depth tutorial in the :doc:`/tutorials/todo-app/index` section! Sponsors -------- Litestar is a community-driven open-source initiative that thrives on the generous contributions of our sponsors, enabling us to pursue innovative developments. A huge thank you to our current sponsors: .. raw:: html

Scalar.com

Scalar.com

Telemetry Sports

Telemetry Sports

We invite organizations and individuals to join our sponsorship program. By becoming a sponsor on `Polar `_ (preferred), or other platforms like `GitHub `_ and `Open Collective `_, you can play a pivotal role in our project's growth. Also, exclusively with `Polar `_, you can engage in pledge-based sponsorships. .. _sponsor-github: https://github.com/sponsors/litestar-org .. _sponsor-oc: https://opencollective.com/litestar .. _sponsor-polar: https://polar.sh/litestar-org Expanded Example ---------------- **Define your data model** using pydantic or any library based on it (for example ormar, beanie, SQLModel): .. code-block:: python from pydantic import BaseModel, UUID4 class User(BaseModel): first_name: str last_name: str id: UUID4 You can also use dataclasses (standard library and Pydantic), :class:`typing.TypedDict`, or :class:`msgspec.Struct`. .. code-block:: python from uuid import UUID from dataclasses import dataclass from litestar.dto import DTOConfig, DataclassDTO @dataclass class User: first_name: str last_name: str id: UUID class PartialUserDTO(DataclassDTO[User]): config = DTOConfig(exclude={"id"}, partial=True) **Define a Controller for your data model:** .. code-block:: python from typing import List from litestar import Controller, get, post, put, patch, delete from litestar.dto import DTOData from pydantic import UUID4 from my_app.models import User, PartialUserDTO class UserController(Controller): path = "/users" @post() async def create_user(self, data: User) -> User: ... @get() async def list_users(self) -> List[User]: ... @patch(path="/{user_id:uuid}", dto=PartialUserDTO) async def partial_update_user( self, user_id: UUID4, data: DTOData[User] ) -> User: ... @put(path="/{user_id:uuid}") async def update_user(self, user_id: UUID4, data: User) -> User: ... @get(path="/{user_id:uuid}") async def get_user(self, user_id: UUID4) -> User: ... @delete(path="/{user_id:uuid}") async def delete_user(self, user_id: UUID4) -> None: ... When instantiating your app, import your *controller* into your application's entry-point and pass it to Litestar: .. code-block:: python from litestar import Litestar from my_app.controllers.user import UserController app = Litestar(route_handlers=[UserController]) To **run your application**, use an ASGI server such as `uvicorn `_ : .. code-block:: shell uvicorn my_app.main:app --reload Philosophy ---------- - Litestar is a community-driven project. This means not a single author, but rather a core team of maintainers is leading the project, supported by a community of contributors. Litestar currently has 5 maintainers and is being very actively developed. - Litestar draws inspiration from `NestJS `_ - a contemporary TypeScript framework - which places opinions and patterns at its core. - While still allowing for **function-based endpoints**, Litestar seeks to build on Python's powerful and versatile OOP, by placing **class-based controllers** at its core. - Litestar is **not** a microframework. Unlike frameworks such as FastAPI, Starlette, or Flask, Litestar includes a lot of functionalities out of the box needed for a typical modern web application, such as ORM integration, client- and server-side sessions, caching, OpenTelemetry integration, and many more. It's not aiming to be "the next Django" (for example, it will never feature its own ORM), but its scope is not micro either. Feature comparison with similar frameworks ------------------------------------------ +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | | Litestar | FastAPI | Starlette | Sanic | Quart | +=============================+====================================+=====================+==================+=====================+=====================+ | OpenAPI | :octicon:`check` | :octicon:`check` | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | Automatic API documentation | Swagger, ReDoc, Stoplight Elements | Swagger, ReDoc | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | Data validation | :octicon:`check` | :octicon:`check` | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | Dependency Injection | :octicon:`check` | :octicon:`check` | :octicon:`dash` | :octicon:`check` | :octicon:`dash` | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | Class based routing | :octicon:`check` | (Through extension) | :octicon:`check` | :octicon:`check` | :octicon:`check` | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | ORM integration | SQLAlchemy, Tortoise, Piccolo | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | (Through extension) | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | Templating | Jinja, Mako | Jinja | Jinja | Jinja | Jinja | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | MessagePack | :octicon:`check` | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | CORS | :octicon:`check` | :octicon:`check` | :octicon:`check` | :octicon:`check` | (Through extension) | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | CSRF | :octicon:`check` | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | Rate-limiting | :octicon:`check` | :octicon:`dash` | :octicon:`dash` | (Through extension) | :octicon:`dash` | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | JWT | :octicon:`check` | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | Sessions | :octicon:`check` | Client-side | Client-side | :octicon:`dash` | Client-side | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | Authentication | JWT / Session based | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ | Caching | :octicon:`check` | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | :octicon:`dash` | +-----------------------------+------------------------------------+---------------------+------------------+---------------------+---------------------+ Example Applications -------------------- * `litestar-fullstack `_ : A fully-capable, production-ready fullstack Litestar web application configured with best practices. It includes SQLAlchemy 2.0, VueJS, `Vite `_, `SAQ job queue `_, ``Jinja`` templates and more. `Read more `_. Like all Litestar projects, this application is open to contributions, big and small. * `litestar-fullstack-inertia `_ : Similar to `Litestar Fullstack `_ but uses `Inertia.js `_. * `litestar-hello-world `_: A bare-minimum application setup. Great for testing and POC work. .. toctree:: :titlesonly: :caption: Documentation :hidden: usage/index reference/index benchmarks .. toctree:: :titlesonly: :caption: Guides :hidden: migration/index topics/index tutorials/index .. toctree:: :titlesonly: :caption: Contributing :hidden: contribution-guide Available Issues Code of Conduct litestar-2.16.0/docs/migration/000077500000000000000000000000001500564371300163545ustar00rootroot00000000000000litestar-2.16.0/docs/migration/fastapi.rst000066400000000000000000000361331500564371300205430ustar00rootroot00000000000000From Starlette / FastAPI ------------------------ Routing Decorators ~~~~~~~~~~~~~~~~~~ Litestar does not include any decorator as part of the ``Router`` or ``Litestar`` instances. Instead, all routes are declared using :doc:`route handlers `, either as standalone functions or controller methods. The handler can then be registered on an application or router instance. .. tab-set:: .. tab-item:: FastAPI :sync: fastapi .. code-block:: python from fastapi import FastAPI app = FastAPI() @app.get("/") async def index() -> dict[str, str]: ... .. tab-item:: Starlette :sync: starlette .. code-block:: python from starlette.applications import Starlette from starlette.routing import Route async def index(request): ... routes = [Route("/", endpoint=index)] app = Starlette(routes=routes) .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, get @get("/") async def index() -> dict[str, str]: ... app = Litestar([index]) .. seealso:: To learn more about registering routes, check out this chapter in the documentation: * :ref:`Routing - Registering Routes ` Routers and Routes ~~~~~~~~~~~~~~~~~~ There are a few key differences between Litestar’s and Starlette’s ``Router`` class: 1. The Litestar version is not an ASGI app 2. The Litestar version does not include decorators: Use :doc:`route handlers `. 3. The Litestar version does not support lifecycle hooks: Those have to be handled on the application layer. See :doc:`lifecycle hooks ` If you are using Starlette’s ``Route``\ s, you will need to replace these with :doc:`route handlers `. Host based routing ~~~~~~~~~~~~~~~~~~ Host based routing class is intentionally unsupported. If your application relies on ``Host`` you will have to separate the logic into different services and handle this part of request dispatching with a proxy server like `nginx `_ or `traefik `_. Dependency Injection ~~~~~~~~~~~~~~~~~~~~ The Litestar dependency injection system is different from the one used by FastAPI. You can read about it in the :doc:`dependency injection ` section of the documentation. In FastAPI you declare dependencies either as a list of functions passed to the ``Router`` or ``FastAPI`` instances, or as a default function argument value wrapped in an instance of the ``Depends`` class. In Litestar **dependencies are always declared using a dictionary** with a string key and the value wrapped in an instance of the ``Provide`` class. This also allows to transparently override dependencies on every level of the application, and to easily access dependencies from higher levels. .. tab-set:: .. tab-item:: FastAPI :sync: fastapi .. code-block:: python from fastapi import FastAPI, Depends, APIRouter async def route_dependency() -> bool: ... async def nested_dependency() -> str: ... async def router_dependency() -> int: ... async def app_dependency(data: str = Depends(nested_dependency)) -> int: ... router = APIRouter(dependencies=[Depends(router_dependency)]) app = FastAPI(dependencies=[Depends(nested_dependency)]) app.include_router(router) @app.get("/") async def handler( val_route: bool = Depends(route_dependency), val_router: int = Depends(router_dependency), val_nested: str = Depends(nested_dependency), val_app: int = Depends(app_dependency), ) -> None: ... .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, Provide, get, Router async def route_dependency() -> bool: ... async def nested_dependency() -> str: ... async def router_dependency() -> int: ... async def app_dependency(nested: str) -> int: ... @get("/", dependencies={"val_route": Provide(route_dependency)}) async def handler( val_route: bool, val_router: int, val_nested: str, val_app: int ) -> None: ... router = Router(dependencies={"val_router": Provide(router_dependency)}) app = Litestar( route_handlers=[handler], dependencies={ "val_app": Provide(app_dependency), "val_nested": Provide(nested_dependency), }, ) .. seealso:: To learn more about dependency injection, check out this chapter in the documentation: * :doc:`/usage/dependency-injection` Lifespan ~~~~~~~~ If you're using an async context manager and pass parameters to it, most likely the order of parameters is inversed between FastAPI and Litestar. .. tab-set:: .. tab-item:: FastAPI :sync: fastapi .. code-block:: python @asynccontextmanager async def lifespan( _app: FastAPI, app_settings: AppSettings, ): # Setup code here yield # Teardown code here .. tab-item:: Litestar :sync: litestar .. code-block:: python @asynccontextmanager async def lifespan( app_settings: AppSettings, _app: Litestar, ): # Setup code here yield # Teardown code here Cookies ~~~~~~~ While with FastAPI you usually set cookies on the response ``Response`` object, in Litestar there are two options: At the decorator level, using the ``response_cookies`` keyword argument, or dynamically at the response level (see: :ref:`Setting Cookies dynamically `) .. tab-set:: .. tab-item:: FastAPI :sync: fastapi .. code-block:: python @app.get("/") async def index(response: Response) -> dict[str, str]: response.set_cookie(key="my_cookie", value="cookie_value") ... .. tab-item:: Litestar :sync: litestar .. code-block:: python @get(response_cookies={"my-cookie": "cookie-value"}) async def handler() -> str: ... Dependencies parameters ~~~~~~~~~~~~~~~~~~~~~~~ The way dependencies parameters are passed differs between FastAPI and Litestar, note the `state: State` parameter in the Litestar example. You can get the state either with the state kwarg in the handler or ``request.state`` (which point to the same object, a request local state, inherited from the application's state), or via `request.app.state`, the application's state. .. tab-set:: .. tab-item:: FastAPI :sync: fastapi .. code-block:: python from fastapi import Request async def get_arqredis(request: Request) -> ArqRedis: return request.state.arqredis .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import State async def get_arqredis(state: State) -> ArqRedis: return state.arqredis Post json ~~~~~~~~~ In FastAPI, you pass the JSON object directly as a parameter to the endpoint, which will then be validated by Pydantic. In Litestar, you use the `data` keyword argument. The data will be parsed and validated by the associated modelling library. .. tab-set:: .. tab-item:: FastAPI :sync: fastapi .. code-block:: python class ObjectType(BaseModel): name: str @app.post("/items/") async def create_item(object_name: ObjectType) -> dict[str, str]: return {"name": object_name.name} .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, post from pydantic import BaseModel class ObjectType(BaseModel): name: str @post("/items/") async def create_item(data: ObjectType) -> dict[str, str]: return {"name": data.name} Default status codes ~~~~~~~~~~~~~~~~~~~~ Post defaults to 200 in FastApi and 201 in Litestar. Templates ~~~~~~~~~ In FastAPI, you use `TemplateResponse` to render templates. In Litestar, you use the `Template` class. Also FastAPI let you pass a dictionary while in Litestar you need to explicitly pass the context kwarg. .. tab-set:: .. tab-item:: FastAPI :sync: fastapi .. code-block:: python @app.get("/uploads") async def get_uploads(request: Request): return templates.TemplateResponse( "uploads.html", {"request": request, "debug": app.state.debug} ) .. tab-item:: Litestar :sync: litestar .. code-block:: python @get("/uploads") async def get_uploads(app_settings) -> Template: return Template( name="uploads.html", context={"debug": app_settings.debug} ) Default handler names ~~~~~~~~~~~~~~~~~~~~~~~ In FastAPI, the handler name defaults to the local name of the function. In Litestar, you need to explicitly declare the `name` parameter in the route decorator. This is important when using e.g. `url_for`. .. tab-set:: .. tab-item:: FastAPI :sync: fastapi .. code-block:: python @app.get("/blabla") async def blabla() -> str: return "Blabla" .. code-block:: html Blabla .. tab-item:: Litestar :sync: litestar .. code-block:: python @get(path="/blabla", name="blabla") async def blabla() -> str: return "Blabla" .. code-block:: html Blabla Uploads ~~~~~~~ In FastAPI, you use the `File` class to handle file uploads. In Litestar, you use the `data` keyword argument with `Body` and specify the `media_type` as `RequestEncodingType.MULTI_PART`. While this is more verbose, it's also more explicit and communicates the intent more clearly. .. tab-set:: .. tab-item:: FastAPI :sync: fastapi .. code-block:: python @app.post("/upload/") async def upload_file(files: list[UploadFile] = File(...)) -> dict[str, str]: return {"file_names": [file.filename for file in files]} .. tab-item:: Litestar :sync: litestar .. code-block:: python @post("/upload/") async def upload_file(data: Annotated[list[UploadFile], Body(media_type=RequestEncodingType.MULTI_PART)]) -> dict[str, str]: return {"file_names": [file.filename for file in data]} app = Litestar([upload_file]) Exceptions signature ~~~~~~~~~~~~~~~~~~~~ In FastAPI, status code and exception details can be passed to `HTTPException` as positional arguments, while in Litestar they are set with keywords arguments, e.g. `status_code`. Positional arguments to `HTTPException` in Litestar will be added to the exception detail. If migrating you just change your HTTPException import this will break. .. tab-set:: .. tab-item:: FastAPI :sync: fastapi .. code-block:: python from fastapi import FastAPI, HTTPException app = FastAPI() @app.get("/") async def index() -> None: response_fields = {"array": "value"} raise HTTPException( 400, detail=f"can't get that field: {response_fields.get('array')}" ) .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, get from litestar.exceptions import HTTPException @get("/") async def index() -> None: response_fields = {"array": "value"} raise HTTPException( status_code=400, detail=f"can't get that field: {response_fields.get('array')}" ) app = Litestar([index]) Authentication ~~~~~~~~~~~~~~ FastAPI promotes a pattern of using dependency injection for authentication. You can do the same in Litestar, but the preferred way of handling this is extending :doc:`/usage/security/abstract-authentication-middleware`. .. tab-set:: .. tab-item:: FastAPI :sync: fastapi .. code-block:: python from fastapi import FastAPI, Depends, Request async def authenticate(request: Request) -> None: ... app = FastAPI() @app.get("/", dependencies=[Depends(authenticate)]) async def index() -> dict[str, str]: ... .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, get, ASGIConnection, BaseRouteHandler async def authenticate( connection: ASGIConnection, route_handler: BaseRouteHandler ) -> None: ... @get("/", guards=[authenticate]) async def index() -> dict[str, str]: ... .. seealso:: To learn more about security and authentication, check out this chapter in the documentation: * :doc:`/usage/security/index` Dependency overrides ~~~~~~~~~~~~~~~~~~~~ While FastAPI includes a mechanism to override dependencies on an existing application object, Litestar promotes architectural solutions to the issue this is aimed to solve. Therefore, overriding dependencies in Litestar is strictly supported at definition time, i.e. when you’re defining handlers, controllers, routers, and applications. Dependency overrides are fundamentally the same idea as mocking and should be approached with the same caution and used sparingly instead of being the default. To achieve the same effect there are three general approaches: 1. Structuring the application with different environments in mind. This could mean for example connecting to a different database depending on the environment, which in turn is set via and env-variable. This is sufficient and most cases and designing your application around this principle is a general good practice since it facilitates configurability and integration-testing capabilities 2. Isolating tests for unit testing and using ``create_test_client`` 3. Resort to mocking if none of the above approaches can be made to work Middleware ~~~~~~~~~~ Pure ASGI middleware is fully compatible, and can be used with any ASGI framework. Middlewares that make use of FastAPI/Starlette specific middleware features such as Starlette’s `BaseHTTPMiddleware `_ are not compatible, but can be easily replaced by :doc:`Creating Middlewares `. litestar-2.16.0/docs/migration/flask.rst000066400000000000000000000601741500564371300202160ustar00rootroot00000000000000From Flask ---------- ASGI vs WSGI ~~~~~~~~~~~~ `Flask `_ is a WSGI framework, whereas Litestar is built using the modern `ASGI `_ standard. A key difference is that *ASGI* is built with async in mind. While Flask has added support for ``async/await``, it remains synchronous at its core; The async support in Flask is limited to individual endpoints. What this means is that while you can use ``async def`` to define endpoints in Flask, **they will not run concurrently** - requests will still be processed one at a time. Flask handles asynchronous endpoints by creating an event loop for each request, run the endpoint function in it, and then return its result. ASGI on the other hand does the exact opposite; It runs everything in a central event loop. Litestar then adds support for synchronous functions by running them in a non-blocking way *on the event loop*. What this means is that synchronous and asynchronous code both run concurrently. Routing ~~~~~~~ .. tab-set:: .. tab-item:: Flask :sync: flask .. code-block:: python from flask import Flask app = Flask(__name__) @app.route("/") def index(): return "Index Page" @app.route("/hello") def hello(): return "Hello, World" .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, get @get("/") def index() -> str: return "Index Page" @get("/hello") def hello() -> str: return "Hello, World" app = Litestar([index, hello]) Path parameters ^^^^^^^^^^^^^^^ .. tab-set:: .. tab-item:: Flask :sync: flask .. code-block:: python from flask import Flask app = Flask(__name__) @app.route("/user/") def show_user_profile(username): return f"User {username}" @app.route("/post/") def show_post(post_id): return f"Post {post_id}" @app.route("/path/") def show_subpath(subpath): return f"Subpath {subpath}" .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, get from pathlib import Path @get("/user/{username:str}") def show_user_profile(username: str) -> str: return f"User {username}" @get("/post/{post_id:int}") def show_post(post_id: int) -> str: return f"Post {post_id}" @get("/path/{subpath:path}") def show_subpath(subpath: Path) -> str: return f"Subpath {subpath}" app = Litestar([show_user_profile, show_post, show_subpath]) .. seealso:: To learn more about path parameters, check out this chapter in the documentation: * :doc:`/usage/routing/parameters` Request object ~~~~~~~~~~~~~~ In Flask, the current request can be accessed through a global ``request`` variable. In Litestar, the request can be accessed through an optional parameter in the handler function. .. tab-set:: .. tab-item:: Flask :sync: flask .. code-block:: python from flask import Flask, request app = Flask(__name__) @app.get("/") def index(): print(request.method) .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, get, Request @get("/") def index(request: Request) -> None: print(request.method) Request methods ^^^^^^^^^^^^^^^ +---------------------------------+-------------------------------------------------------------------------------------------------------+ | Flask | Litestar | +=================================+=======================================================================================================+ | ``request.args`` | ``request.query_params`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.base_url`` | ``request.base_url`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.authorization`` | ``request.auth`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.cache_control`` | ``request.headers.get("cache-control")`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.content_encoding`` | ``request.headers.get("content-encoding")`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.content_length`` | ``request.headers.get("content-length")`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.content_md5`` | :octicon:`dash` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.content_type`` | ``request.content_type`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.cookies`` | ``request.cookies`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.data`` | ``request.body()`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.date`` | ``request.headers.get("date")`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.endpoint`` | ``request.route_handler`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.environ`` | ``request.scope`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.files`` | Use ``UploadFile`` see in :doc:`/usage/requests` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.form`` | ``request.form()``, prefer ``Body`` see in :doc:`/usage/requests` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.get_json`` | ``request.json()``, prefer the ``data`` keyword argument, see in :doc:`/usage/requests` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.headers`` | ``request.headers`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.host`` | :octicon:`dash` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.host_url`` | :octicon:`dash` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.if_match`` | ``request.headers.get("if-match")`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.if_modified_since`` | ``request.headers.get("if_modified_since")`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.if_none_match`` | ``request.headers.get("if_none_match")`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.if_range`` | ``request.headers.get("if_range")`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.if_unmodified_since`` | ``request.headers.get("if_unmodified_since")`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.method`` | ``request.method`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.mimetype`` | :octicon:`dash` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.mimetype_params`` | :octicon:`dash` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.origin`` | :octicon:`dash` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.path`` | ``request.scope["path"]`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.query_string`` | ``request.scope["query_string"]`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.range`` | ``request.headers.get("range")`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.referrer`` | ``request.headers.get("referrer")`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.remote_addr`` | :octicon:`dash` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.remote_user`` | :octicon:`dash` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.root_path`` | ``request.scope["root_path"]`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.server`` | ``request.scope["server"]`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.stream`` | ``request.stream`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.url`` | ``request.url`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.url_charset`` | :octicon:`dash` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.user_agent`` | ``request.headers.get("user-agent")`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ | ``request.user_agent`` | ``request.headers.get("user-agent")`` | +---------------------------------+-------------------------------------------------------------------------------------------------------+ .. seealso:: To learn more about requests, check out these chapters in the documentation * :doc:`/usage/requests` * :doc:`/reference/connection` Static files ~~~~~~~~~~~~ Like Flask, Litestar also has capabilities for serving static files, but while Flask will automatically serve files from a ``static`` folder, this has to be configured explicitly in Litestar. .. code-block:: python from litestar import Litestar from litestar.static_files import create_static_files_router app = Litestar(route_handlers=[ create_static_files_router(path="/static", directories=["assets"]), ]) .. seealso:: To learn more about static files, check out this chapter in the documentation * :doc:`/usage/static-files` Templates ~~~~~~~~~ Flask comes with the `Jinja `_ templating engine built-in. You can use Jinja with Litestar as well, but you’ll need to install it explicitly. You can do by installing Litestar with ``pip install litestar[jinja]``. In addition to Jinja, Litestar supports `Mako `_ and `Minijinja `_ templates as well. .. tab-set:: .. tab-item:: Flask :sync: flask .. code-block:: python from flask import Flask, render_template app = Flask(__name__) @app.route("/hello/") def hello(name): return render_template("hello.html", name=name) .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, get from litestar.contrib.jinja import JinjaTemplateEngine from litestar.response import Template from litestar.template.config import TemplateConfig @get("/hello/{name:str}") def hello(name: str) -> Template: return Template(response_name="hello.html", context={"name": name}) app = Litestar( [hello], template_config=TemplateConfig(directory="templates", engine=JinjaTemplateEngine), ) .. seealso:: To learn more about templates, check out this chapter in the documentation: * :doc:`/usage/templating` Setting cookies and headers ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. tab-set:: .. tab-item:: Flask :sync: flask .. code-block:: python from flask import Flask, make_response app = Flask(__name__) @app.get("/") def index(): response = make_response("hello") response.set_cookie("my-cookie", "cookie-value") response.headers["my-header"] = "header-value" return response .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, get, Response from litestar.datastructures import ResponseHeader, Cookie @get( "/static", response_headers={"my-header": ResponseHeader(value="header-value")}, response_cookies=[Cookie("my-cookie", "cookie-value")], ) def static() -> str: # you can set headers and cookies when defining handlers ... @get("/dynamic") def dynamic() -> Response[str]: # or dynamically, by returning an instance of Response return Response( "hello", headers={"my-header": "header-value"}, cookies=[Cookie("my-cookie", "cookie-value")], ) .. seealso:: To learn more about response headers and cookies, check out these chapters in the documentation: - :ref:`Responses - Setting Response Headers ` - :ref:`Responses - Setting Response Cookies ` Redirects ~~~~~~~~~ For redirects, instead of ``redirect`` use ``Redirect``: .. tab-set:: .. tab-item:: Flask :sync: flask .. code-block:: python from flask import Flask, redirect, url_for app = Flask(__name__) @app.get("/") def index(): return "hello" @app.get("/hello") def hello(): return redirect(url_for("index")) .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, get from litestar.response import Redirect @get("/") def index() -> str: return "hello" @get("/hello") def hello() -> Redirect: return Redirect(path="/") app = Litestar([index, hello]) Raising HTTP errors ~~~~~~~~~~~~~~~~~~~ Instead of using the ``abort`` function, raise an ``HTTPException``: .. tab-set:: .. tab-item:: Flask :sync: flask .. code-block:: python from flask import Flask, abort app = Flask(__name__) @app.get("/") def index(): abort(400, "this did not work") .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, get from litestar.exceptions import HTTPException @get("/") def index() -> None: raise HTTPException(status_code=400, detail="this did not work") app = Litestar([index]) .. seealso:: To learn more about exceptions, check out this chapter in the documentation: * :doc:`/usage/exceptions` Setting status codes ~~~~~~~~~~~~~~~~~~~~ .. tab-set:: .. tab-item:: Flask :sync: flask .. code-block:: python from flask import Flask app = Flask(__name__) @app.get("/") def index(): return "not found", 404 .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, get, Response @get("/static", status_code=404) def static_status() -> str: return "not found" @get("/dynamic") def dynamic_status() -> Response[str]: return Response("not found", status_code=404) app = Litestar([static_status, dynamic_status]) Serialization ~~~~~~~~~~~~~ Flask uses a mix of explicit conversion (such as ``jsonify``) and inference (i.e. the type of the returned data) to determine how data should be serialized. Litestar instead assumes the data returned is intended to be serialized into JSON and will do so unless told otherwise. .. tab-set:: .. tab-item:: Flask :sync: flask .. code-block:: python from flask import Flask, Response app = Flask(__name__) @app.get("/json") def get_json(): return {"hello": "world"} @app.get("/text") def get_text(): return "hello, world!" @app.get("/html") def get_html(): return Response("hello, world", mimetype="text/html") .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, get, MediaType @get("/json") def get_json() -> dict[str, str]: return {"hello": "world"} @get("/text", media_type=MediaType.TEXT) def get_text() -> str: return "hello, world" @get("/html", media_type=MediaType.HTML) def get_html() -> str: return "hello, world" app = Litestar([get_json, get_text, get_html]) Error handling ~~~~~~~~~~~~~~ .. tab-set:: .. tab-item:: Flask :sync: flask .. code-block:: python from flask import Flask from werkzeug.exceptions import HTTPException app = Flask(__name__) @app.errorhandler(HTTPException) def handle_exception(e): ... .. tab-item:: Litestar :sync: litestar .. code-block:: python from litestar import Litestar, Request, Response from litestar.exceptions import HTTPException def handle_exception(request: Request, exception: Exception) -> Response: ... app = Litestar([], exception_handlers={HTTPException: handle_exception}) .. seealso:: To learn more about exception handling, check out this chapter in the documentation: * :ref:`usage/exceptions:exception handling` litestar-2.16.0/docs/migration/index.rst000066400000000000000000000007161500564371300202210ustar00rootroot00000000000000Migrating to Litestar ===================== Migrating from `Starlette `_ or `FastAPI `_ to Litestar is straightforward, as they are both ASGI frameworks and as such build on the same fundamental principles. The following sections can help to navigate a migration from either framework by introducing Litestar-equivalents to common functionalities. .. toctree:: :titlesonly: flask fastapi litestar-2.16.0/docs/reference/000077500000000000000000000000001500564371300163215ustar00rootroot00000000000000litestar-2.16.0/docs/reference/app.rst000066400000000000000000000000641500564371300176330ustar00rootroot00000000000000app === .. automodule:: litestar.app :members: litestar-2.16.0/docs/reference/background_tasks.rst000066400000000000000000000001331500564371300223740ustar00rootroot00000000000000background_tasks ================ .. automodule:: litestar.background_tasks :members: litestar-2.16.0/docs/reference/channels/000077500000000000000000000000001500564371300201145ustar00rootroot00000000000000litestar-2.16.0/docs/reference/channels/backends/000077500000000000000000000000001500564371300216665ustar00rootroot00000000000000litestar-2.16.0/docs/reference/channels/backends/asyncpg.rst000066400000000000000000000001221500564371300240570ustar00rootroot00000000000000asyncpg ======= .. automodule:: litestar.channels.backends.asyncpg :members: litestar-2.16.0/docs/reference/channels/backends/base.rst000066400000000000000000000001121500564371300233240ustar00rootroot00000000000000base ===== .. automodule:: litestar.channels.backends.base :members: litestar-2.16.0/docs/reference/channels/backends/index.rst000066400000000000000000000001271500564371300235270ustar00rootroot00000000000000backends ======== .. toctree:: base memory redis psycopg asyncpg litestar-2.16.0/docs/reference/channels/backends/memory.rst000066400000000000000000000001171500564371300237270ustar00rootroot00000000000000memory ====== .. automodule:: litestar.channels.backends.memory :members: litestar-2.16.0/docs/reference/channels/backends/psycopg.rst000066400000000000000000000001221500564371300240770ustar00rootroot00000000000000psycopg ======= .. automodule:: litestar.channels.backends.psycopg :members: litestar-2.16.0/docs/reference/channels/backends/redis.rst000066400000000000000000000001141500564371300235220ustar00rootroot00000000000000redis ===== .. automodule:: litestar.channels.backends.redis :members: litestar-2.16.0/docs/reference/channels/index.rst000066400000000000000000000001161500564371300217530ustar00rootroot00000000000000channels ======== .. toctree:: plugin subscriber backends/index litestar-2.16.0/docs/reference/channels/plugin.rst000066400000000000000000000002011500564371300221350ustar00rootroot00000000000000plugin ====== .. autoclass:: litestar.channels.plugin.ChannelsPlugin .. autoclass:: litestar.channels.plugin.ChannelsException litestar-2.16.0/docs/reference/channels/subscriber.rst000066400000000000000000000001161500564371300230070ustar00rootroot00000000000000subscriber ========== .. autoclass:: litestar.channels.subscriber.Subscriber litestar-2.16.0/docs/reference/cli.rst000066400000000000000000000002001500564371300176120ustar00rootroot00000000000000cli === .. automodule:: litestar.cli :members: .. click:: litestar.cli:litestar_group :prog: litestar :nested: full litestar-2.16.0/docs/reference/concurrency.rst000066400000000000000000000001141500564371300214010ustar00rootroot00000000000000concurrency =========== .. automodule:: litestar.concurrency :members: litestar-2.16.0/docs/reference/config.rst000066400000000000000000000005401500564371300203170ustar00rootroot00000000000000config ====== .. automodule:: litestar.config.allowed_hosts :members: .. automodule:: litestar.config.app :members: .. automodule:: litestar.config.compression :members: .. automodule:: litestar.config.cors :members: .. automodule:: litestar.config.csrf :members: .. automodule:: litestar.config.response_cache :members: litestar-2.16.0/docs/reference/connection.rst000066400000000000000000000001111500564371300212030ustar00rootroot00000000000000connection ========== .. automodule:: litestar.connection :members: litestar-2.16.0/docs/reference/contrib/000077500000000000000000000000001500564371300177615ustar00rootroot00000000000000litestar-2.16.0/docs/reference/contrib/htmx.rst000066400000000000000000000002511500564371300214710ustar00rootroot00000000000000HTMX ==== Request ------- .. automodule:: litestar.contrib.htmx.request :members: Response -------- .. automodule:: litestar.contrib.htmx.response :members: litestar-2.16.0/docs/reference/contrib/index.rst000066400000000000000000000002241500564371300216200ustar00rootroot00000000000000contrib ======= .. toctree:: :maxdepth: 1 htmx jinja jwt mako opentelemetry piccolo pydantic sqlalchemy/index litestar-2.16.0/docs/reference/contrib/jinja.rst000066400000000000000000000001021500564371300215770ustar00rootroot00000000000000jinja ===== .. automodule:: litestar.contrib.jinja :members: litestar-2.16.0/docs/reference/contrib/jwt.rst000066400000000000000000000000771500564371300213230ustar00rootroot00000000000000jwt === This page has moved to :doc:`/reference/security/jwt` litestar-2.16.0/docs/reference/contrib/mako.rst000066400000000000000000000000771500564371300214460ustar00rootroot00000000000000mako ==== .. automodule:: litestar.contrib.mako :members: litestar-2.16.0/docs/reference/contrib/opentelemetry.rst000066400000000000000000000002511500564371300234050ustar00rootroot00000000000000opentelemetry ============= .. automodule:: litestar.contrib.opentelemetry :members: .. autoclass:: litestar.contrib.opentelemetry.config.OpenTelemetryHookHandler litestar-2.16.0/docs/reference/contrib/piccolo.rst000066400000000000000000000001201500564371300221340ustar00rootroot00000000000000piccolo_orm =========== .. automodule:: litestar.contrib.piccolo :members: litestar-2.16.0/docs/reference/contrib/pydantic.rst000066400000000000000000000001131500564371300223210ustar00rootroot00000000000000pydantic ======== .. automodule:: litestar.contrib.pydantic :members: litestar-2.16.0/docs/reference/contrib/repository/000077500000000000000000000000001500564371300222005ustar00rootroot00000000000000litestar-2.16.0/docs/reference/contrib/repository/abc.rst000066400000000000000000000001131500564371300234520ustar00rootroot00000000000000:orphan: abc === This page has moved to :doc:`/reference/repository/abc` litestar-2.16.0/docs/reference/contrib/repository/exceptions.rst000066400000000000000000000001401500564371300251060ustar00rootroot00000000000000:orphan: exceptions ========== This page has moved to :doc:`/reference/repository/exceptions` litestar-2.16.0/docs/reference/contrib/repository/filters.rst000066400000000000000000000001271500564371300244020ustar00rootroot00000000000000:orphan: filters ======= This page has moved to :doc:`/reference/repository/filters` litestar-2.16.0/docs/reference/contrib/repository/handlers.rst000066400000000000000000000001331500564371300245270ustar00rootroot00000000000000:orphan: handlers ========= This page has moved to :doc:`/reference/repository/handlers` litestar-2.16.0/docs/reference/contrib/repository/testing.rst000066400000000000000000000001271500564371300244070ustar00rootroot00000000000000:orphan: testing ======= This page has moved to :doc:`/reference/repository/testing` litestar-2.16.0/docs/reference/contrib/sqlalchemy/000077500000000000000000000000001500564371300221235ustar00rootroot00000000000000litestar-2.16.0/docs/reference/contrib/sqlalchemy/base.rst000066400000000000000000000001121500564371300235610ustar00rootroot00000000000000base ==== .. automodule:: litestar.contrib.sqlalchemy.base :members: litestar-2.16.0/docs/reference/contrib/sqlalchemy/dto.rst000066400000000000000000000001321500564371300234370ustar00rootroot00000000000000DTO === This page has moved to :doc:`advanced-alchemy:reference/extensions/litestar/dto` litestar-2.16.0/docs/reference/contrib/sqlalchemy/index.rst000066400000000000000000000001541500564371300237640ustar00rootroot00000000000000sqlalchemy ========== .. toctree:: :titlesonly: plugins repository types base dto litestar-2.16.0/docs/reference/contrib/sqlalchemy/plugins.rst000066400000000000000000000001231500564371300243320ustar00rootroot00000000000000plugins ======= .. automodule:: litestar.contrib.sqlalchemy.plugins :members: litestar-2.16.0/docs/reference/contrib/sqlalchemy/repository.rst000066400000000000000000000001331500564371300250710ustar00rootroot00000000000000repository ========== This page has moved to :doc:`advanced-alchemy:reference/repository` litestar-2.16.0/docs/reference/contrib/sqlalchemy/types.rst000066400000000000000000000001141500564371300240150ustar00rootroot00000000000000types ===== This page has moved to :doc:`advanced-alchemy:reference/types` litestar-2.16.0/docs/reference/controller.rst000066400000000000000000000001111500564371300212270ustar00rootroot00000000000000controller ========== .. automodule:: litestar.controller :members: litestar-2.16.0/docs/reference/data_extractors.rst000066400000000000000000000001311500564371300222350ustar00rootroot00000000000000data_extractors =============== .. automodule:: litestar.data_extractors :members: litestar-2.16.0/docs/reference/datastructures/000077500000000000000000000000001500564371300213765ustar00rootroot00000000000000litestar-2.16.0/docs/reference/datastructures/index.rst000066400000000000000000000002071500564371300232360ustar00rootroot00000000000000datastructures ============== .. automodule:: litestar.datastructures :members: .. toctree:: :maxdepth: 1 secret_values litestar-2.16.0/docs/reference/datastructures/secret_values.rst000066400000000000000000000001411500564371300247700ustar00rootroot00000000000000secret_values ============= .. automodule:: litestar.datastructures.secret_values :members: litestar-2.16.0/docs/reference/di.rst000066400000000000000000000000611500564371300174440ustar00rootroot00000000000000di -- .. automodule:: litestar.di :members: litestar-2.16.0/docs/reference/dto/000077500000000000000000000000001500564371300171075ustar00rootroot00000000000000litestar-2.16.0/docs/reference/dto/base_dto.rst000066400000000000000000000001071500564371300214170ustar00rootroot00000000000000base_dto ======== .. automodule:: litestar.dto.base_dto :members: litestar-2.16.0/docs/reference/dto/config.rst000066400000000000000000000001011500564371300210760ustar00rootroot00000000000000config ====== .. automodule:: litestar.dto.config :members: litestar-2.16.0/docs/reference/dto/data_structures.rst000066400000000000000000000001341500564371300230530ustar00rootroot00000000000000data_structures =============== .. automodule:: litestar.dto.data_structures :members: litestar-2.16.0/docs/reference/dto/dataclass_dto.rst000066400000000000000000000001261500564371300224450ustar00rootroot00000000000000dataclass_dto ============= .. automodule:: litestar.dto.dataclass_dto :members: litestar-2.16.0/docs/reference/dto/field.rst000066400000000000000000000000761500564371300207270ustar00rootroot00000000000000field ===== .. automodule:: litestar.dto.field :members: litestar-2.16.0/docs/reference/dto/index.rst000066400000000000000000000002121500564371300207430ustar00rootroot00000000000000dto === .. toctree:: :maxdepth: 1 config data_structures field types base_dto msgspec_dto dataclass_dto litestar-2.16.0/docs/reference/dto/msgspec_dto.rst000066400000000000000000000001201500564371300221410ustar00rootroot00000000000000msgspec_dto =========== .. automodule:: litestar.dto.msgspec_dto :members: litestar-2.16.0/docs/reference/dto/types.rst000066400000000000000000000000761500564371300210100ustar00rootroot00000000000000types ===== .. automodule:: litestar.dto.types :members: litestar-2.16.0/docs/reference/enums.rst000066400000000000000000000000721500564371300202010ustar00rootroot00000000000000enums ===== .. automodule:: litestar.enums :members: litestar-2.16.0/docs/reference/events.rst000066400000000000000000000002031500564371300203520ustar00rootroot00000000000000events ====== .. automodule:: litestar.events :members: BaseEventEmitterBackend, SimpleEventEmitter, EventListener, listener litestar-2.16.0/docs/reference/exceptions.rst000066400000000000000000000002061500564371300212320ustar00rootroot00000000000000exceptions ========== .. automodule:: litestar.exceptions :members: .. automodule:: litestar.exceptions.responses :members: litestar-2.16.0/docs/reference/handlers.rst000066400000000000000000000001031500564371300206450ustar00rootroot00000000000000handlers ======== .. automodule:: litestar.handlers :members: litestar-2.16.0/docs/reference/index.rst000066400000000000000000000011551500564371300201640ustar00rootroot00000000000000API reference ============= .. toctree:: :titlesonly: :maxdepth: 1 app background_tasks channels/index cli config connection contrib/index controller concurrency data_extractors datastructures/index di dto/index enums events exceptions handlers logging/index middleware/index openapi/index pagination params plugins/index repository/index response/index router routes security/index serialization static_files status_codes stores/index template testing types typing litestar-2.16.0/docs/reference/logging/000077500000000000000000000000001500564371300177475ustar00rootroot00000000000000litestar-2.16.0/docs/reference/logging/config.rst000066400000000000000000000001051500564371300217420ustar00rootroot00000000000000config ====== .. automodule:: litestar.logging.config :members: litestar-2.16.0/docs/reference/logging/index.rst000066400000000000000000000001301500564371300216020ustar00rootroot00000000000000logging ======= .. toctree:: :titlesonly: config picologging standard litestar-2.16.0/docs/reference/logging/picologging.rst000066400000000000000000000001251500564371300230000ustar00rootroot00000000000000picologging ============ .. automodule:: litestar.logging.picologging :members: litestar-2.16.0/docs/reference/logging/standard.rst000066400000000000000000000001131500564371300222740ustar00rootroot00000000000000standard ======== .. automodule:: litestar.logging.standard :members: litestar-2.16.0/docs/reference/middleware/000077500000000000000000000000001500564371300204365ustar00rootroot00000000000000litestar-2.16.0/docs/reference/middleware/allowed_hosts.rst000066400000000000000000000001351500564371300240360ustar00rootroot00000000000000allowed_hosts ============= .. automodule:: litestar.middleware.allowed_hosts :members: litestar-2.16.0/docs/reference/middleware/authentication.rst000066400000000000000000000001401500564371300242020ustar00rootroot00000000000000authentication ============== .. automodule:: litestar.middleware.authentication :members: litestar-2.16.0/docs/reference/middleware/compression.rst000066400000000000000000000001301500564371300235230ustar00rootroot00000000000000compression ============ .. automodule:: litestar.middleware.compression :members: litestar-2.16.0/docs/reference/middleware/cors.rst000066400000000000000000000001021500564371300221270ustar00rootroot00000000000000cors ==== .. automodule:: litestar.middleware.cors :members: litestar-2.16.0/docs/reference/middleware/csrf.rst000066400000000000000000000001021500564371300221160ustar00rootroot00000000000000csrf ==== .. automodule:: litestar.middleware.csrf :members: litestar-2.16.0/docs/reference/middleware/index.rst000066400000000000000000000003201500564371300222720ustar00rootroot00000000000000middleware ========== .. automodule:: litestar.middleware .. toctree:: :maxdepth: 1 allowed_hosts authentication compression cors csrf logging rate_limit session/index litestar-2.16.0/docs/reference/middleware/logging.rst000066400000000000000000000001131500564371300226110ustar00rootroot00000000000000logging ======= .. automodule:: litestar.middleware.logging :members: litestar-2.16.0/docs/reference/middleware/rate_limit.rst000066400000000000000000000002201500564371300233130ustar00rootroot00000000000000rate_limit ========== .. automodule:: litestar.middleware.rate_limit :members: .. autoclass:: litestar.middleware.rate_limit.DurationUnit litestar-2.16.0/docs/reference/middleware/session/000077500000000000000000000000001500564371300221215ustar00rootroot00000000000000litestar-2.16.0/docs/reference/middleware/session/base.rst000066400000000000000000000001121500564371300235570ustar00rootroot00000000000000base ==== .. automodule:: litestar.middleware.session.base :members: litestar-2.16.0/docs/reference/middleware/session/client_side.rst000066400000000000000000000001401500564371300251300ustar00rootroot00000000000000client_side =========== .. automodule:: litestar.middleware.session.client_side :members: litestar-2.16.0/docs/reference/middleware/session/index.rst000066400000000000000000000001311500564371300237550ustar00rootroot00000000000000session ======= .. toctree:: :maxdepth: 1 base client_side server_side litestar-2.16.0/docs/reference/middleware/session/server_side.rst000066400000000000000000000001401500564371300251600ustar00rootroot00000000000000server_side =========== .. automodule:: litestar.middleware.session.server_side :members: litestar-2.16.0/docs/reference/openapi/000077500000000000000000000000001500564371300177545ustar00rootroot00000000000000litestar-2.16.0/docs/reference/openapi/index.rst000066400000000000000000000001231500564371300216110ustar00rootroot00000000000000openapi ======== .. toctree:: :maxdepth: 1 openapi plugins spec litestar-2.16.0/docs/reference/openapi/openapi.rst000066400000000000000000000001001500564371300221300ustar00rootroot00000000000000openapi ======= .. automodule:: litestar.openapi :members: litestar-2.16.0/docs/reference/openapi/plugins.rst000066400000000000000000000001101500564371300221570ustar00rootroot00000000000000plugins ======= .. automodule:: litestar.openapi.plugins :members: litestar-2.16.0/docs/reference/openapi/spec.rst000066400000000000000000000001721500564371300214400ustar00rootroot00000000000000spec ==== .. automodule:: litestar.openapi.spec :members: .. autodata:: litestar.openapi.spec.SecurityRequirement litestar-2.16.0/docs/reference/pagination.rst000066400000000000000000000001101500564371300211740ustar00rootroot00000000000000pagination ========== .. automodule:: litestar.pagination :members: litestar-2.16.0/docs/reference/params.rst000066400000000000000000000000751500564371300203400ustar00rootroot00000000000000params ====== .. automodule:: litestar.params :members: litestar-2.16.0/docs/reference/plugins/000077500000000000000000000000001500564371300200025ustar00rootroot00000000000000litestar-2.16.0/docs/reference/plugins/attrs.rst000066400000000000000000000001021500564371300216620ustar00rootroot00000000000000attrs ===== .. automodule:: litestar.plugins.attrs :members: litestar-2.16.0/docs/reference/plugins/flash_messages.rst000066400000000000000000000001111500564371300235110ustar00rootroot00000000000000===== flash ===== .. automodule:: litestar.plugins.flash :members: litestar-2.16.0/docs/reference/plugins/htmx.rst000066400000000000000000000001051500564371300215100ustar00rootroot00000000000000==== htmx ==== .. automodule:: litestar.plugins.htmx :members: litestar-2.16.0/docs/reference/plugins/index.rst000066400000000000000000000003501500564371300216410ustar00rootroot00000000000000======= plugins ======= .. automodule:: litestar.plugins :members: .. toctree:: :maxdepth: 1 :hidden: attrs flash_messages htmx problem_details prometheus pydantic structlog sqlalchemy litestar-2.16.0/docs/reference/plugins/problem_details.rst000066400000000000000000000001611500564371300236770ustar00rootroot00000000000000=============== problem details =============== .. automodule:: litestar.plugins.problem_details :members: litestar-2.16.0/docs/reference/plugins/prometheus.rst000066400000000000000000000001211500564371300227210ustar00rootroot00000000000000prometheus ========== .. automodule:: litestar.plugins.prometheus :members: litestar-2.16.0/docs/reference/plugins/pydantic.rst000066400000000000000000000001131500564371300223420ustar00rootroot00000000000000pydantic ======== .. automodule:: litestar.plugins.pydantic :members: litestar-2.16.0/docs/reference/plugins/sqlalchemy.rst000066400000000000000000000001341500564371300226740ustar00rootroot00000000000000========== sqlalchemy ========== .. automodule:: litestar.plugins.sqlalchemy :members: litestar-2.16.0/docs/reference/plugins/structlog.rst000066400000000000000000000001301500564371300225540ustar00rootroot00000000000000========= structlog ========= .. automodule:: litestar.plugins.structlog :members: litestar-2.16.0/docs/reference/repository/000077500000000000000000000000001500564371300205405ustar00rootroot00000000000000litestar-2.16.0/docs/reference/repository/abc.rst000066400000000000000000000000771500564371300220230ustar00rootroot00000000000000abc === .. automodule:: litestar.repository.abc :members: litestar-2.16.0/docs/reference/repository/exceptions.rst000066400000000000000000000001301500564371300234450ustar00rootroot00000000000000exceptions ========== This page has moved to :doc:`advanced-alchemy:reference/filters` litestar-2.16.0/docs/reference/repository/filters.rst000066400000000000000000000001221500564371300227350ustar00rootroot00000000000000filters ======= This page has moved to :doc:`advanced-alchemy:reference/filters` litestar-2.16.0/docs/reference/repository/handlers.rst000066400000000000000000000001171500564371300230710ustar00rootroot00000000000000handlers ========= .. automodule:: litestar.repository.handlers :members: litestar-2.16.0/docs/reference/repository/index.rst000066400000000000000000000001621500564371300224000ustar00rootroot00000000000000repository ========== .. toctree:: :titlesonly: abc filters exceptions testing handlers litestar-2.16.0/docs/reference/repository/testing.rst000066400000000000000000000001431500564371300227450ustar00rootroot00000000000000testing ======= .. automodule:: litestar.repository.testing :members: generic_mock_repository litestar-2.16.0/docs/reference/response/000077500000000000000000000000001500564371300201575ustar00rootroot00000000000000litestar-2.16.0/docs/reference/response/base.rst000066400000000000000000000001001500564371300216120ustar00rootroot00000000000000base ==== .. automodule:: litestar.response.base :members: litestar-2.16.0/docs/reference/response/file.rst000066400000000000000000000001001500564371300216170ustar00rootroot00000000000000file ==== .. automodule:: litestar.response.file :members: litestar-2.16.0/docs/reference/response/index.rst000066400000000000000000000002621500564371300220200ustar00rootroot00000000000000response ======== .. automodule:: litestar.response :members: .. toctree:: :maxdepth: 1 :hidden: base file redirect streaming sse template litestar-2.16.0/docs/reference/response/redirect.rst000066400000000000000000000001141500564371300225060ustar00rootroot00000000000000redirect ======== .. automodule:: litestar.response.redirect :members: litestar-2.16.0/docs/reference/response/sse.rst000066400000000000000000000001471500564371300215050ustar00rootroot00000000000000SSE (Server Sent Events) ======================== .. automodule:: litestar.response.sse :members: litestar-2.16.0/docs/reference/response/streaming.rst000066400000000000000000000001171500564371300227010ustar00rootroot00000000000000streaming ========= .. automodule:: litestar.response.streaming :members: litestar-2.16.0/docs/reference/response/template.rst000066400000000000000000000001141500564371300225200ustar00rootroot00000000000000template ======== .. automodule:: litestar.response.template :members: litestar-2.16.0/docs/reference/router.rst000066400000000000000000000001031500564371300203650ustar00rootroot00000000000000router ====== .. automodule:: litestar.router :members: Router litestar-2.16.0/docs/reference/routes.rst000066400000000000000000000000761500564371300203770ustar00rootroot00000000000000routes ====== .. automodule:: litestar.routes :members: litestar-2.16.0/docs/reference/security/000077500000000000000000000000001500564371300201705ustar00rootroot00000000000000litestar-2.16.0/docs/reference/security/index.rst000066400000000000000000000001601500564371300220260ustar00rootroot00000000000000security ======== .. automodule:: litestar.security .. toctree:: :maxdepth: 1 jwt session_auth litestar-2.16.0/docs/reference/security/jwt.rst000066400000000000000000000000751500564371300215300ustar00rootroot00000000000000jwt === .. automodule:: litestar.security.jwt :members: litestar-2.16.0/docs/reference/security/session_auth.rst000066400000000000000000000003011500564371300234200ustar00rootroot00000000000000session_auth ============ .. autoclass:: litestar.security.session_auth.SessionAuth :members: .. autoclass:: litestar.security.session_auth.middleware.SessionAuthMiddleware :members: litestar-2.16.0/docs/reference/serialization.rst000066400000000000000000000002371500564371300217320ustar00rootroot00000000000000serialization ============= .. automodule:: litestar.serialization :members: default_serializer, encode_json, decode_json, encode_msgpack, decode_msgpack litestar-2.16.0/docs/reference/static_files.rst000066400000000000000000000001171500564371300215230ustar00rootroot00000000000000static_files ============ .. automodule:: litestar.static_files :members: litestar-2.16.0/docs/reference/status_codes.rst000066400000000000000000000000771500564371300215570ustar00rootroot00000000000000status_code =========== .. automodule:: litestar.status_codes litestar-2.16.0/docs/reference/stores/000077500000000000000000000000001500564371300176405ustar00rootroot00000000000000litestar-2.16.0/docs/reference/stores/base.rst000066400000000000000000000001271500564371300213040ustar00rootroot00000000000000base ==== .. automodule:: litestar.stores.base :members: Store, NamespacedStore litestar-2.16.0/docs/reference/stores/file.rst000066400000000000000000000000761500564371300213140ustar00rootroot00000000000000file ==== .. automodule:: litestar.stores.file :members: litestar-2.16.0/docs/reference/stores/index.rst000066400000000000000000000001341500564371300214770ustar00rootroot00000000000000stores ====== .. toctree:: base file memory redis registry valkey litestar-2.16.0/docs/reference/stores/memory.rst000066400000000000000000000001041500564371300216750ustar00rootroot00000000000000memory ====== .. automodule:: litestar.stores.memory :members: litestar-2.16.0/docs/reference/stores/redis.rst000066400000000000000000000001011500564371300214700ustar00rootroot00000000000000redis ===== .. automodule:: litestar.stores.redis :members: litestar-2.16.0/docs/reference/stores/registry.rst000066400000000000000000000001301500564371300222340ustar00rootroot00000000000000registry ======== .. automodule:: litestar.stores.registry :members: StoreRegistry litestar-2.16.0/docs/reference/stores/valkey.rst000066400000000000000000000001041500564371300216600ustar00rootroot00000000000000valkey ====== .. automodule:: litestar.stores.valkey :members: litestar-2.16.0/docs/reference/template.rst000066400000000000000000000001031500564371300206600ustar00rootroot00000000000000template ======== .. automodule:: litestar.template :members: litestar-2.16.0/docs/reference/testing.rst000066400000000000000000000005121500564371300205260ustar00rootroot00000000000000testing ======= .. automodule:: litestar.testing :members: RequestFactory, BaseTestClient, TestClient, AsyncTestClient, create_async_test_client, create_test_client, subprocess_sync_client, subprocess_async_client :undoc-members: WebSocketTestSession .. autoclass:: litestar.testing.life_span_handler.LifeSpanHandler litestar-2.16.0/docs/reference/types.rst000066400000000000000000000073201500564371300202210ustar00rootroot00000000000000types ===== .. module:: litestar.types Callable types -------------- .. autodata:: litestar.types.AfterExceptionHookHandler .. autodata:: litestar.types.AfterRequestHookHandler .. autodata:: litestar.types.AfterResponseHookHandler .. autodata:: litestar.types.AnyCallable .. autodata:: litestar.types.AsyncAnyCallable .. autodata:: litestar.types.BeforeMessageSendHookHandler .. autodata:: litestar.types.BeforeRequestHookHandler .. autodata:: litestar.types.CacheKeyBuilder .. autodata:: litestar.types.ExceptionHandler .. autodata:: litestar.types.Guard .. autodata:: litestar.types.LifespanHook .. autodata:: litestar.types.OnAppInitHandler .. autodata:: litestar.types.Serializer ASGI Types ---------- .. autodata:: litestar.types.Method ASGI Application ~~~~~~~~~~~~~~~~~ .. autodata:: litestar.types.ASGIApp ASGI Application Parameters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autodata:: litestar.types.Scope .. autodata:: litestar.types.Receive .. autodata:: litestar.types.Send ASGI Scopes ~~~~~~~~~~~~ .. autoclass:: litestar.types.ASGIVersion .. autoclass:: litestar.types.BaseScope .. autoclass:: litestar.types.HTTPScope .. autoclass:: litestar.types.LifeSpanScope .. autoclass:: litestar.types.WebSocketScope ASGI Events ~~~~~~~~~~~~ .. autoclass:: litestar.types.HTTPRequestEvent .. autoclass:: litestar.types.HTTPResponseStartEvent .. autoclass:: litestar.types.HTTPResponseBodyEvent .. autoclass:: litestar.types.HTTPServerPushEvent .. autoclass:: litestar.types.HTTPDisconnectEvent .. autoclass:: litestar.types.WebSocketConnectEvent .. autoclass:: litestar.types.WebSocketAcceptEvent .. autoclass:: litestar.types.WebSocketReceiveEvent .. autoclass:: litestar.types.WebSocketSendEvent .. autoclass:: litestar.types.WebSocketResponseStartEvent .. autoclass:: litestar.types.WebSocketResponseBodyEvent .. autoclass:: litestar.types.WebSocketDisconnectEvent .. autoclass:: litestar.types.WebSocketCloseEvent .. autoclass:: litestar.types.LifeSpanStartupEvent .. autoclass:: litestar.types.LifeSpanShutdownEvent .. autoclass:: litestar.types.LifeSpanStartupCompleteEvent .. autoclass:: litestar.types.LifeSpanStartupFailedEvent .. autoclass:: litestar.types.LifeSpanShutdownCompleteEvent .. autoclass:: litestar.types.LifeSpanShutdownFailedEvent Event Groupings ~~~~~~~~~~~~~~~ .. autodata:: litestar.types.HTTPReceiveMessage .. autodata:: litestar.types.WebSocketReceiveMessage .. autodata:: litestar.types.LifeSpanReceiveMessage .. autodata:: litestar.types.HTTPSendMessage .. autodata:: litestar.types.WebSocketSendMessage .. autodata:: litestar.types.LifeSpanSendMessage .. autodata:: litestar.types.LifeSpanReceive .. autodata:: litestar.types.LifeSpanSend Send / Receive Parameter Types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autodata:: litestar.types.Message .. autodata:: litestar.types.ReceiveMessage Helper Types ------------ Helper types are useful generic types that can be used. .. autoclass:: litestar.types.SyncOrAsyncUnion .. autoclass:: litestar.types.AnyIOBackend .. autoclass:: litestar.types.OptionalSequence Protocols --------- .. autoclass:: litestar.types.Logger Composite Types --------------- .. autoclass:: litestar.types.Dependencies .. autoclass:: litestar.types.ExceptionHandlersMap .. autodata:: litestar.types.Middleware .. autoclass:: litestar.types.ResponseCookies .. autoclass:: litestar.types.ResponseHeaders .. autoclass:: litestar.types.PathType .. autodata:: litestar.types.Scopes .. autoclass:: litestar.types.TypeEncodersMap .. autoclass:: litestar.types.TypeDecodersSequence .. autoclass:: litestar.types.ParametersMap File types ---------- .. autoclass:: litestar.types.FileInfo :members: .. autoclass:: litestar.types.FileSystemProtocol :members: litestar-2.16.0/docs/reference/typing.rst000066400000000000000000000001431500564371300203630ustar00rootroot00000000000000typing ====== .. py:currentmodule:: litestar.typing .. automodule:: litestar.typing :members: litestar-2.16.0/docs/release-notes/000077500000000000000000000000001500564371300171315ustar00rootroot00000000000000litestar-2.16.0/docs/release-notes/changelog.rst000066400000000000000000007056431500564371300216310ustar00rootroot00000000000000:orphan: 2.x Changelog ============= .. changelog:: 2.16.0 :date: 2025-05-04 .. change:: Logging: Selectively disable logging for status codes or exception types :type: feature :pr: 4086 :issue: 4081 Add support for disabling stack traces for specific status codes or exception types when in debug mode or running with ``log_exceptions="always"`` .. code-block:: python :caption: Disable tracebacks for '404 - Not Found' exceptions from litestar import Litestar, get from litestar.logging import LoggingConfig app = Litestar( route_handlers=[index, value_error, name_error], logging_config=LoggingConfig( disable_stack_trace={404}, log_exceptions="always", ), ) .. change:: Reference route handler in error message for return value / status code mismatch :type: feature :pr: 4157 Improve error message of :exc:`ImproperlyConfiguredException` raised when a route handler's return value annotation is incompatible with its status code. .. change:: DTO: Improve inspection and tracebacks for generated functions :type: feature :pr: 4159 Generated transfer functions now populate :mod:`linecache` to improve tracebacks and support introspection of the generated functions e.g. via :func:`inspect.getsource` **Before:** .. code-block:: text File "", line 18, in func TypeError: **After:** .. code-block:: text File "dto_transfer_function_0971e01f653c", line 18, in func TypeError: .. change:: DTO: Add custom attribute accessor callable :type: feature :pr: 4160 Add :attr:`~litestar.dto.base_dto.AbstractDTO.attribute_accessor` property to ``AbstractDTO``, that can be set to a custom :func:`getattr`\ -like function which will be used every time an attribute is accessed on a source instance .. change:: Typing: remove usage of private ``_AnnotatedAlias`` :type: bugfix :pr: 4126 Remove deprecated usage of ``_AnnotatedAlias``, which is no longer needed for backwards compatibility. .. change:: DI: Ensure generator dependencies always handle error during clean up :type: bugfix :pr: 4148 Fix issue where dependency cleanup could be skipped during exception handling, if another exception happened during the cleanup itself. - Ensure all dependencies are cleaned up, even if exceptions occur. - Group exceptions using :exc:`ExceptionGroup` during cleanup phase. .. change:: CLI: Improve error message on ``ImportError`` :type: bugfix :pr: 4152 :issue: 4129 Fix misleading error message when using ``--app`` CLI argument and an unrelated :exc:`ImportError` occurs. Unrelated import errors will now propagate as usual .. change:: CLI: Ensure dynamically added commands / groups are always visible :type: bugfix :pr: 4161 :issue: 2783 Fix an issue where dynamically added commands or groups were not always visible during listing e.g. via ``--help`` .. change:: Testing: Ensure subprocess client does not swallow startup failure :type: bugfix :pr: 4153 :issue: 4021 Ensure ``StartupError`` is raised by :func:`~litestar.testing.subprocess_sync_client` and :func:`~litestar.testing.subprocess_async_client` if the application failed to start within the timeout. .. change:: OpenAPI: Use ``prefixItems`` for fixed-length tuples :type: bugfix :pr: 4132 :issue: 4130 Use ``prefixItems`` instead of ``array`` syntax to render fixed-length tuples .. change:: OpenAPI: Add custom example ids support :type: feature :pr: 4133 :issue: 4013 Add a new field ``id`` to :class:`~litestar.openapi.spec.Example`, to set a custom ID for examples .. change:: OpenAPI: Allow passing scalar configuration options :type: feature :pr: 4162 :issue: 3951 Add an ``options`` parameter to :class:`~litestar.openapi.plugins.ScalarRenderPlugin`, that can be used to pass options directly to scalar. .. code-block:: python from litestar import Litestar, get from litestar.openapi.config import OpenAPIConfig from litestar.openapi.plugins import ScalarRenderPlugin scalar_plugin = ScalarRenderPlugin(version="1.19.5", options={"showSidebar": False}) app = Litestar( route_handlers=[hello_world], openapi_config=OpenAPIConfig( title="Litestar Example", description="Example of Litestar with Scalar OpenAPI docs", version="0.0.1", render_plugins=[scalar_plugin], path="/docs", ), ) .. changelog:: 2.15.2 :date: 2025-04-06 .. change:: Events: Fix error handling for synchronous handlers :type: bugfix :pr: 4045 Fix a bug where exceptions weren't handled correctly on synchronous event handlers, and would result in another exception. .. code-block:: python @listener("raise_exception") def raise_exception_if_odd(value) -> None: if value is not None and value % 2 != 0: raise ValueError(f"{value} is odd") Would raise an ``AttributeError: 'AsyncCallable' object has no attribute '__name__'. Did you mean: '__ne__'?`` .. change:: Fix wrong order of arguments in FileSystemAdapter passed to ``open`` fsspec file system :type: bugfix :pr: 4049 The order of arguments of various fsspec implementations varies, causing ``FileSystemAdapter.open`` to fail in different ways. This was fixed by always passing arguments as keywords to the file system. .. change:: Correctly handle ``typing_extensions.TypeAliasType`` on ``typing-extensions>4.13.0`` :type: bugfix :pr: 4089 :issue: 4088 Handle the diverging ``TypeAliasType`` introduced in typing-extensions ``4.13.0``; This type is no longer backwards compatible, as it is a distinct new type from ``typing.TypeAliasType`` .. changelog:: 2.15.1 :date: 2025-02-27 .. change:: Warn about using streaming responses with a ``body`` :type: bugfix :pr: 4033 Issue a warning if the ``body`` parameter of a streaming response is used, as setting this has no effect .. change:: Fix incorrect deprecation warning issued when subclassing middlewares :type: bugfix :pr: 4036 :issue: 4035 Fix a bug introduced in #3996 that would incorrectly issue a deprecation warning if a user subclassed a Litestar built-in middleware which itself subclasses ``AbstractMiddleware`` .. changelog:: 2.15.0 :date: 2025-02-26 .. change:: Prevent accidental ``scope`` key overrides by mounted ASGI apps :type: bugfix :pr: 3945 :issue: 3934 When mounting ASGI apps, there's no guarantee they won't overwrite some key in the ``scope`` that we rely on, e.g. ``scope["app"]``, which is what caused https://github.com/litestar-org/litestar/issues/3934. To prevent this, two thing shave been changed: 1. We do not store the Litestar instance under the generic ``app`` key anymore, but the more specific ``litestar_app`` key. In addition the :meth:`~litestar.app.Litestar.from_scope` method has been added, which can be used to safely access the current app from the scope 2. A new parameter ``copy_scope`` has been added to the ASGI route handler, which, when set to ``True`` will copy the scope before calling into the mounted ASGI app, aiming to make things behave more as expected, by giving the called app its own environment without causing any side-effects. Since this change might break some things, It's been left it with a default of ``None``, which does not copy the scope, but will issue a warning if the mounted app modified it, enabling users to decide how to deal with that situation .. change:: Fix deprecated ``attrs`` import :type: bugfix :pr: 3947 :issue: 3946 A deprecated import of the ``attrs`` plugins caused a warning. This has been fixed. .. change:: JWT: Revoked token handler :type: feature :pr: 3960 Add a new ``revoked_token_handler`` on same level as ``retrieve_user_handler``, for :class:`~litestar.security.jwt.BaseJWTAuth`. .. change:: Allow ``route_reverse`` params of type ``uuid`` to be passed as ``str`` :type: feature :pr: 3972 Allows params of type ``uuid`` to be passed as strings (e.g. their hex representation) into :meth:`~litestar.app.Litestar.route_reverse` .. change:: CLI: Better error message for invalid ``--app`` string :type: feature :pr: 3977 :issue: 3893 Improve the error handling when an invalid ``--app`` string is passed .. change:: DTO: Support ``@property`` fields for msgspec and dataclass :type: feature :pr: 3981 Support :class:`property` fields for msgspec and dataclasses during serialization and for OpenAPI schema generation. .. change:: Add new ``ASGIMiddleware`` :type: feature :pr: 3996 Add a new base middleware class to facilitate easier configuration and middleware dispatching. The new :class:`~litestar.middleware.ASGIMiddleware` features the same functionality as :class:`~litestar.middleware.AbstractMiddleware`, but makes it easier to pass configuration directly to middleware classes without a separate configuration object, allowing the need to use :class:`~litestar.middleware.DefineMiddleware`. .. seealso:: :doc:`/usage/middleware/creating-middleware` .. change:: Add ``SerializationPlugin`` and ``InitPlugin`` to replace their respective protocols :type: feature :pr: 4025 - Add :class:`~litestar.plugins.SerializationPlugin` to replace :class:`~litestar.plugins.SerializationPluginProtocol` - Add :class:`~litestar.plugins.InitPlugin` to replace :class:`~litestar.plugins.InitPluginProtocol` Following the same approach as for other plugins, they inherit their respective protocol for now, to keep type / `isinstance` checks compatible. .. important:: The plugin protocols will be removed in version 3.0 .. change:: Allow passing a ``debugger_module`` to the application :type: feature :pr: 3967 A new ``debugger_module`` parameter has been added to :class:`~litestar.app.Litestar`, which can receive any debugger module that implements a :func:`pdb.post_mortem` function with the same signature as the stdlib. This function will be called when an exception occurs and ``pdb_on_exception`` is set to ``True``\ . .. changelog:: 2.14.0 :date: 2025-02-12 .. change:: Deprecate ``litestar.contrib.prometheus`` in favour of ``litestar.plugins.prometheus`` :type: feature :pr: 3863 The module ``litestar.contrib.prometheus`` has been moved to ``litestar.plugins.prometheus``. ``litestar.contrib.prometheus`` will be deprecated in the next major version .. change:: Deprecate ``litestar.contrib.attrs`` in favour of ``litestar.plugins.attrs`` :type: feature :pr: 3862 The module ``litestar.contrib.attrs`` has been moved to ``litestar.plugins.attrs``. ``litestar.contrib.attrs`` will be deprecated in the next major version .. change:: Add a streaming multipart parser :type: feature :pr: 3872 Add a streaming multipart parser via the `multipart `_ library This provides - Ability to stream large / larger-than-memory file uploads - Better / more correct edge case handling - Still good performance .. change:: Add WebSocket send stream :type: feature :pr: 3894 Add a new :func:`~litestar.handlers.websocket_stream` route handler that supports streaming data *to* a WebSocket via an async generator. .. code-block:: python @websocket_stream("/") async def handler() -> AsyncGenerator[str, None]: yield str(time.time()) await asyncio.sleep(.1) This is roughly equivalent to (with some edge case handling omitted): .. code-block:: python @websocket("/") async def handler(socket: WebSocket) -> None: await socket.accept() try: async with anyio.task_group() as tg: # 'receive' in the background to catch client disconnects tg.start_soon(socket.receive) while True: socket.send_text(str(time.time())) await asyncio.sleep(.1) finally: await socket.close() Just like the WebSocket listeners, it also supports dependency injection and serialization: .. code-block:: python @dataclass class Event: time: float data: str async def provide_client_info(socket: WebSocket) -> str: return f"{socket.client.host}:{socket.client.port}" @websocket_stream("/", dependencies={"client_info": provide_client_info}) async def handler(client_info: str) -> AsyncGenerator[Event, None]: yield Event(time=time.time(), data="hello, world!") await asyncio.sleep(.1) .. seealso:: :ref:`usage/websockets:WebSocket Streams` .. change:: Add query params to ``Redirect`` :type: feature :pr: 3901 :issue: 3891 Add a ``query_params`` parameter to :class:`~litestar.response.Redirect`, to supply query parameters for a redirect .. change:: Add Valkey as a native store :type: feature :pr: 3892 Add a new :class:`~litestar.stores.valkey.ValkeyStore`, which provides the same functionality as the :class:`~litestar.stores.redis.RedisStore` but using valkey instead. The necessary dependencies can be installed with the ``litestar[valkey]`` extra, which includes ``valkey`` as well as ``libvalkey`` as an optimisation layer. .. change:: Correctly specify ``"path"`` as an error message source for validation errors :type: feature :pr: 3920 :issue: 3919 Use ``"path"`` as the ``"source"`` property of a validation error message if the key is a path parameter. .. change:: Add subprocess test client :type: feature :pr: 3655 :issue: 3654 Add new :func:`~litestar.testing.subprocess_async_client` and :func:`~litestar.testing.subprocess_sync_client`, which can run an application in a new process, primarily for the purpose of end-to-end testing. The application will be run with ``uvicorn``, which has to be installed separately or via the ``litestar[standard]`` group. .. change:: Support for Python 3.13 :type: feature :pr: 3850 Support Python 3.13 .. important:: - There are no Python 3.13 prebuilt wheels for ``psycopg[binary]``. If you rely on this for development, you'll need to have the postgres development libraries installed - ``picologging`` does not currently support Python 3.13 .. change:: OpenAPI: Always generate refs for enums :type: bugfix :pr: 3525 :issue: 3518 Ensure that enums always generate a schema reference instead of being inlined .. change:: Support varying ``mtime`` semantics across different fsspec implementations :type: bugfix :pr: 3902 :issue: 3899 Change the implementation of :class:`~litestar.response.File` to be able to handle most fsspec implementation's ``mtime`` equivalent. This is necessary because fsspec implementations do not have a standardised way to retrieve an ``mtime`` equivalent; Some report an ``mtime``, while some may use a different key (e.g. ``Last-Modified``) and others do not report this value at all. .. change:: OpenAPI: Ensure query-only properties are only included in queries :type: bugfix :pr: 3909 :issue: 3908 Remove the inclusion of the query-only properties ``allowEmptyValue`` and ``allowReserved`` in path, cookie, header parameter and response header schemas .. change:: Channels: Use ``SQL`` function for in psycopg backend :type: bugfix :pr: 3916 Update the :class:`~litestar.channels.backends.psycopg.PsycoPgChannelsBackend` backend to use the native psycopg ``SQL`` API .. changelog:: 2.13.0 :date: 2024-11-20 .. change:: Add ``request_max_body_size`` layered parameter :type: feature Add a new ``request_max_body_size`` layered parameter, which limits the maximum size of a request body before returning a ``413 - Request Entity Too Large``. .. seealso:: :ref:`usage/requests:limits` .. change:: Send CSRF request header in OpenAPI plugins :type: feature :pr: 3754 Supported OpenAPI UI clients will extract the CSRF cookie value and attach it to the request headers if CSRF is enabled on the application. .. change:: deprecate `litestar.contrib.sqlalchemy` :type: feature :pr: 3755 Deprecate the ``litestar.contrib.sqlalchemy`` module in favor of ``litestar.plugins.sqlalchemy`` .. change:: implement `HTMX` plugin using `litestar-htmx` :type: feature :pr: 3837 This plugin migrates the HTMX integration to ``litestar.plugins.htmx``. This logic has been moved to it's own repository named ``litestar-htmx`` .. change:: Pydantic: honor ``hide_input_in_errors`` in throwing validation exceptions :type: feature :pr: 3843 Pydantic's ``BaseModel`` supports configuration to hide data values when throwing exceptions, via setting ``hide_input_in_errors`` -- see https://docs.pydantic.dev/2.0/api/config/#pydantic.config.ConfigDict.hide_input_in_errors and https://docs.pydantic.dev/latest/usage/model_config/#hide-input-in-errors Litestar will now honour this setting .. change:: deprecate``litestar.contrib.pydantic`` :type: feature :pr: 3852 :issue: 3787 ## Description Deprecate ``litestar.contrib.pydantic`` in favor of ``litestar.plugins.pydantic`` .. change:: Fix sign bug in rate limit middelware :type: bugfix :pr: 3776 Fix a bug in the rate limit middleware, that would cause the response header fields ``RateLimit-Remaining`` and ``RateLimit-Reset`` to have negative values. .. change:: OpenAPI: map JSONSchema spec naming convention to snake_case when names from ``schema_extra`` are not found :type: bugfix :pr: 3767 :issue: 3766 Address rejection of ``schema_extra`` values using JSONSchema spec-compliant key names by mapping between the relevant naming conventions. .. change:: Use correct path template for routes without path parameters :type: bugfix :pr: 3784 Fix a but where, when using ``PrometheusConfig.group_path=True``, the metrics exporter response content would ignore all paths with no path parameters. .. change:: Fix a dangling anyio stream in ``TestClient`` :type: bugfix :pr: 3836 :issue: 3834 Fix a dangling anyio stream in ``TestClient`` that would cause a resource warning Closes #3834. .. change:: Fix bug in handling of missing ``more_body`` key in ASGI response :type: bugfix :pr: 3845 Some frameworks do not include the ``more_body`` key in the "http.response.body" ASGI event. According to the ASGI specification, this key should be set to ``False`` when there is no additional body content. Litestar expects ``more_body`` to be explicitly defined, but others might not. This leads to failures when an ASGI framework mounted on Litestar throws error if this key is missing. .. change:: Fix duplicate ``RateLimit-*`` headers with caching :type: bugfix :pr: 3855 :issue: 3625 Fix a bug where ``RateLimitMiddleware`` duplicate all ``RateLimit-*`` headers when handler cache is enabled. .. changelog:: 2.12.1 :date: 2024-09-21 .. change:: Fix base package requiring ``annotated_types`` dependency :type: bugfix :pr: 3750 :issue: 3749 Fix a bug introduced in #3721 that was released with ``2.12.0`` caused an :exc:`ImportError` when the ``annotated_types`` package was not installed. .. changelog:: 2.12.0 :date: 2024-09-21 .. change:: Fix overzealous warning for greedy middleware ``exclude`` pattern :type: bugfix :pr: 3712 Fix a bug introduced in ``2.11.0`` (https://github.com/litestar-org/litestar/pull/3700), where the added warning for a greedy pattern use for the middleware ``exclude`` parameter was itself greedy, and would warn for non-greedy patterns, e.g. ``^/$``. .. change:: Fix dangling coroutines in request extraction handling cleanup :type: bugfix :pr: 3735 :issue: 3734 Fix a bug where, when a required header parameter was defined for a request that also expects a request body, failing to provide the header resulted in a :exc:`RuntimeWarning`. .. code-block:: python @post() async def handler(data: str, secret: Annotated[str, Parameter(header="x-secret")]) -> None: return None If the ``x-secret`` header was not provided, warning like this would be seen: .. code-block:: RuntimeWarning: coroutine 'json_extractor' was never awaited .. change:: OpenAPI: Correctly handle ``type`` keyword :type: bugfix :pr: 3715 :issue: 3714 Fix a bug where a type alias created with the ``type`` keyword would create an empty OpenAPI schema entry for that parameter .. change:: OpenAPI: Ensure valid schema keys :type: bugfix :pr: 3635 :issue: 3630 Ensure that generated schema component keys are always valid according to `§ 4.8.7.1 `_ of the OpenAPI specification. .. change:: OpenAPI: Correctly handle ``msgspec.Struct`` tagged unions :type: bugfix :pr: 3742 :issue: 3659 Fix a bug where the OpenAPI schema would not include the struct fields implicitly generated by msgspec for its `tagged union `_ support. The tag field of the struct will now be added as a ``const`` of the appropriate type to the schema. .. change:: OpenAPI: Fix Pydantic 1 constrained string with default factory :type: bugfix :pr: 3721 :issue: 3710 Fix a bug where using a Pydantic model with a ``default_factory`` set for a constrained string field would raise a :exc:`SerializationException`. .. code-block:: python class Model(BaseModel): field: str = Field(default_factory=str, max_length=600) .. change:: OpenAPI/DTO: Fix missing Pydantic 2 computed fields :type: bugfix :pr: 3721 :issue: 3656 Fix a bug that would lead to Pydantic computed fields to be ignored during schema generation when the model was using a :class:`~litestar.contrib.pydantic.PydanticDTO`. .. code-block:: python :caption: Only the ``foo`` field would be included in the schema class MyModel(BaseModel): foo: int @computed_field def bar(self) -> int: return 123 @get(path="/", return_dto=PydanticDTO[MyModel]) async def test() -> MyModel: return MyModel.model_validate({"foo": 1}) .. change:: OpenAPI: Fix Pydantic ``json_schema_extra`` overrides only being merged partially :type: bugfix :pr: 3721 :issue: 3656 Fix a bug where ``json_schema_extra`` were not reliably extracted from Pydantic models and included in the OpenAPI schema. .. code-block:: python :caption: Only the title set directly on the field would be used for the schema class Model(pydantic.BaseModel): with_title: str = pydantic.Field(title="new_title") with_extra_title: str = pydantic.Field(json_schema_extra={"title": "more_new_title"}) @get("/example") async def example_route() -> Model: return Model(with_title="1", with_extra_title="2") .. change:: Support strings in ``media_type`` for ``ResponseSpec`` :type: feature :pr: 3729 :issue: 3728 Accept strings for the ``media_type`` parameter of :class:`~litestar.openapi.datastructures.ResponseSpec`, making it behave the same way as :paramref:`~litestar.response.Response.media_type`. .. change:: OpenAPI: Allow customizing schema component keys :type: feature :pr: 3738 Allow customizing the schema key used for a component in the OpenAPI schema. The supplied keys are enforced to be unique, and it is checked that they won't be reused across different types. The keys can be set with the newly introduced ``schema_component_key`` parameter, which is available on :class:`~litestar.params.KwargDefinition`, :func:`~litestar.params.Body` and :func:`~litestar.params.Parameter`. .. code-block:: python :caption: Two components will be generated: ``Data`` and ``not_data`` @dataclass class Data: pass @post("/") def handler( data: Annotated[Data, Parameter(schema_component_key="not_data")], ) -> Data: return Data() @get("/") def handler_2() -> Annotated[Data, Parameter(schema_component_key="not_data")]: return Data() .. change:: Raise exception when body parameter is annotated with non-bytes type :type: feature :pr: 3740 Add an informative error message to help avoid the common mistake of attempting to use the ``body`` parameter to receive validated / structured data by annotating it with a type such as ``list[str]``, instead of ``bytes``. .. change:: OpenAPI: Default to ``latest`` scalar version :type: feature :pr: 3747 Change the default version of the scalar OpenAPI renderer to ``latest`` .. changelog:: 2.11.0 :date: 2024-08-27 .. change:: Use PyJWT instead of python-jose :type: feature :pr: 3684 The functionality in :mod:`litestar.security.jwt` is now backed by `PyJWT `_ instead of `python-jose `_, due to the unclear maintenance status of the latter. .. change:: DTO: Introduce ``forbid_unknown_fields`` config :type: feature :pr: 3690 Add a new config option to :class:`~litestar.dto.config.DTOConfig`: :attr:`~litestar.dto.config.DTOConfig.forbid_unknown_fields` When set to ``True``, a validation error response will be returned if the source data contains fields not defined on the model. .. change:: DTO: Support ``extra="forbid"`` model config for ``PydanticDTO`` :type: feature :pr: 3691 For Pydantic models with `extra="forbid" `_ in their configuration: .. tab-set:: .. tab-item:: Pydantic 2 .. code-block:: python class User(BaseModel): model_config = ConfigDict(extra='ignore') name: str .. tab-item:: Pydantic 1 .. code-block:: python class User(BaseModel): class Config: extra = "ignore" name: str :attr:`~litestar.dto.config.DTOConfig.forbid_unknown_fields` will be set to ``True`` by default. .. note:: It's still possible to override this configuration at the DTO level To facilitate this feature, :meth:`~litestar.dto.base_dto.AbstractDTO.get_config_for_model_type` has been added to :class:`~litestar.dto.base_dto.AbstractDTO`, allowing the customization of the base config defined on the DTO factory for a specific model type. It will be called on DTO factory initialization, and receives the concrete DTO model type along side the :class:`~litestar.dto.config.DTOConfig` defined on the base DTO, which it can alter and return a new version to be used within the DTO instance. .. change:: Custom JWT payload classes :type: feature :pr: 3692 Support extending the default :class:`~litestar.security.jwt.Token` class used by the JWT backends decode the payload into. - Add new ``token_cls`` field on the JWT auth config classes - Add new ``token_cls`` parameter to JWT auth middlewares - Switch to using msgspec to convert the JWT payload into instances of the token class .. code-block:: python import dataclasses import secrets from typing import Any, Dict from litestar import Litestar, Request, get from litestar.connection import ASGIConnection from litestar.security.jwt import JWTAuth, Token @dataclasses.dataclass class CustomToken(Token): token_flag: bool = False @dataclasses.dataclass class User: id: str async def retrieve_user_handler(token: CustomToken, connection: ASGIConnection) -> User: return User(id=token.sub) TOKEN_SECRET = secrets.token_hex() jwt_auth = JWTAuth[User]( token_secret=TOKEN_SECRET, retrieve_user_handler=retrieve_user_handler, token_cls=CustomToken, ) @get("/") def handler(request: Request[User, CustomToken, Any]) -> Dict[str, Any]: return {"id": request.user.id, "token_flag": request.auth.token_flag} .. change:: Extended JWT configuration options :type: feature :pr: 3695 **New JWT backend fields** - :attr:`~litestar.security.jwt.JWTAuth.accepted_audiences` - :attr:`~litestar.security.jwt.JWTAuth.accepted_issuers` - :attr:`~litestar.security.jwt.JWTAuth.require_claims` - :attr:`~litestar.security.jwt.JWTAuth.verify_expiry` - :attr:`~litestar.security.jwt.JWTAuth.verify_not_before` - :attr:`~litestar.security.jwt.JWTAuth.strict_audience` **New JWT middleware parameters** - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.token_audience` - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.token_issuer` - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.require_claims` - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.verify_expiry` - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.verify_not_before` - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.strict_audience` **New ``Token.decode`` parameters** - :paramref:`~litestar.security.jwt.Token.decode.audience` - :paramref:`~litestar.security.jwt.Token.decode.issuer` - :paramref:`~litestar.security.jwt.Token.decode.require_claims` - :paramref:`~litestar.security.jwt.Token.decode.verify_exp` - :paramref:`~litestar.security.jwt.Token.decode.verify_nbf` - :paramref:`~litestar.security.jwt.Token.decode.strict_audience` **Other changes** :meth`Token.decode_payload <~litestar.security.jwt.Token.decode_payload>` has been added to make customization of payload decoding / verification easier without having to re-implement the functionality of the base class method. .. seealso:: :doc:`/usage/security/jwt` .. change:: Warn about greedy exclude patterns in middlewares :type: feature :pr: 3700 Raise a warning when a middlewares ``exclude`` pattern greedily matches all paths. .. code-block:: python from litestar.middlewares class MyMiddleware(AbstractMiddleware): exclude = ["/", "/home"] async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: await self.app(scope, receive, send) Middleware like this would silently be disabled for every route, since the exclude pattern ``/`` matches all paths. If a configuration like this is detected, a warning will now be raised at application startup. .. change:: RFC 9457 *Problem Details* plugin :type: feature :pr: 3323 :issue: 3199 Add a plugin to support `RFC 9457 `_ *Problem Details* responses for error response. :class:`~litestar.plugins.problem_details.ProblemDetailsPlugin` enables to selectively or collectively turn responses with an error status code into *Problem Detail* responses. .. seealso:: :doc:`/usage/plugins/problem_details` .. change:: Fix creation of ``FormMultiDict`` in ``Request.form`` to properly handle multi-keys :type: bugfix :pr: 3639 :issue: 3627 Fix https://github.com/litestar-org/litestar/issues/3627 by properly handling the creation of :class:`~litestar.datastructures.FormMultiDict` where multiple values are given for a single key, to make :meth:`~litestar.connection.Request.form` match the behaviour of receiving form data via the ``data`` kwarg inside a route handler. **Before** .. code-block:: python @post("/") async def handler(request: Request) -> Any: return (await request.form()).getall("foo") with create_test_client(handler) as client: print(client.post("/", data={"foo": ["1", "2"]}).json()) # [["1", "2"]] **After** .. code-block:: python @post("/") async def handler(request: Request) -> Any: return (await request.form()).getall("foo") with create_test_client(handler) as client: print(client.post("/", data={"foo": ["1", "2"]}).json()) # ["1", "2"] .. change:: DTO: Fix inconsistent use of strict decoding mode :type: bugfix :pr: 3685 Fix inconsistent usage of msgspec's ``strict`` mode in the base DTO backend. ``strict=False`` was being used when transferring from builtins, while ``strict=True`` was used transferring from raw data, causing an unwanted discrepancy in behaviour. .. change:: Use path template for prometheus metrics :type: bugfix :pr: 3687 Changed previous 1-by-1 replacement logic for ``PrometheusMiddleware.group_path=true`` with a more robust and slightly faster solution. .. change:: Ensure OpenTelemetry captures exceptions in the outermost application layers :type: bugfix :pr: 3689 :issue: 3663 A bug was fixed that resulted in exception occurring in the outermost application layer not being captured under the current request span, which led to incomplete traces. .. change:: Fix CSRFMiddleware sometimes setting cookies for excluded paths :type: bugfix :pr: 3698 :issue: 3688 Fix a bug that would cause :class:`~litestar.middleware.csrf.CSRFMiddleware` to set a cookie (which would not be used subsequently) on routes it had been excluded from via a path pattern. .. change:: Make override behaviour consistent between ``signature_namespace`` and ``signature_types`` :type: bugfix :pr: 3696 :issue: 3681 Ensure that adding signature types to ``signature_namespace`` and ``signature_types`` behaves the same way when a name was already present in the namespace. Both will now issue a warning if a name is being overwritten with a different type. If a name is registered again for the same type, no warning will be given. .. note:: You can disable this warning globally by setting ``LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE=0`` in your environment .. changelog:: 2.10.0 :date: 2024-07-26 .. change:: Allow creating parent directories for a file store :type: feature :pr: 3526 Allow ``mkdir`` True when creating a file store. .. change:: Add ``logging_module`` parameter to ``LoggingConfig`` :type: feature :pr: 3578 :issue: 3536 Provide a way in the ``logging_module`` to switch easily from ``logging`` to ``picologging``. .. change:: Add handler name to exceptions in handler validation :type: feature :pr: 3575 Add handler name to exceptions raise by ``_validate_handler_function``. .. change:: Add strict validation support for Pydantic plugin :type: feature :pr: 3608 :issue: 3572 Adds parameters in pydantic plugin to support strict validation and all the ``model_dump`` args .. change:: Fix signature model signatures clash :type: bugfix :pr: 3605 :issue: 3593 Ensures that the functions used by the signature model itself do not interfere with the signature model created. .. change:: Correctly handle Annotated ``NewType`` :type: bugfix :pr: 3615 :issue: 3614 Resolves infinite loop in schema generation when a model has an Annotated ``NewType``. .. change:: Use `ASGIConnection` instead of ``Request`` for ``flash`` :type: bugfix :pr: 3626 Currently, the ``FlashPlugin`` expects the ``request`` parameter to be a type of ``Request``. However, there's no reason it can't use the parent class ``ASGIConnection``. Doing this, allows for flash to be called in guards that expect an ``ASGIConnection`` instead of ``Request``: .. code-block:: python def requires_active_user(connection: ASGIConnection, _: BaseRouteHandler) -> None: if connection.user.is_active: return msg = "Your user account is inactive." flash(connection, msg, category="error") raise PermissionDeniedException(msg) .. change:: Allow returning ``Response[None]`` from head route handlers :type: bugfix :pr: 3641 :issue: 3640 Fix a bug where the validation of the return annotation for the ``head`` route handler was too strict and would not allow returning a ``Response[None]``. .. changelog:: 2.9.1 :date: 2024-06-21 .. change:: Add OPTIONS to the default safe methods for CSRFConfig :type: bugfix :pr: 3538 Add ``OPTIONS`` to the default safe methods for :class:`~litestar.config.csrf.CSRFConfig` .. change:: Prometheus: Capture templated route name for metrics :type: bugfix :pr: 3533 Adding new extraction function for prometheus metrics to avoid high cardinality issue in prometheus, eg having metrics ``GET /v1/users/{id}`` is preferable over ``GET /v1/users/1``, ``GET /v1/users/2,GET /v1/users/3`` More info about prometheus high cardinality https://grafana.com/blog/2022/02/15/what-are-cardinality-spikes-and-why-do-they-matter/ .. change:: Respect ``base_url`` in ``.websocket_connect`` :type: bugfix :pr: 3567 Fix a bug that caused :meth:`~litestar.testing.TestClient.websocket_connect` / :meth:`~litestar.testing.AsyncTestClient.websocket_connect` to not respect the ``base_url`` set in the client's constructor, and instead would use the static ``ws://testerver`` URL as a base. Also removes most of the test client code as it was unneeded and in the way of this fix :) Explanation for the last part: All the extra code we had was just proxying method calls to the ``httpx.Client`` / ``httpx.AsyncClient``, while altering the base URL. Since we already set the base URL on the httpx Client's superclass instance, which in turn does this merging internally, this step isn't needed at all. .. change:: Fix deprecation warning for subclassing route handler decorators :type: bugfix :pr: 3569 :issue: 3552 Fix an issue where there was a deprecation warning emitted by all route handler decorators. This warning was introduced in ``2.9.0`` to warn about the upcoming deprecation, but should have only applied to user subclasses of the handler classes, and not the built-in ones (``get``, ``post``, etc.) .. change:: CLI: Don't call ``rich_click.patch`` if ``rich_click`` is installed :type: bugfix :pr: 3570 :issue: 3534 Don't call ``rich_click.patch`` if ``rich_click`` is installed. As this monkey patches click globally, it can introduce unwanted side effects. Instead, use conditional imports to refer to the correct library. External libraries will still be able to make use of ``rich_click`` implicitly when it's installed by inheriting from ``LitestarGroup`` / ``LitestarExtensionGroup``, which they will by default. .. change:: Correctly handle ``typing.NewType`` :type: bugfix :pr: 3580 When encountering a :class:`typing.NewType` during OpenAPI schema generation, we currently treat it as an opaque type. This PR changes the behaviour such that :class`typing.NewType`s are always unwrapped during schema generation. .. change:: Encode response content object returned from an exception handler. :type: bugfix :pr: 3585 When an handler raises an exception and exception handler returns a Response with a model (e.g. pydantic) object, ensure that object can be encoded as when returning data from a regular handler. .. changelog:: 2.9.0 :date: 2024-06-02 .. change:: asgi lifespan msg after lifespan context exception :type: bugfix :pr: 3315 An exception raised within an asgi lifespan context manager would result in a "lifespan.startup.failed" message being sent after we've already sent a "lifespan.startup.complete" message. This would cause uvicorn to raise a ``STATE_TRANSITION_ERROR`` assertion error due to their check for that condition , if asgi lifespan is forced (i.e., with ``$ uvicorn test_apps.test_app:app --lifespan on``). E.g., .. code-block:: During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/home/peter/.local/share/pdm/venvs/litestar-dj-FOhMr-3.8/lib/python3.8/site-packages/uvicorn/lifespan/on.py", line 86, in main await app(scope, self.receive, self.send) File "/home/peter/.local/share/pdm/venvs/litestar-dj-FOhMr-3.8/lib/python3.8/site-packages/uvicorn/middleware/proxy_headers.py", line 69, in __call__ return await self.app(scope, receive, send) File "/home/peter/PycharmProjects/litestar/litestar/app.py", line 568, in __call__ await self.asgi_router.lifespan(receive=receive, send=send) # type: ignore[arg-type] File "/home/peter/PycharmProjects/litestar/litestar/_asgi/asgi_router.py", line 180, in lifespan await send(failure_message) File "/home/peter/.local/share/pdm/venvs/litestar-dj-FOhMr-3.8/lib/python3.8/site-packages/uvicorn/lifespan/on.py", line 116, in send assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR AssertionError: Got invalid state transition on lifespan protocol. This PR modifies ``ASGIRouter.lifespan()`` so that it sends a shutdown failure message if we've already confirmed startup. .. change:: bug when pydantic==1.10 is installed :type: bugfix :pr: 3335 :issue: 3334 Fix a bug introduced in #3296 where it failed to take into account that the ``pydantic_v2`` variable could be ``Empty``. .. change:: OpenAPI router and controller on same app. :type: bugfix :pr: 3338 :issue: 3337 Fixes an :exc`ImproperlyConfiguredException` where an app that explicitly registers an ``OpenAPIController`` on the application, and implicitly uses the OpenAPI router via the `OpenAPIConfig` object. This was caused by the two different handlers being given the same name as defined in ``litestar.constants``. PR adds a distinct name for use by the handler that serves ``openapi.json`` on the controller. .. change:: pydantic v2 import tests for pydantic v1.10.15 :type: bugfix :pr: 3347 :issue: 3348 Fixes bug with Pydantic V1 environment test where the test was run against v2. Adds assertion for version to the test. Fixes a bug exposed by above that relied on pydantic not having ``v1`` in the package namespace if ``v1`` is installed. This doesn't hold true after pydantic's ``1.10.15`` release. .. change:: schema for generic wrapped return types with DTO :type: bugfix :pr: 3371 :issue: 2929 Fix schema generated for DTOs where the supported type is wrapped in a generic outer type. Prior behavior of using the ``backend.annotation`` as the basis for generating the openapi schema for the represented type is not applicable for the case where the DTO supported type is wrapped in a generic outer object. In that case ``backend.annotation`` only represents the type of the attribute on the generic type that holds the DTO supported type annotation. This change detects the case where we unwrap an outer generic type, and rebuilds the generic annotation in a manner appropriate for schema generation, before generating the schema for the annotation. It does this by substituting the DTOs transfer model for the original model in the original annotations type arguments. .. change:: Ambiguous default warning for no signature default :type: bugfix :pr: 3378 :issue: 3372 We now only issue a single warning for the case where a default value is supplied via ``Parameter()`` and not via a regular signature default. .. change:: Path param consumed by dependency treated as unconsumed :type: bugfix :pr: 3380 :issue: 3369 Consider parameters defined in handler dependencies in order to determine if a path parameter has been consumed for openapi generation purposes. Fixes an issue where path parameters not consumed by the handler, but consumed by dependencies would cause an :exc`ImproperlyConfiguredException`. .. change:: "name" and "in" should not be included in openapi headers :type: bugfix :pr: 3417 :issue: 3416 Exclude the "name" and "in" fields from openapi schema generated for headers. Add ``BaseSchemaObject._iter_fields()`` method that allows schema types to define the fields that should be included in their openapi schema representation and override that method for ``OpenAPIHeader``. .. change:: top-level import of optional package :type: bugfix :pr: 3418 :issue: 3415 Fix import from ``contrib.minijinja`` without handling for case where dependency is not installed. .. change:: regular handler under mounted app :type: bugfix :pr: 3430 :issue: 3429 Fix an issue where a regular handler under a mounted asgi app would prevent a request from routing through the mounted application if the request path contained the path of the regular handler as a substring. .. change:: logging to file with structlog :type: bugfix :pr: 3425 Fix and issue with converting ``StructLoggingConfig`` to dict during call to ``configure()`` when the config object has a custom logger factory that references a ``TextIO`` object, which cannot be pickled. .. change:: clear session cookie if new session exceeds ``CHUNK_SIZE`` :type: bugfix :pr: 3446 :issue: 3441 Fix an issue where the connection session cookie is not cleared if the response session is stored across multiple cookies. .. change:: flash messages were not displayed on Redirect :type: bugfix :pr: 3420 :issue: 3325 Fix an issue where flashed messages were not shown after a redirect .. change:: Validation of optional sequence in multipart data with one value :type: bugfix :pr: 3408 :issue: 3407 A ``Sequence[UploadFile] | None`` would not pass validation when a single value was provided for a structured type, e.g. dataclass. .. change:: field not optional if default value :type: bugfix :pr: 3476 :issue: 3471 Fix issue where a pydantic v1 field annotation is wrapped with ``Optional`` if it is marked not required, but has a default value. .. change:: prevent starting multiple responses :type: bugfix :pr: 3479 Prevent the app's exception handler middleware from starting a response after one has already started. When something in the middleware stack raises an exception after a "http.response.start" message has already been sent, we end up with long exception chains that obfuscate the original exception. This change implements tracking of when a response has started, and if so, we immediately raise the exception instead of sending it through the usual exception handling code path. .. change:: logging middleware with multi-body response :type: bugfix :pr: 3478 :issue: 3477 Prevent logging middleware from failing with a :exc:`KeyError` when a response sends multiple "http.response.body" messages. .. change:: handle dto type nested in mapping :type: bugfix :pr: 3486 :issue: 3463 Added handling for transferring data from a transfer model, to a DTO supported instance when the DTO supported type is nested in a mapping. I.e, handles this case: .. code-block:: python @dataclass class NestedDC: a: int b: str @dataclass class DC: nested_mapping: Dict[str, NestedDC] .. change:: examples omitted in schema produced by dto :type: bugfix :pr: 3510 :issue: 3505 Fixes issue where a ``BodyKwarg`` instance provided as metadata to a data type annotation was ignored for OpenAPI schema generation when the data type is managed by a DTO. .. change:: fix handling validation of subscribed generics :type: bugfix :pr: 3519 Fix a bug that would lead to a :exc:`TypeError` when subscribed generics were used in a route handler signature and subject to validation. .. code-block:: python from typing import Generic, TypeVar from litestar import get from litestar.testing import create_test_client T = TypeVar("T") class Foo(Generic[T]): pass async def provide_foo() -> Foo[str]: return Foo() @get("/", dependencies={"foo": provide_foo}) async def something(foo: Foo[str]) -> None: return None with create_test_client([something]) as client: client.get("/") .. change:: exclude static file from schema :type: bugfix :pr: 3509 :issue: 3374 Exclude static file routes created with ``create_static_files_router`` from the OpenAPI schema by default .. change:: use re.match instead of re.search for mounted app path (#3501) :type: bugfix :pr: 3511 :issue: 3501 When mounting an app, path resolution uses ``re.search`` instead or ``re.match``, thus mounted app matches any path which contains mount path. .. change:: do not log exceptions twice, deprecate ``traceback_line_limit`` and fix ``pretty_print_tty`` :type: bugfix :pr: 3507 :issue: 3228 * The wording of the log message, when logging an exception, has been updated. * For structlog, the ``traceback`` field in the log message (which contained a truncated stacktrace) has been removed. The ``exception`` field is still around and contains the full stacktrace. * The option ``traceback_line_limit`` has been deprecated. The value is now ignored, the full stacktrace will be logged. .. change:: YAML schema dump :type: bugfix :pr: 3537 Fix an issue in the OpenAPI YAML schema dump logic of ``OpenAPIController`` where the endpoint for the OpenAPI YAML schema file returns an empty response if a request has been made to the OpenAPI JSON schema previously due to an incorrect variable check. .. change:: Add async ``websocket_connect`` to ``AsyncTestClient`` :type: feature :pr: 3328 :issue: 3133 Add async ``websocket_connect`` to ``AsyncTestClient`` .. change:: add ``SecretString`` and ``SecretBytes`` datastructures :type: feature :pr: 3322 :issue: 1312, 3248 Implement ``SecretString`` and ``SecretBytes`` data structures to hide sensitive data in tracebacks, etc. .. change:: Deprecate subclassing route handler decorators :type: feature :pr: 3439 Deprecation for the 2.x release line of the semantic route handler classes removed in #3436. .. changelog:: 2.8.3 :date: 2024-05-06 .. change:: Fix improper limitation of a pathname to a restricted directory :type: bugfix Fix a path traversal vulnerability disclosed in https://github.com/litestar-org/litestar/security/advisories/GHSA-83pv-qr33-2vcf .. change:: Remove use of asserts for control flow. :type: bugfix :pr: 3359 :issue: 3354 #3347 introduced a new pattern to differentiate between Pydantic v1 and v2 installs, however it relies on using `assert` which is an issue as can optimised away. This PR changes the approach to manually throw an `ImportError` instead. .. change:: schema for generic wrapped return types with DTO :type: bugfix :pr: 3371 :issue: 2929 Fix schema generated for DTOs where the supported type is wrapped in a generic outer type. .. change:: Ambiguous default warning for no signature default :type: bugfix :pr: 3378 :issue: 3372 We now only issue a single warning for the case where a default value is supplied via `Parameter()` and not via a regular signature default. .. change:: Path param consumed by dependency treated as unconsumed :type: bugfix :pr: 3380 :issue: 3369 Consider parameters defined in handler dependencies in order to determine if a path parameter has been consumed for openapi generation purposes. Fixes an issue where path parameters not consumed by the handler, but consumed by dependencies would cause an `ImproperlyConfiguredException`. .. change:: Solve a caching issue in `CacheControlHeader` :type: bugfix :pr: 3383 Fixes an issue causing return of invalid values from cache. .. change:: "name" and "in" should not be included in openapi headers :type: bugfix :pr: 3417 :issue: 3416 Exclude the "name" and "in" fields from openapi schema generated for headers. .. change:: top-level import of optional package :type: bugfix :pr: 3418 :issue: 3415 Fix import from `contrib.minijinja` without handling for case where dependency is not installed. .. change:: regular handler under mounted app :type: bugfix :pr: 3430 :issue: 3429 Fix an issue where a regular handler under a mounted asgi app would prevent a request from routing through the mounted application if the request path contained the path of the regular handler as a substring. .. change:: logging to file with structlog :type: bugfix :pr: 3425 PR fixes issue with converting `StructLoggingConfig` to dict during call to `configure()` when the config object has a custom logger factory that references a `TextIO` object, which cannot be pickled. .. change:: clear session cookie if new session gt CHUNK_SIZE :type: bugfix :pr: 3446 :issue: 3441 Fix an issue where the connection session cookie is not cleared if the response session is stored across multiple cookies. .. change:: flash messages were not displayed on Redirect :type: bugfix :pr: 3420 :issue: 3325 Fixes issue where flash messages were not displayed on redirect. .. change:: Validation of optional sequence in multipart data with one value :type: bugfix :pr: 3408 :issue: 3407 A `Sequence[UploadFile] | None` would not pass validation when a single value was provided for a structured type, e.g. dataclass. .. changelog:: 2.8.2 :date: 2024-04-09 .. change:: pydantic v2 import tests for pydantic v1.10.15 :type: bugfix :pr: 3347 :issue: 3348 Fixes bug with Pydantic v1 environment test causing the test to run against v2. Adds assertion for version to the test. Fixes a bug exposed by above that relied on Pydantic not having `v1` in the package namespace if `v1` is installed. This doesn't hold true after Pydantic's `1.10.15` release. Moves application environment tests from the release job into the normal CI run. .. changelog:: 2.8.1 :date: 2024-04-08 .. change:: ASGI lifespan msg after lifespan context exception :type: bugfix :pr: 3315 An exception raised within an asgi lifespan context manager would result in a "lifespan.startup.failed" message This PR modifies `ASGIRouter.lifespan()` so that it sends a shutdown failure message if we've already confirmed startup. .. change:: Fix when pydantic==1.10 is installed :type: bugfix :pr: 3335 :issue: 3334 This PR fixes a bug introduced in #3296 where it failed to take into account that the `pydantic_v2` variable could be `Empty`. .. change:: OpenAPI router and controller on same app. :type: bugfix :pr: 3338 :issue: 3337 Fixes an `ImproperlyConfiguredException` where an app that explicitly registers an `OpenAPIController` on the application, and implicitly uses the OpenAPI router via the `OpenAPIConfig` object. This was caused by the two different handlers being given the same name as defined in `litestar.constants`. PR adds a distinct name for use by the handler that serves `openapi.json` on the controller. .. changelog:: 2.8.0 :date: 2024-04-05 .. change:: Unique schema names for nested models (#3134) :type: bugfix :pr: 3136 :issue: 3134 Fixes an issue where nested models beyond the ``max_nested_depth`` would not have unique schema names in the OpenAPI documentation. The fix appends the nested model's name to the ``unique_name`` to differentiate it from the parent model. .. change:: Add ``path`` parameter to Litestar application class :type: feature :pr: 3314 Exposes :paramref:`~.app.Litestar.parameter` at :class:`~.app.Litestar` application class level .. change:: Remove duplicate ``rich-click`` config options :type: bugfix :pr: 3274 Removes duplicate config options from click cli .. change:: Fix Pydantic ``json_schema_extra`` examples. :type: bugfix :pr: 3281 :issue: 3277 Fixes a regression introduced in ``2.7.0`` where an example for a field provided in Pydantic's ``Field.json_schema_extra`` would cause an error. .. change:: Set default on schema from :class:`~.typing.FieldDefinition` :type: bugfix :pr: 3280 :issue: 3278 Consider the following: .. code-block:: python def get_foo(foo_id: int = 10) -> None: ... In such cases, no :class:`~.params.KwargDefinition` is created since there is no metadata provided via ``Annotated``. The default is still parsed, and set on the generated ``FieldDefinition``, however the ``SchemaCreator`` currently only considers defaults that are set on ``KwargDefinition``. So in such cases, we should fallback to the default set on the ``FieldDefinition`` if there is a valid default value. .. change:: Custom types cause serialisation error in exception response with non-JSON media-type :type: bugfix :pr: 3284 :issue: 3192 Fixes a bug when using a non-JSON media type (e.g., ``text/plain``), :class:`~.exceptions.http_exceptions.ValidationException`'s would not get serialized properly because they would ignore custom ``type_encoders``. .. change:: Ensure default values are always represented in schema for dataclasses and :class:`msgspec.Struct`\ s :type: bugfix :pr: 3285 :issue: 3201 Fixes a bug that would prevent default values for dataclasses and ``msgspec.Struct`` s to be included in the OpenAPI schema. .. change:: Pydantic v2 error handling/serialization when for non-Pydantic exceptions :type: bugfix :pr: 3286 :issue: 2365 Fixes a bug that would cause a :exc:`TypeError` when non-Pydantic errors are raised during Pydantic's validation process while using DTOs. .. change:: Fix OpenAPI schema generation for paths with path parameters of different types on the same path :type: bugfix :pr: 3293 :issue: 2700 Fixes a bug that would cause no OpenAPI schema to be generated for paths with path parameters that only differ on the path parameter type, such as ``/{param:int}`` and ``/{param:str}``. This was caused by an internal representation issue in Litestar's routing system. .. change:: Document unconsumed path parameters :type: bugfix :pr: 3295 :issue: 3290 Fixes a bug where path parameters not consumed by route handlers would not be included in the OpenAPI schema. This could/would not include the ``{param}`` in the schema, yet it is still required to be passed when calling the path. .. change:: Allow for console output to be silenced :type: feature :pr: 3180 Introduces optional environment variables that allow customizing the "Application" name displayed in the console output and suppressing the initial ``from_env`` or the ``Rich`` info table at startup. Provides flexibility in tailoring the console output to better integrate Litestar into larger applications or CLIs. .. change:: Add flash plugin :type: feature :pr: 3145 :issue: 1455 Adds a flash plugin akin to Django or Flask that uses the request state .. change:: Use memoized :paramref:`~.handlers.HTTPRouteHandler.request_class` and :paramref:`~.handlers.HTTPRouteHandler.response_class` values :type: feature :pr: 3205 Uses memoized ``request_class`` and ``response_class`` values .. change:: Enable codegen backend by default :type: feature :pr: 3215 Enables the codegen backend for DTOs introduced in https://github.com/litestar-org/litestar/pull/2388 by default. .. change:: Added precedence of CLI parameters over envs :type: feature :pr: 3190 :issue: 3188 Adds precedence of CLI parameters over environment variables. Before this change, environment variables would take precedence over CLI parameters. Since CLI parameters are more explicit and are set by the user, they should take precedence over environment variables. .. change:: Only print when terminal is ``TTY`` enabled :type: feature :pr: 3219 Sets ``LITESTAR_QUIET_CONSOLE`` and ``LITESTAR_APP_NAME`` in the autodiscovery function. Also prevents the tabular console output from printing when the terminal is not ``TTY`` .. change:: Support ``schema_extra`` in :class:`~.openapi.spec.parameter.Parameter` and `Body` :type: feature :pr: 3204 Introduces a way to modify the generated OpenAPI spec by adding a ``schema_extra`` parameter to the Parameter and Body classes. The ``schema_extra`` parameter accepts a ``dict[str, Any]`` where the keys correspond to the keyword parameter names in Schema, and the values are used to override items in the generated Schema object. Provides a convenient way to customize the OpenAPI documentation for inbound parameters. .. change:: Add :class:`typing.TypeVar` expansion :type: feature :pr: 3242 Adds a method for TypeVar expansion on registration This allows the use of generic route handler and generic controller without relying on forward references. .. change:: Add ``LITESTAR_`` prefix before ``WEB_CONCURRENCY`` env option :type: feature :pr: 3227 Adds ``LITESTAR_`` prefix before the ``WEB_CONCURRENCY`` environment option .. change:: Warn about ambiguous default values in parameter specifications :type: feature :pr: 3283 As discussed in https://github.com/litestar-org/litestar/pull/3280#issuecomment-2026878325, we want to warn about, and eventually disallow specifying parameter defaults in two places. To achieve this, 2 warnings are added: - A deprecation warning if a default is specified when using ``Annotated``: ``param: Annotated[int, Parameter(..., default=1)]`` instead of ``param: Annotated[int, Parameter(...)] = 1`` - An additional warning in the above case if two default values are specified which do not match in value: ``param: Annotated[int, Parameter(..., default=1)] = 2`` In a future version, the first one should result in an exception at startup, preventing both of these scenarios. .. change:: Support declaring :class:`~.dto.field.DTOField` via ``Annotated`` :type: feature :pr: 3289 :issue: 2351 Deprecates passing :class:`~.dto.field.DTOField` via ``[pydantic]`` extra. .. change:: Add "TRACE" to HttpMethod enum :type: feature :pr: 3294 Adds the ``TRACE`` HTTP method to :class:`~.enums.HttpMethod` enum .. change:: Pydantic DTO non-instantiable types :type: feature :pr: 3296 Simplifies the type that is applied to DTO transfer models for certain Pydantic field types. It addresses ``JsonValue``, ``EmailStr``, ``IPvAnyAddress``/``IPvAnyNetwork``/``IPvAnyInterface`` types by using appropriate :term:`type annotations ` on the transfer models to ensure compatibility with :doc:`msgspec:index` serialization and deserialization. .. changelog:: 2.7.1 :date: 2024-03-22 .. change:: replace TestClient.__enter__ return type with Self :type: bugfix :pr: 3194 ``TestClient.__enter__`` and ``AsyncTestClient.__enter__`` return ``Self``. If you inherit ``TestClient``, its ``__enter__`` method should return derived class's instance unless override the method. ``Self`` is a more flexible return type. .. change:: use the full path for fetching openapi.json :type: bugfix :pr: 3196 :issue: 3047 This specifies the ``spec-url`` and ``apiDescriptionUrl`` of Rapidoc, and Stoplight Elements as absolute paths relative to the root of the site. This ensures that both of the send the request for the JSON of the OpenAPI schema to the right endpoint. .. change:: JSON schema ``examples`` were OpenAPI formatted :type: bugfix :pr: 3224 :issue: 2849 The generated ``examples`` in *JSON schema* objects were formatted as: .. code-block:: json "examples": { "some-id": { "description": "Lorem ipsum", "value": "the real beef" } } However, above is OpenAPI example format, and must not be used in JSON schema objects. Schema objects follow different formatting: .. code-block:: json "examples": [ "the real beef" ] * Explained in `APIs You Won't Hate blog post `_. * `Schema objects spec `_ * `OpenAPI example format spec `_. This is referenced at least from parameters, media types and components. The technical change here is to define ``Schema.examples`` as ``list[Any]`` instead of ``list[Example]``. Examples can and must still be defined as ``list[Example]`` for OpenAPI objects (e.g. ``Parameter``, ``Body``) but for JSON schema ``examples`` the code now internally generates/converts ``list[Any]`` format instead. Extra confusion here comes from the OpenAPI 3.0 vs OpenAPI 3.1 difference. OpenAPI 3.0 only allowed ``example`` (singular) field in schema objects. OpenAPI 3.1 supports the full JSON schema 2020-12 spec and so ``examples`` array in schema objects. Both ``example`` and ``examples`` seem to be supported, though the former is marked as deprecated in the latest specs. This can be tested over at https://editor-next.swagger.io by loading up the OpenAPI 3.1 Pet store example. Then add ``examples`` in ``components.schemas.Pet`` using the both ways and see the Swagger UI only render the example once it's properly formatted (it ignores is otherwise). .. change:: queue_listener handler for Python >= 3.12 :type: bugfix :pr: 3185 :issue: 2954 - Fix the ``queue_listener`` handler for Python 3.12 Python 3.12 introduced a new way to configure ``QueueHandler`` and ``QueueListener`` via ``logging.config.dictConfig()``. As described in the `logging documentation `_. The listener still needs to be started & stopped, as previously. To do so, we've introduced ``LoggingQueueListener``. And as stated in the doc: * Any custom queue handler and listener classes will need to be defined with the same initialization signatures as `QueueHandler `_ and `QueueListener `_. .. change:: extend openapi meta collected from domain models :type: bugfix :pr: 3237 :issue: 3232 :class:`~litestar.typing.FieldDefinition` s pack any OpenAPI metadata onto a ``KwargDefinition`` instance when types are parsed from domain models. When we produce a DTO type, we transfer this meta from the `KwargDefinition` to a `msgspec.Meta` instance, however so far this has only included constraints, not attributes such as descriptions, examples and title. This change ensures that we transfer the openapi meta for the complete intersection of fields that exist on b oth `KwargDefinition` and `Meta`. .. change:: kwarg ambiguity exc msg for path params :type: bugfix :pr: 3261 Fixes the way we construct the exception message when there is a kwarg ambiguity detected for path parameters. .. changelog:: 2.7.0 :date: 2024-03-10 .. change:: missing cors headers in response :type: bugfix :pr: 3179 :issue: 3178 Set CORS Middleware headers as per spec. Addresses issues outlined on https://github.com/litestar-org/litestar/issues/3178 .. change:: sending empty data in sse in js client :type: bugfix :pr: 3176 Fix an issue with SSE where JavaScript clients fail to receive an event without data. The `spec `_ is not clear in whether or not an event without data is ok. Considering the EventSource "client" is not ok with it, and that it's so easy DX-wise to make the mistake not explicitly sending it, this change fixes it by defaulting to the empty-string .. change:: Support ``ResponseSpec(..., examples=[...])`` :type: feature :pr: 3100 :issue: 3068 Allow defining custom examples for the responses via ``ResponseSpec``. The examples set this way are always generated locally, for each response: Examples that go within the schema definition cannot be set by this. .. code-block:: json { "paths": { "/": { "get": { "responses": { "200": { "content": { "application/json": { "schema": {}, "examples": "..."}} }} }} } } .. change:: support "+json"-suffixed response media types :type: feature :pr: 3096 :issue: 3088 Automatically encode responses with media type of the form ``application/+json`` as json. .. change:: Allow reusable ``Router`` instances :type: feature :pr: 3103 :issue: 3012 It was not possible to re-attach a router instance once it was attached. This makes that possible. The router instance now gets deepcopied when it's registered to another router. The application startup performance gets a hit here, but the same approach is already used for controllers and handlers, so this only harmonizes the implementation. .. change:: only display path in ``ValidationException``\ s :type: feature :pr: 3064 :issue: 3061 Fix an issue where ``ValidationException`` exposes the full URL in the error response, leaking internal IP(s) or other similar infra related information. .. change:: expose ``request_class`` to other layers :type: feature :pr: 3125 Expose ``request_class`` to other layers .. change:: expose ``websocket_class`` :type: feature :pr: 3152 Expose ``websocket_class`` to other layers .. change:: Add ``type_decoders`` to Router and route handlers :type: feature :pr: 3153 Add ``type_decoders`` to ``__init__`` method for handler, routers and decorators to keep consistency with ``type_encoders`` parameter .. change:: Pass ``type_decoders`` in ``WebsocketListenerRouteHandler`` :type: feature :pr: 3162 Pass ``type_decoders`` to parent's ``__init__`` in ``WebsocketListenerRouteHandler`` init, otherwise ``type_decoders`` will be ``None`` replace params order in docs, ``__init__`` (`decoders` before `encoders`) .. change:: 3116 enhancement session middleware :type: feature :pr: 3127 :issue: 3116 For server side sessions, the session id is now generated before the route handler. Thus, on first visit, a session id will be available inside the route handler's scope instead of afterwards A new abstract method ``get_session_id`` was added to ``BaseSessionBackend`` since this method will be called for both ClientSideSessions and ServerSideSessions. Only for ServerSideSessions it will return an actual id. Using ``request.set_session(...)`` will return the session id for ServerSideSessions and None for ClientSideSessions The session auth MiddlewareWrapper now refers to the Session Middleware via the configured backend, instead of it being hardcoded .. change:: make random seed for openapi example generation configurable :type: feature :pr: 3166 Allow random seed used for generating the examples in the OpenAPI schema (when ``create_examples`` is set to ``True``) to be configured by the user. This is related to https://github.com/litestar-org/litestar/issues/3059 however whether this change is enough to close that issue or not is not confirmed. .. change:: generate openapi components schemas in a deterministic order :type: feature :pr: 3172 Ensure that the insertion into the ``Components.schemas`` dictionary of the OpenAPI spec will be in alphabetical order (based on the normalized name of the ``Schema``). .. changelog:: 2.6.3 :date: 2024-03-04 .. change:: Pydantic V1 schema generation for PrivateAttr in GenericModel :type: bugfix :pr: 3161 :issue: 3150 Fixes a bug that caused a ``NameError`` when a Pydantic V1 ``GenericModel`` has a private attribute of which the type annotation cannot be resolved at the time of schema generation. .. changelog:: 2.6.2 :date: 2024/03/02 .. change:: DTO msgspec meta constraints not being included in transfer model :type: bugfix :pr: 3113 :issue: 3026 Fix an issue where msgspec constraints set in ``msgspec.Meta`` would not be honoured by the DTO. In the given example, the ``min_length=3`` constraint would be ignored by the model generated by ``MsgspecDTO``. .. code-block:: python from typing import Annotated import msgspec from litestar import post, Litestar from litestar.dto import MsgspecDTO class Request(msgspec.Struct): foo: Annotated[str, msgspec.Meta(min_length=3)] @post("/example/", dto=MsgspecDTO[Request]) async def example(data: Request) -> Request: return data Constraints like these are now transferred. Two things to note are: - For DTOs with ``DTOConfig(partial=True)`` we cannot transfer the length constraints as they are only supported on fields that as subtypes of ``str``, ``bytes`` or a collection type, but ``partial=True`` sets all fields as ``T | UNSET`` - For the ``PiccoloDTO``, fields which are not required will also drop the length constraints. A warning about this will be raised here. .. change:: Missing control header for static files :type: bugfix :pr: 3131 :issue: 3129 Fix an issue where a ``cache_control`` that is set on a router created by ``create_static_files_router`` wasn't passed to the generated handler .. change:: Fix OpenAPI schema generation for Pydantic v2 constrained ``Secret`` types :type: bugfix :pr: 3149 :issue: 3148 Fix schema generation for ``pydantic.SecretStr`` and ``pydantic.SecretBytes`` which, when constrained, would not be recognised as such with Pydantic V2 since they're not subtypes of their respective bases anymore. .. change:: Fix OpenAPI schema generation for Pydantic private attributes :type: bugfix :pr: 3151 :issue: 3150 Fix a bug that caused a :exc:`NameError` when trying to resolve forward references in Pydantic private fields. Although private fields were respected excluded from the schema, it was still attempted to extract their type annotation. This was fixed by not relying on ``typing.get_type_hints`` to get the type information, but instead using Pydantic's own APIs, allowing us to only extract information about the types of relevant fields. .. change:: OpenAPI description not set for UUID based path parameters in OpenAPI :type: bugfix :pr: 3118 :issue: 2967 Resolved a bug where the description was not set for UUID-based path parameters in OpenAPI due to the reason mentioned in the issue. .. change:: Fix ``RedisStore`` client created with ``with_client`` unclosed :type: bugfix :pr: 3111 :issue: 3083 Fix a bug where, when a :class:`~litestar.stores.redis.RedisStore` was created with the :meth:`~litestar.stores.redis.RedisStore.with_client` method, that client wasn't closed explicitly .. changelog:: 2.6.1 :date: 2024/02/14 .. change:: SQLAlchemy: Use `IntegrityError` instead of deprecated `ConflictError` :type: bugfix :pr: 3094 Updated the repository to return ``IntegrityError`` instead of the now deprecated ``ConflictError`` .. change:: Remove usage of deprecated `static_files` property :type: bugfix :pr: 3087 Remove the usage of the deprecated ``Litestar.static_files_config`` in ``Litestar.__init__``. .. change:: Sessions: Fix cookie naming for short cookies :type: bugfix :pr: 3095 :issue: 3090 Previously, cookie names always had a suffix of the form ``"-{i}"`` appended to them. With this change, the suffix is omitted if the cookie is short enough (< 4 KB) to not be split into multiple chunks. .. change:: Static files: Fix path resolution for windows :type: bugfix :pr: 3102 Fix an issue with the path resolution on Windows introduced in https://github.com/litestar-org/litestar/pull/2960 that would lead to 404s .. change:: Fix logging middleware with structlog causes application to return a ``500`` when request body is malformed :type: bugfix :pr: 3109 :issue: 3063 Gracefully handle malformed request bodies during parsing when using structlog; Instead of erroring out and returning a ``500``, the raw body is now being used when an error occurs during parsing .. change:: OpenAPI: Generate correct response schema for ``ResponseSpec(None)`` :type: bugfix :pr: 3098 :issue: 3069 Explicitly declaring ``responses={...: ResponseSpec(None)}`` used to generate OpenAPI a ``content`` property, when it should be omitted. .. change:: Prevent exception handlers from extracting details from non-Litestar exceptions :type: bugfix :pr: 3106 :issue: 3082 Fix a bug where exception classes that had a ``status_code`` attribute would be treated as Litestar exceptions and details from them would be extracted and added to the exception response. .. changelog:: 2.6.0 :date: 2024/02/06 .. change:: Enable disabling configuring ``root`` logger within ``LoggingConfig`` :type: feature :pr: 2969 The option :attr:`~litestar.logging.config.LoggingConfig.configure_root_logger` was added to :class:`~litestar.logging.config.LoggingConfig` attribute. It is enabled by default to not implement a breaking change. When set to ``False`` the ``root`` logger will not be modified for ``logging`` or ``picologging`` loggers. .. change:: Simplified static file handling and enhancements :type: feature :pr: 2960 :issue: 2629 Static file serving has been implemented with regular route handlers instead of a specialised ASGI app. At the moment, this is complementary to the usage of :class:`~litestar.static_files.StaticFilesConfig` to maintain backwards compatibility. This achieves a few things: - Fixes https://github.com/litestar-org/litestar/issues/2629 - Circumvents special casing needed in the routing logic for the static files app - Removes the need for a ``static_files_config`` attribute on the app - Removes the need for a special :meth:`~litestar.app.Litestar.url_for_static_asset` method on the app since `route_reverse` can be used instead Additionally: - Most router options can now be passed to the :func:`~litestar.static_files.create_static_files_router`, allowing further customisation - A new ``resolve_symlinks`` flag has been added, defaulting to ``True`` to keep backwards compatibility **Usage** Instead of .. code-block:: python app = Litestar( static_files_config=[StaticFilesConfig(path="/static", directories=["some_dir"])] ) You can now simply use .. code-block:: python app = Litestar( route_handlers=[ create_static_files_router(path="/static", directories=["some_dir"]) ] ) .. seealso:: :doc:`/usage/static-files` .. change:: Exclude Piccolo ORM columns with ``secret=True`` from ``PydanticDTO`` output :type: feature :pr: 3030 For Piccolo columns with ``secret=True`` set, corresponding ``PydanticDTO`` attributes will be marked as ``WRITE_ONLY`` to prevent the column being included in ``return_dto`` .. change:: Allow discovering registered plugins by their fully qualified name :type: feature :pr: 3027 `PluginRegistryPluginRegistry`` now supports retrieving a plugin by its fully qualified name. .. change:: Support externally typed classes as dependency providers :type: feature :pr: 3066 :issue: 2979 - Implement a new :class:`~litestar.plugins.DIPlugin` class that allows the generation of signatures for arbitrary types where their signature cannot be extracted from the type's ``__init__`` method - Implement ``DIPlugin``\ s for Pydantic and Msgspec to allow using their respective modelled types as dependency providers. These plugins will be registered by default .. change:: Add structlog plugin :type: feature :pr: 2943 A Structlog plugin to make it easier to configure structlog in a single place. The plugin: - Detects if a logger has ``setLevel`` before calling - Set even message name to be init-cap - Add ``set_level`` interface to config - Allows structlog printer to detect if console is TTY enabled. If so, a Struglog color formatter with Rich traceback printer is used - Auto-configures stdlib logger to use the structlog logger .. change:: Add reload-include and reload-exclude to CLI run command :type: feature :pr: 2973 :issue: 2875 The options ``reload-exclude`` and ``reload-include`` were added to the CLI ``run`` command to explicitly in-/exclude specific paths from the reloading watcher. .. changelog:: 2.5.5 :date: 2024/02/04 .. change:: Fix scope ``state`` key handling :type: bugfix :pr: 3070 Fix a regression introduced in #2751 that would wrongfully assume the ``state`` key is always present within the ASGI Scope. This is *only* the case when the Litestar root application is invoked first, since we enforce such a key there, but the presence of that key is not actually guaranteed by the ASGI spec and some servers, such as hypercorn, do not provide it. .. changelog:: 2.5.4 :date: 2024/01/31 .. change:: Handle ``KeyError`` when `root_path` is not present in ASGI scope :type: bugfix :pr: 3051 Nginx Unit ASGI server does not set "root_path" in the ASGI scope, which is expected as part of the changes done in #3039. This PR fixes the assumption that the key is always present and instead tries to optionally retrieve it. .. code-block:: KeyError on GET / 'root_path' .. change:: ServerSentEvent typing error :type: bugfix :pr: 3048 fixes small typing error: .. code-block:: error: Argument 1 to "ServerSentEvent" has incompatible type "AsyncIterable[ServerSentEventMessage]"; expected "str | bytes | Iterable[str | bytes] | Iterator[str | bytes] | AsyncIterable[str | bytes] | AsyncIterator[str | bytes]" [arg-type] inside ``test_sse`` there was a ``Any`` I changed to trigger the test then solved it. .. changelog:: 2.5.3 :date: 2024/01/29 .. change:: Handle diverging ASGI ``root_path`` behaviour :type: bugfix :pr: 3039 :issue: 3041 Uvicorn `0.26.0 `_ introduced a breaking change in its handling of the ASGI ``root_path`` behaviour, which, while adhering to the spec, diverges from the interpretation of other ASGI servers of this aspect of the spec (e.g. hypercorn and daphne do not follow uvicorn's interpretation as of today). A fix was introduced that ensures consistent behaviour of applications in any case. .. changelog:: 2.5.2 :date: 2024/01/27 .. change:: Ensure ``MultiDict`` and ``ImmutableMultiDict`` copy methods return the instance's type :type: bugfix :pr: 3009 :issue: 2549 Ensure :class:`~litestar.datastructures.MultiDict` and :class:`~litestar.datastructures.ImmutableMultiDict` copy methods return a new instance of ``MultiDict`` and ``ImmutableMultiDict``. Previously, these would return a :class:`multidict.MultiDict` instance. .. change:: Ensure ``exceptiongroup`` is installed on Python 3.11 :type: bugfix :pr: 3035 :issue: 3029 Add the `exceptiongroup `_ package as a required dependency on Python ``<3.11`` (previously ``<3.10``) as a backport of `Exception Groups `_ .. changelog:: 2.5.1 :date: 2024/01/18 .. change:: Fix OpenAPI schema generation for Union of multiple ``msgspec.Struct``\ s and ``None`` :type: bugfix :pr: 2982 :issue: 2971 The following code would raise a :exc:`TypeError` .. code-block:: python import msgspec from litestar import get from litestar.testing import create_test_client class StructA(msgspec.Struct): pass class StructB(msgspec.Struct): pass @get("/") async def handler() -> StructA | StructB | None: return StructA() .. change:: Fix misleading error message for missing dependencies provide by a package extra :type: bugfix :pr: 2921 Ensure that :exc:`MissingDependencyException` includes the correct name of the package to install if the package name differs from the Litestar package extra. (e.g. ``pip install litestar[jinja]`` vs ``pip install jinja2``). Previously the exception assumed the same name for both the package and package-extra name. .. change:: Fix OpenAPI schema file upload schema types for swagger :type: bugfix :pr: 2745 :issue: 2628 - Always set ``format`` as ``binary`` - Fix schema for swagger with multiple files, which requires the type of the request body schema to be ``object`` with ``properties`` instead of a schema of type ``array`` and ``items``. .. changelog:: 2.5.0 :date: 2024/01/06 .. change:: Fix serialization of custom types in exception responses :type: bugfix :issue: 2867 :pr: 2941 Fix a bug that would lead to a :exc:`SerializationException` when custom types were present in an exception response handled by the built-in exception handlers. .. code-block:: python class Foo: pass @get() def handler() -> None: raise ValidationException(extra={"foo": Foo("bar")}) app = Litestar(route_handlers=[handler], type_encoders={Foo: lambda foo: "foo"}) The cause was that, in examples like the one shown above, ``type_encoders`` were not resolved properly from all layers by the exception handling middleware, causing the serializer to throw an exception for an unknown type. .. change:: Fix SSE reverting to default ``event_type`` after 1st message :type: bugfix :pr: 2888 :issue: 2877 The ``event_type`` set within an SSE returned from a handler would revert back to a default after the first message sent: .. code-block:: python @get("/stream") async def stream(self) -> ServerSentEvent: async def gen() -> AsyncGenerator[str, None]: c = 0 while True: yield f"
{c}
\n" c += 1 return ServerSentEvent(gen(), event_type="my_event") In this example, the event type would only be ``my_event`` for the first message, and fall back to a default afterwards. The implementation has been fixed and will now continue sending the set event type for all messages. .. change:: Correctly handle single file upload validation when multiple files are specified :type: bugfix :pr: 2950 :issue: 2939 Uploading a single file when the validation target allowed multiple would cause a :exc:`ValidationException`: .. code-block:: python class FileUpload(Struct): files: list[UploadFile] @post(path="/") async def upload_files_object( data: Annotated[FileUpload, Body(media_type=RequestEncodingType.MULTI_PART)] ) -> list[str]: pass This could would only allow for 2 or more files to be sent, and otherwise throw an exception. .. change:: Fix trailing messages after unsubscribe in channels :type: bugfix :pr: 2894 Fix a bug that would allow some channels backend to receive messages from a channel it just unsubscribed from, for a short period of time, due to how the different brokers handle unsubscribes. .. code-block:: python await backend.subscribe(["foo", "bar"]) # subscribe to two channels await backend.publish( b"something", ["foo"] ) # publish a message to a channel we're subscribed to # start the stream after publishing. Depending on the backend # the previously published message might be in the stream event_generator = backend.stream_events() # unsubscribe from the channel we previously published to await backend.unsubscribe(["foo"]) # this should block, as we expect messages from channels # we unsubscribed from to not appear in the stream anymore print(anext(event_generator)) Backends affected by this were in-memory, Redis PubSub and asyncpg. The Redis stream and psycopg backends were not affected. .. change:: Postgres channels backends :type: feature :pr: 2803 Two new channel backends were added to bring Postgres support: :class:`~litestar.channels.backends.asyncpg.AsyncPgChannelsBackend`, using the `asyncpg `_ driver and :class:`~litestar.channels.backends.psycopg.PsycoPgChannelsBackend` using the `psycopg3 `_ async driver. .. seealso:: :doc:`/usage/channels` .. change:: Add ``--schema`` and ``--exclude`` option to ``litestar route`` CLI command :type: feature :pr: 2886 Two new options were added to the ``litestar route`` CLI command: - ``--schema``, to include the routes serving OpenAPI schema and docs - ``--exclude`` to exclude routes matching a specified pattern .. seealso:: Read more in the CLI :doc:`/reference/cli` section. .. change:: Improve performance of threaded synchronous execution :type: misc :pr: 2937 Performance of threaded synchronous code was improved by using the async library's native threading helpers instead of anyio. On asyncio, :meth:`asyncio.loop.run_in_executor` is now used and on trio :func:`trio.to_thread.run_sync`. Beneficiaries of these performance improvements are: - Synchronous route handlers making use of ``sync_to_thread=True`` - Synchronous dependency providers making use of ``sync_to_thread=True`` - Synchronous SSE generators - :class:`~litestar.stores.file.FileStore` - Large file uploads where the ``max_spool_size`` is exceeded and the spooled temporary file has been rolled to disk - :class:`~litestar.response.file.File` and :class:`~litestar.response.file.ASGIFileResponse` .. changelog:: 2.4.5 :date: 2023/12/23 .. change:: Fix validation of empty payload data with default values :type: bugfix :issue: 2902 :pr: 2903 Prior to this fix, a handler like: .. code-block:: python @post(path="/", sync_to_thread=False) def test(data: str = "abc") -> dict: return {"foo": data} ``$ curl localhost:8000 -X POST`` would return a client error like: .. code-block:: bash {"status_code":400,"detail":"Validation failed for POST http://localhost:8000/","extra":[{"message":"Expected `str`, got `null`","key":"data","source":"body"}]} .. change:: Support for returning ``Response[None]`` with a ``204`` status code from a handler :type: bugfix :pr: 2915 :issue: 2914 Returning a ``Response[None]`` from a route handler for a response with a ``204`` now works as expected without resulting in an :exc:`ImproperlyConfiguredException` .. change:: Fix error message of ``get_logger_placeholder()`` :type: bugfix :pr: 2919 Using a method on :attr:`Request.logger ` when not setting a ``logging_config`` on the application would result in a non-descriptive :exc:`TypeError`. An :exc:`ImproperlyConfiguredException` with an explanation is now raised instead. .. changelog:: 2.4.4 :date: 2023/12/13 .. change:: Support non-valid identifier as serialization target name :type: bugfix :pr: 2850 :issue: 2845 Fix a bug where DTOs would raise a ``TypeError: __slots__ must be identifiers`` during serialization, if a non-valid identifier (such as ``field-name``)was used for field renaming. .. change:: Fix regression signature validation for DTO validated types :type: bugfix :pr: 2854 :issue: 2149 Fix a regression introduced in ``2.0.0rc1`` that would cause data validated by the DTO to be validated again by the signature model. .. change:: Fix regression in OpenAPI schema key names :type: bugfix :pr: 2841 :issue: 2804 Fix a regression introduced in ``2.4.0`` regarding the naming of OpenAPI schema keys, in which a change was introduced to the way that keys for the OpenAPI components/schemas objects were calculated to address the possibility of name collisions. This behaviour was reverted for the case where a name has no collision, and now only introduces extended keys for the case where there are multiple objects with the same name, a case which would previously result in an exception. .. change:: Fix regression in OpenAPI handling of routes with multiple handlers :type: bugfix :pr: 2864 :issue: 2863 Fix a regression introduced in ``2.4.3`` causing two routes registered with the same path, but different methods to break OpenAPI schema generation due to both of them having the same value for operation ID. .. change:: Fix OpenAPI schema generation for recursive models :type: bugfix :pr: 2869 :issue: 2429 Fix an issue that would lead to a :exc:`RecursionError` when including nested models in the OpenAPI schema. .. changelog:: 2.4.3 :date: 2023/12/07 .. change:: Fix OpenAPI schema for ``Literal | None`` unions :type: bugfix :issue: 2812 :pr: 2818 Fix a bug where an incorrect OpenAPI schema was generated generated when any ``Literal | None``-union was present in an annotation. For example .. code-block:: python type: Literal["sink", "source"] | None would generate .. code-block:: json { "name": "type", "in": "query", "schema": { "type": "string", "enum": [ "sink", "source", null ] } } .. change:: Fix advanced-alchemy 0.6.0 compatibility issue with ``touch_updated_timestamp`` :type: bugfix :pr: 2843 Fix an incorrect import for ``touch_updated_timestamp`` of Advanced Alchemy, introduced in Advanced-Alchemy version 0.6.0. .. changelog:: 2.4.2 :date: 2023/12/02 .. change:: Fix OpenAPI handling of parameters with duplicated names :type: bugfix :issue: 2662 :pr: 2788 Fix a bug where schema generation would consider two parameters with the same name but declared in different places (eg., header, cookie) as an error. .. change:: Fix late failure where ``DTOData`` is used without a DTO :type: bugfix :issue: 2779 :pr: 2789 Fix an issue where a handler would be allowed to be registered with a ``DTOData`` annotation without having a DTO defined, which would result in a runtime exception. In cases like these, a configuration error is now raised during startup. .. change:: Correctly propagate camelCase names on OpenAPI schema :type: bugfix :pr: 2800 Fix a bug where OpenAPI schema fields would be inappropriately propagated as camelCase where they should have been snake_case .. change:: Fix error handling in event handler stream :type: bugfix :pr: 2810, 2814 Fix a class of errors that could result in the event listener stream being terminated when an exception occurred within an event listener. Errors in event listeners are now not propagated anymore but handled by the backend and logged instead. .. change:: Fix OpenAPI schema for Pydantic computed fields :type: bugfix :pr: 2797 :issue: 2792 Add support for including computed fields in schemas generated from Pydantic models. .. changelog:: 2.4.1 :date: 2023/11/28 .. change:: Fix circular import when importing from ``litestar.security.jwt`` :type: bugfix :pr: 2784 :issue: 2782 An :exc:`ImportError` was raised when trying to import from ``litestar.security.jwt``. This was fixed by removing the imports from the deprecated ``litestar.contrib.jwt`` within ``litesetar.security.jwt``. .. change:: Raise config error when generator dependencies are cached :type: bugfix :pr: 2780 :issue: 2771 Previously, an :exc:`InternalServerError` was raised when attempting to use `use_cache=True` with generator dependencies. This will now raise a configuration error during application startup. .. changelog:: 2.4.0 :date: 2023/11/27 .. change:: Fix ``HTTPException`` handling during concurrent dependency resolving :type: bugfix :pr: 2596 :issue: 2594 An issue was fixed that would lead to :exc:`HTTPExceptions` not being re-raised properly when they occurred within the resolution of nested dependencies during the request lifecycle. .. change:: Fix OpenAPI examples format :type: bugfix :pr: 2660 :issue: 2272 Fix the OpenAPI examples format by removing the wrapping object. Before the change, for a given model .. code-block:: python @dataclass class Foo: foo: int The following example would be generated: .. code-block:: json { "description": "Example value", "value": { "foo": 7906 } } After the fix, this is now: .. code-block:: json { "foo": 7906 } .. change:: Fix CLI plugin commands not showing up in command list :type: bugfix :pr: 2441 Fix a bug where commands registered by CLI plugins were available, but would not show up in the commands list .. change:: Fix missing ``write-only`` mark in ``dto_field()`` signature :type: bugfix :pr: 2684 Fix the missing ``write-only`` string literal in the ``mark`` parameter of :func:`~litestar.dto.field.dto_field` .. change:: Fix OpenAPI schemas incorrectly flagged as duplicates :type: bugfix :pr: 2475 :issue: 2471 Fix an issue that would lead to OpenAPI schemas being incorrectly considered duplicates, resulting in an :exc:`ImproperlyConfiguredException` being raised. .. change:: Fix Pydantic URL type support in OpenAPI and serialization :type: bugfix :pr: 2701 :issue: 2664 Add missing support for Pydantic's URL types (``AnyUrl`` and its descendants) for both serialization and OpenAPI schema generation. These types were only partially supported previously; Serialization support was lacking for v1 and v2, and OpenAPI support was missing for v2. .. change:: Fix incorrect ``ValidationException`` message when multiple errors were encountered :type: bugfix :pr: 2716 :issue: 2714 Fix a bug where :exc:`ValidationException` could contain duplicated messages in ``extra`` field, when multiple errors were encountered during validation .. change:: Fix DTO renaming renames all fields of the same name in nested DTOs :type: bugfix :pr: 2764 :issue: 2721 Fix an issue with nested field renaming in DTOs that would lead to all fields with a given name to be renamed in a nested structure. In the below example, both ``Foo.id`` and ``Bar.id`` would have been renamed to ``foo_id`` .. code-block:: python from dataclasses import dataclass @dataclass class Bar: id: str @dataclass class Foo: id: str bar: Bar FooDTO = DataclassDTO[Annotated[Foo, DTOConfig(rename_fields={"id": "foo_id"})]] .. change:: Fix handling of DTO objects nested in mappings :type: bugfix :pr: 2775 :issue: 2737 Fix a bug where DTOs nested in a :class:`~typing.Mapping` type would fail to serialize correctly. .. change:: Fix inconsistent sequence union parameter errors :type: bugfix :pr: 2776 :issue: 2600 Fix a bug where unions of collection types would result in different errors depending on whether the union included :obj:`None` or not. .. change:: Fix graceful handling of WebSocket disconnect in channels WebSockets handlers :type: bugfix :pr: 2691 Fix the behaviour of WebSocket disconnect handling within the WebSocket handlers provided by :doc:`channels
`, that would sometimes lead to a ``RuntimeError: Unexpected ASGI message 'websocket.close', after sending 'websocket.close'.`` exception being raised upon the closing of a WebSocket connection. .. change:: Add ``server_lifespan`` hook :type: feature :pr: 2658 A new ``server_lifespan`` hook is now available on :class:`~litestar.app.Litestar`. This hook works similar to the regular ``lifespan`` context manager, with the difference being is that it is only called once for the entire server lifespan, not for each application startup phase. Note that these only differ when running with an ASGI server that's using multiple worker processes. .. change:: Allow rendering templates directly from strings :type: feature :pr: 2689 :issue: 2687 A new ``template_string`` parameter was added to :class:`~litestar.template.Template`, allowing to render templates directly from strings. .. seealso:: :ref:`usage/templating:Template Files vs. Strings` .. change:: Support nested DTO field renaming :type: feature :pr: 2764 :issue: 2721 Using similar semantics as for exclusion/inclusion, nested DTO fields can now also be renamed: .. code-block:: python from dataclasses import dataclass @dataclass class Bar: id: str @dataclass class Foo: id: str bars: list[Bar] FooDTO = DataclassDTO[Annotated[Foo, DTOConfig(rename_fields={"bars.0.id": "bar_id"})]] .. changelog:: 2.3.2 :date: 2023/11/06 .. change:: Fix recursion error when re-using the path of a route handler for static files :type: bugfix :pr: 2630 :issue: 2629 A regression was fixed that would cause a recursion error when the path of a static files host was reused for a route handler with a different HTTP method. .. code-block:: python from litestar import Litestar from litestar import post from litestar.static_files import StaticFilesConfig @post("/uploads") async def handler() -> None: pass app = Litestar( [handler], static_files_config=[ StaticFilesConfig(directories=["uploads"], path="/uploads"), ], ) .. changelog:: 2.3.1 :date: 2023/11/04 .. change:: CLI: Fix not providing SSL certfiles breaks uvicorn command when using reload or multiple workers :type: bugfix :pr: 2616 :issue: 2613 Fix an issue where not providing the ``--ssl-certfile`` and ``--ssl-keyfile`` options to the ``litestar run`` command would cause a :exc:`FileNotFoundError` in uvicorn, when used together with the ``--reload``, ``--web-concurrency`` options. .. changelog:: 2.3.0 :date: 2023/11/02 .. change:: Python 3.12 support :type: feature :pr: 2396 :issue: 1862 Python 3.12 is now fully supported and tested. .. change:: New layered parameter ``signature_types`` :type: feature :pr: 2422 Types in this collection are added to ``signature_namespace`` using the type's ``__name__`` attribute. This provides a nicer interface when adding names to the signature namespace w ithout modifying the type name, e.g.: ``signature_namespace={"Model": Model}`` is equivalent to ``signature_types=[Model]``. The implementation makes it an error to supply a type in ``signature_types`` that has a value for ``__name__`` already in the signature namespace. It will also throw an error if an item in ``signature_types`` has no ``__name__`` attribute. .. change:: Added RapiDoc for OpenAPI schema visualisation :type: feature :pr: 2522 Add support for using `RapiDoc `_ for OpenAPI schema visualisation. .. change:: Support Pydantic 1 & 2 within the same application :type: feature :pr: 2487 Added support for Pydantic 1 & 2 within the same application by integrating with Pydantic's backwards compatibility layer: .. code-block:: python from litestar import get from pydantic.v1 import BaseModel as BaseModelV1 from pydantic import BaseModel class V1Foo(BaseModelV1): bar: str class V2Foo(BaseModel): bar: str @get("/1") def foo_v1(data: V1Foo) -> V1Foo: return data @get("/2") def foo_v2(data: V2Foo) -> V2Foo: return data .. change:: Add ``ResponseCacheConfig.cache_response_filter`` to allow filtering responses eligible for caching :type: feature :pr: 2537 :issue: 2501 ``ResponseCacheConfig.cache_response_filter`` is predicate called by the response cache middleware that discriminates whether a response should be cached, or not. .. change:: SSL support and self-signed certificates for CLI :type: feature :pr: 2554 :issue: 2335 Add support for SSL and generating self-signed certificates to the CLI. For this, three new arguments were added to the CLI's ``run`` command: - ``--ssl-certfile`` - ``--ssl-keyfile`` - ``--create-self-signed-cert`` The ``--ssl-certfile`` and `--ssl-keyfile` flags are passed to uvicorn when using ``litestar run``. Uvicorn requires both to be passed (or neither) but additional validation was added to generate a more user friendly CLI errors. The other SSL-related flags (like password or CA) were not added (yet). See `uvicorn CLI docs `_ **Generating of a self-signed certificate** One more CLI flag was added (``--create-devcert``) that uses the ``cryptography`` module to generate a self-signed development certificate. Both of the previous flags must be passed when using this flag. Then the following logic is used: - If both files already exists, they are used and nothing is generated - If neither file exists, the dev cert and key are generated - If only one file exists, it is ambiguous what to do so an exception is raised .. change:: Use custom request class when given during exception handling :type: bugfix :pr: 2444 :issue: 2399 When a custom ``request_class`` is provided, it will now be used while returning an error response .. change:: Fix missing OpenAPI schema for generic response type annotations :type: bugfix :pr: 2463 :issue: 2383 OpenAPI schemas are now correctly generated when a response type annotation contains a generic type such as .. code-block:: python from msgspec import Struct from litestar import Litestar, get, Response from typing import TypeVar, Generic, Optional T = TypeVar("T") class ResponseStruct(Struct, Generic[T]): code: int data: Optional[T] @get("/") def test_handler() -> Response[ResponseStruct[str]]: return Response( ResponseStruct(code=200, data="Hello World"), ) .. change:: Fix rendering of OpenAPI examples :type: bugfix :pr: 2509 :issue: 2494 An issue was fixed where OpenAPI examples would be rendered as .. code-block:: json { "parameters": [ { "schema": { "type": "string", "examples": [ { "summary": "example summary", "value": "example value" } ] } } ] } instead of .. code-block:: json { "parameters": [ { "schema": { "type": "string" }, "examples": { "example1": { "summary": "example summary" "value": "example value" } } } ] } .. change:: Fix non UTF-8 handling when logging requests :type: bugfix :issue: 2529 :pr: 2530 When structlog is not installed, the request body would not get parsed and shown as a byte sequence. Instead, it was serialized into a string with the assumption that it is valid UTF-8. This was fixed by decoding the bytes with ``backslashreplace`` before displaying them. .. change:: Fix ``ExceptionHandler`` typing to properly support ``Exception`` subclasses :type: bugfix :issue: 2520 :pr: 2533 Fix the typing for ``ExceptionHandler`` to support subclasses of ``Exception``, such that code like this will type check properly: .. code-block:: python from litestar import Litestar, Request, Response class CustomException(Exception): ... def handle_exc(req: Request, exc: CustomException) -> Response: ... .. change:: Fix OpenAPI schema generation for variable length tuples :type: bugfix :issue: 2460 :pr: 2552 Fix a bug where an annotation such as ``tuple[str, ...]`` would cause a ``TypeError: '<' not supported between instances of 'NoneType' and 'OpenAPIType')``. .. change:: Fix channels performance issue when polling with no subscribers in ``arbitrary_channels_allowed`` mode :type: bugfix :pr: 2547 Fix a bug that would cause high CPU loads while idling when using a ``ChannelsPlugin`` with the ``arbitrary_channels_allowed`` enabled and while no subscriptions for any channel were active. .. change:: Fix CLI schema export for non-serializable types when using ``create_examples=True`` :type: bugfix :pr: 2581 :issue: 2575 When trying to export a schema via the ``litestar schema openapi --output schema.json`` making use of a non-JSON serializable type, would result in an encoding error because the standard library JSON serializer was used. This has been fixed by using Litestar's own JSON encoder, enabling the serialization of all types supplied by the schema. .. change:: Fix OpenAPI schema generation for ``Literal`` and ``Enum`` unions with ``None`` :type: bugfix :pr: 2550 :issue: 2546 Existing behavior was to make the schema for every type that is a union with ``None`` a ``"one_of"`` schema, that includes ``OpenAPIType.NULL`` in the ``"one_of"`` types. When a ``Literal`` or ``Enum`` type is in a union with ``None``, this behavior is not desirable, as we want to have ``null`` available in the list of available options on the type's schema. This was fixed by modifying ``Literal`` and ``Enum`` schema generation so that i t can be identified that the types are in a union with ``None``, allowing ``null`` to be included in ``Schema.enum`` values. .. change:: Fix cache overrides when using same route with different handlers :type: bugfix :pr: 2592 :issue: 2573, 2588 A bug was fixed that would cause the cache for routes being overwritten by a route handler on that same route with a different HTTP method. .. changelog:: 2.2.0 :date: 2023/10/12 .. change:: Fix implicit conversion of objects to ``bool`` in debug response :type: bugfix :pr: 2384 :issue: 2381 The exception handler middleware would, when in debug mode, implicitly call an object's :meth:`__bool__ `, which would lead to errors if that object overloaded the operator, for example if the object in question was a SQLAlchemy element. .. change:: Correctly re-export filters and exceptions from ``advanced-alchemy`` :type: bugfix :pr: 2360 :issue: 2358 Some re-exports of filter and exception types from ``advanced-alchemy`` were missing, causing various issues when ``advanced-alchemy`` was installed, but Litestar would still use its own version of these classes. .. change:: Re-add ``create_engine`` method to SQLAlchemy configs :type: bugfix :pr: 2382 The ``create_engine`` method was removed in an ``advanced-alchemy`` releases. This was addresses by re-adding it to the versions provided by Litestar. .. change:: Fix ``before_request`` modifies route handler signature :type: bugfix :pr: 2391 :issue: 2368 The ``before_request`` would modify the return annotation of associated route handlers to conform with its own return type annotation, which would cause issues and unexpected behaviour when that annotation was not compatible with the original one. This was fixed by not having the ``before_request`` handler modify the route handler's signature. Users are now expected to ensure that values returned from a ``before_request`` handler conform to the return type annotation of the route handler. .. change:: Ensure compression is applied before caching when using compression middleware :type: bugfix :pr: 2393 :issue: 1301 A previous limitation was removed that would apply compression from the :class:`~litestar.middleware.compression.CompressionMiddleware` only *after* a response was restored from the cache, resulting in unnecessary repeated computation and increased size of the stored response. This was due to caching being handled on the response layer, where a response object would be pickled, restored upon a cache hit and then re-sent, including all middlewares. The new implementation now instead applies caching on the ASGI level; Individual messages sent to the ``send`` callable are cached, and later re-sent. This process ensures that the compression middleware has been applied before, and will be skipped when re-sending a cached response. In addition, this increases performance and reduces storage size even in cases where no compression is applied because the slow and inefficient pickle format can be avoided. .. change:: Fix implicit JSON parsing of URL encoded data :type: bugfix :pr: 2394 A process was removed where Litestar would implicitly attempt to parse parts of URL encoded data as JSON. This was originally added to provide some performance boosts when that data was in fact meant to be JSON, but turned out to be too fragile. Regular data conversion / validation is unaffected by this. .. change:: CLI enabled by default :type: feature :pr: 2346 :issue: 2318 The CLI and all its dependencies are now included by default, to enable a better and more consistent developer experience out of the box. The previous ``litestar[cli]`` extra is still available for backwards compatibility, but as of ``2.2.0`` it is without effect. .. change:: Customization of Pydantic integration via ``PydanticPlugin`` :type: feature :pr: 2404 :issue: 2373 A new :class:`~litestar.contrib.pydantic.PydanticPlugin` has been added, which can be used to configure Pydantic behaviour. Currently it supports setting a ``prefer_alias`` option, which will pass the ``by_alias=True`` flag to Pydantic when exporting models, as well as generate schemas accordingly. .. change:: Add ``/schema/openapi.yml`` to the available schema paths :type: feature :pr: 2411 The YAML version of the OpenAPI schema is now available under ``/schema/openapi.yml`` in addition to ``/schema/openapi.yaml``. .. change:: Add experimental DTO codegen backend :type: feature :pr: 2388 A new DTO backend was introduced which speeds up the transfer process by generating optimized Python code ahead of time. Testing shows that the new backend is between 2.5 and 5 times faster depending on the operation and data provided. The new backend can be enabled globally for all DTOs by passing the appropriate feature flag to the Litestar application: .. code-block:: python from litestar import Litestar from litestar.config.app import ExperimentalFeatures app = Litestar(experimental_features=[ExperimentalFeatures.DTO_CODEGEN]) .. seealso:: For more information see :ref:`usage/dto/0-basic-use:Improving performance with the codegen backend` .. change:: Improved error messages for missing required parameters :type: feature :pr: 2418 Error messages for missing required parameters will now also contain the source of the expected parameter: Before: .. code-block:: json { "status_code": 400, "detail": "Missing required parameter foo for url http://testerver.local" } After: .. code-block:: json { "status_code": 400, "detail": "Missing required header parameter 'foo' for url http://testerver.local" } .. changelog:: 2.1.1 :date: 2023/09/24 .. change:: Fix ``DeprecationWarning`` raised by ``Response.to_asgi_response`` :type: bugfix :pr: 2364 :meth:`~litestar.response.Response.to_asgi_response` was passing a non-:obj:`None` default value (``[]``) to ``ASGIResponse`` for ``encoded_headers``, resulting in a :exc:`DeprecationWarning` being raised. This was fixed by leaving the default value as :obj:`None`. .. changelog:: 2.1.0 :date: 2023/09/23 `View the full changelog `_ .. change:: Make ``302`` the default ``status_code`` for redirect responses :type: feature :pr: 2189 :issue: 2138 Make ``302`` the default ``status_code`` for redirect responses .. change:: Add :meth:`include_in_schema` option for all layers :type: feature :pr: 2295 :issue: 2267 Adds the :meth:`include_in_schema` option to all layers, allowing to include/exclude specific routes from the generated OpenAPI schema. .. change:: Deprecate parameter ``app`` of ``Response.to_asgi_response`` :type: feature :pr: 2268 :issue: 2217 Adds deprecation warning for unused ``app`` parameter of ``to_asgi_response`` as it is unused and redundant due to ``request.app`` being available. .. change:: Authentication: Add parameters to set the JWT ``extras`` field :type: feature :pr: 2313 Adds ``token_extras`` to both :func:`BaseJWTAuth.login` and :meth:`BaseJWTAuth.create_token` methods, to allow the definition of the ``extras`` JWT field. .. change:: Templating: Add possibility to customize Jinja environment :type: feature :pr: 2195 :issue: 965 Adds the ability to pass a custom Jinja2 ``Environment`` or Mako ``TemplateLookup`` by providing a dedicated class method. .. change:: Add support for `minjinja `_ :type: feature :pr: 2250 Adds support for MiniJinja, a minimal Jinja2 implementation. .. seealso:: :doc:`/usage/templating` .. change:: SQLAlchemy: Exclude implicit fields for SQLAlchemy DTO :type: feature :pr: 2170 :class:`SQLAlchemyDTO (Advanced Alchemy) ` can now be configured using a separate config object. This can be set using both class inheritance and `Annotated `_: .. code-block:: python :caption: :class:`SQLAlchemyDTO (Advanced Alchemy) ` can now be configured using a separate config object using ``config`` object. class MyModelDTO(SQLAlchemyDTO[MyModel]): config = SQLAlchemyDTOConfig() or .. code-block:: python :caption: :class:`SQLAlchemyDTO (Advanced Alchemy) ` can now be configured using a separate config object using ``Annotated``. MyModelDTO = SQLAlchemyDTO[Annotated[MyModel, SQLAlchemyDTOConfig()]] The new configuration currently accepts a single attribute which is ``include_implicit_fields`` that has a default value of ``True``. If set to to ``False``, all implicitly mapped columns will be hidden from the ``DTO``. If set to ``hybrid-only``, then hybrid properties will be shown but not other implicit columns. Finally, implicit columns that are marked with ``Mark.READ_ONLY`` or ``Mark.WRITE_ONLY`` will always be shown regardless of the value of ``include_implicit_fields``. .. change:: SQLAlchemy: Allow repository functions to be filtered by expressions :type: feature :pr: 2265 Enhances the SQLALchemy repository so that you can more easily pass in complex ``where`` expressions into the repository functions. .. tip:: Without this, you have to override the ``statement`` parameter and it separates the where conditions from the filters and the ``kwargs``. Allows usage of this syntax: .. code-block:: python locations, total_count = await model_service.list_and_count( ST_DWithin(UniqueLocation.location, geog, 1000), account_id=str(account_id) ) instead of the previous method of overriding the ``statement``: .. code-block:: python locations, total_count = await model_service.list_and_count( statement=select(Model).where(ST_DWithin(UniqueLocation.location, geog, 1000)), account_id=str(account_id), ) .. change:: SQLAlchemy: Use :func:`lambda_stmt ` in the repository :type: feature :pr: 2179 Converts the repository to use :func:`lambda_stmt ` instead of the normal ``select`` .. change:: SQLAlchemy: Swap to the `advanced_alchemy `_ implementations :type: feature :pr: 2312 Swaps the internal SQLAlchemy repository to use the external `advanced_alchemy `_ library implementations .. change:: Remove usages of deprecated ``ExceptionHandlerMiddleware`` ``debug`` parameter :type: bugfix :pr: 2192 Removes leftover usages of deprecated ``ExceptionHandlerMiddleware`` debug parameter. .. change:: DTOs: Raise :class:`ValidationException` when Pydantic validation fails :type: bugfix :pr: 2204 :issue: 2190 Ensures that when the Pydantic validation fails in the Pydantic DTO, a :class:`ValidationException` is raised with the extras set to the errors given by Pydantic. .. change:: Set the max width of the console to 80 :type: bugfix :pr: 2244 Sets the max width of the console to 80, to prevent the output from being wrapped. .. change:: Handling of optional path parameters :type: bugfix :pr: 2224 :issue: 2222 Resolves an issue where optional path parameters caused a 500 error to be raised. .. change:: Use os.replace instead of shutil.move for renaming files :type: bugfix :pr: 2223 Change to using :func:`os.replace` instead of :func:`shutil.move` for renaming files, to ensure atomicity. .. change:: Exception detail attribute :type: bugfix :pr: 2231 Set correctly the detail attribute on :class:`LitestarException` and :class:`HTTPException` regardless of whether it's passed positionally or by name. .. change:: Filters not available in ``exists()`` :type: bugfix :pr: 2228 :issue: 2221 Fixes :meth:`exists` method for SQLAlchemy sync and async. .. change:: Add Pydantic types to SQLAlchemy registry only if Pydantic is installed :type: bugfix :pr: 2252 Allows importing from ``litestar.contrib.sqlalchemy.base`` even if Pydantic is not installed. .. change:: Don't add content type for responses that don't have a body :type: bugfix :pr: 2263 :issue: 2106 Ensures that the ``content-type`` header is not added for responses that do not have a body such as responses with status code ``204 (No Content)``. .. change:: ``SQLAlchemyPlugin`` refactored :type: bugfix :pr: 2269 Changes the way the ``SQLAlchemyPlugin`` to now append the other plugins instead of the inheritance that was previously used. This makes using the ``plugins.get`` function work as expected. .. change:: Ensure ``app-dir`` is appended to path during autodiscovery :type: bugfix :pr: 2277 :issue: 2266 Fixes a bug which caused the ``--app-dir`` option to the Litestar CLI to not be propagated during autodiscovery. .. change:: Set content length header by default :type: bugfix :pr: 2271 Sets the ``content-length`` header by default even if the length of the body is ``0``. .. change:: Incorrect handling of mutable headers in :class:`ASGIResponse` :type: bugfix :pr: 2308 :issue: 2196 Update :class:`ASGIResponse`, :class:`Response` and friends to address a few issues related to headers: - If ``encoded_headers`` were passed in at any point, they were mutated within responses, leading to a growing list of headers with every response - While mutating ``encoded_headers``, the checks performed to assert a value was (not) already present, headers were not treated case-insensitive - Unnecessary work was performed while converting cookies / headers into an encoded headers list This was fixed by: - Removing the use of and deprecate ``encoded_headers`` - Handling headers on :class:`ASGIResponse` with :class:`MutableScopeHeaders`, which allows for case-insensitive membership tests, ``.setdefault`` operations, etc. .. change:: Adds missing ORM registry export :type: bugfix :pr: 2316 Adds an export that was overlooked for the base repo .. change:: Discrepancy in ``attrs``, ``msgspec`` and ``Pydantic`` for multi-part forms :type: bugfix :pr: 2280 :issue: 2278 Resolves issue in ``attrs``, ``msgspec`` and Pydantic for multi-part forms .. change:: Set proper default for ``exclude_http_methods`` in auth middleware :type: bugfix :pr: 2325 :issue: 2205 Sets ``OPTIONS`` as the default value for ``exclude_http_methods`` in the base authentication middleware class. .. changelog:: 2.0.0 :date: 2023/08/19 .. change:: Regression | Missing ``media_type`` information to error responses :type: bugfix :pr: 2131 :issue: 2024 Fixed a regression that caused error responses to be sent using a mismatched media type, e.g. an error response from a ``text/html`` endpoint would be sent as JSON. .. change:: Regression | ``Litestar.debug`` does not propagate to exception handling middleware :type: bugfix :pr: 2153 :issue: 2147 Fixed a regression where setting ``Litestar.debug`` would not propagate to the exception handler middleware, resulting in exception responses always being sent using the initial debug value. .. change:: Static files not being served if a route handler with the same base path was registered :type: bugfix :pr: 2154 Fixed a bug that would result in a ``404 - Not Found`` when requesting a static file where the :attr:`~litestar.static_files.StaticFilesConfig.path` was also used by a route handler. .. change:: HTMX: Missing default values for ``receive`` and ``send`` parameters of ``HTMXRequest`` :type: bugfix :pr: 2145 Add missing default values for the ``receive`` and ``send`` parameters of :class:`~litestar.contrib.htmx.request.HTMXRequest`. .. change:: DTO: Excluded attributes accessed during transfer :type: bugfix :pr: 2127 :issue: 2125 Fix the behaviour of DTOs such that they will no longer access fields that have been included. This behaviour would previously cause issues when these attributes were either costly or impossible to access (e.g. lazy loaded relationships of a SQLAlchemy model). .. change:: DTO | Regression: ``DTOData.create_instance`` ignores renaming :type: bugfix :pr: 2144 Fix a regression where calling :meth:`~litestar.dto.data_structures.DTOData.create_instance` would ignore the renaming settings of fields. .. change:: OpenAPI | Regression: Response schema for files and streams set ``application/octet-stream`` as ``contentEncoding`` instead of ``contentMediaType`` :type: bugfix :pr: 2130 Fix a regression that would set ``application/octet-stream`` as the ``contentEncoding`` instead of ``contentMediaType`` in the response schema of :class:`~litestar.response.File` :class:`~litestar.response.Stream`. .. change:: OpenAPI | Regression: Response schema diverges from ``prefer_alias`` setting for Pydantic models :type: bugfix :pr: 2150 Fix a regression that made the response schema use ``prefer_alias=True``, diverging from how Pydantic models are exported by default. .. change:: OpenAPI | Regression: Examples not being generated deterministically :type: bugfix :pr: 2161 Fix a regression that made generated examples non-deterministic, caused by a misconfiguration of the random seeding. .. change:: SQLAlchemy repository: Handling of dialects not supporting JSON :type: bugfix :pr: 2139 :issue: 2137 Fix a bug where SQLAlchemy would raise a :exc:`TypeError` when using a dialect that does not support JSON with the SQLAlchemy repositories. .. change:: JWT | Regression: ``OPTIONS`` and ``HEAD`` being authenticated by default :type: bugfix :pr: 2160 Fix a regression that would make ``litestar.contrib.jwt.JWTAuthenticationMiddleware`` authenticate ``OPTIONS`` and ``HEAD`` requests by default. .. change:: SessionAuth | Regression: ``OPTIONS`` and ``HEAD`` being authenticated by default :type: bugfix :pr: 2182 Fix a regression that would make :class:`~litestar.security.session_auth.middleware.SessionAuthMiddleware` authenticate ``OPTIONS`` and ``HEAD`` requests by default. .. changelog:: 2.0.0rc1 :date: 2023/08/05 .. change:: Support for server-sent-events :type: feature :pr: 2035 :issue: 1185 Support for `Server-sent events ` has been added with the :class:`ServerSentEvent <.response.ServerSentEvent>`: .. code-block:: python async def my_generator() -> AsyncGenerator[bytes, None]: count = 0 while count < 10: await sleep(0.01) count += 1 yield str(count) @get(path="/count") def sse_handler() -> ServerSentEvent: return ServerSentEvent(my_generator()) .. seealso:: :ref:`Server Sent Events ` .. change:: SQLAlchemy repository: allow specifying ``id_attribute`` per method :type: feature :pr: 2052 The following methods now accept an ``id_attribute`` argument, allowing to specify an alternative value to the models primary key: - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.delete`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.delete_many`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.get`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.update`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.delete`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.delete_many`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.get`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.update`` .. change:: SQLAlchemy repository: New ``upsert_many`` method :type: feature :pr: 2056 A new method ``upsert_many`` has been added to the SQLAlchemy repositories, providing equivalent functionality to the ``upsert`` method for multiple model instances. .. seealso:: ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.upsert_many`` ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.upsert_many`` .. change:: SQLAlchemy repository: New filters: ``OnBeforeAfter``, ``NotInCollectionFilter`` and ``NotInSearchFilter`` :type: feature :pr: 2057 The following filters have been added to the SQLAlchemy repositories: ``litestar.contrib.repository.filters.OnBeforeAfter`` Allowing to filter :class:`datetime.datetime` columns ``litestar.contrib.repository.filters.NotInCollectionFilter`` Allowing to filter using a ``WHERE ... NOT IN (...)`` clause ``litestar.contrib.repository.filters.NotInSearchFilter`` Allowing to filter using a `WHERE field_name NOT LIKE '%' || :value || '%'`` clause .. change:: SQLAlchemy repository: Configurable chunk sizing for ``delete_many`` :type: feature :pr: 2061 The repository now accepts a ``chunk_size`` parameter, determining the maximum amount of parameters in an ``IN`` statement before it gets chunked. This is currently only used in the ``delete_many`` method. .. change:: SQLAlchemy repository: Support InstrumentedAttribute for attribute columns :type: feature :pr: 2054 Support :class:`~sqlalchemy.orm.InstrumentedAttribute` for in the repository's ``id_attribute``, and the following methods: - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.delete`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.delete_many`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.get`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.update`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.delete`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.delete_many`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.get`` - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.update`` .. change:: OpenAPI: Support callable ``operation_id`` on route handlers :type: feature :pr: 2078 Route handlers may be passed a callable to ``operation_id`` to create the OpenAPI operation ID. .. change:: Run event listeners concurrently :type: feature :pr: 2096 :doc:`/usage/events` now run concurrently inside a task group. .. change:: Support extending the CLI with plugins :type: feature :pr: 2066 A new plugin protocol :class:`~litestar.plugins.CLIPluginProtocol` has been added that can be used to extend the Litestar CLI. .. seealso:: :ref:`usage/cli:Using a plugin` .. change:: DTO: Support renamed fields in ``DTOData`` and ``create_instance`` :type: bugfix :pr: 2065 A bug was fixed that would cause field renaming to be skipped within :class:`~litestar.dto.data_structures.DTOData` and :meth:`~litestar.dto.data_structures.DTOData.create_instance`. .. change:: SQLAlchemy repository: Fix ``health_check`` for oracle :type: bugfix :pr: 2060 The emitted statement for oracle has been changed to ``SELECT 1 FROM DUAL``. .. change:: Fix serialization of empty strings in multipart form :type: bugfix :pr: 2044 A bug was fixed that would cause a validation error to be raised for empty strings during multipart form decoding. .. change:: Use debug mode by default in test clients :type: misc :pr: 2113 The test clients will now default to ``debug=True`` instead of ``debug=None``. .. change:: Removal of deprecated ``partial`` module :type: misc :pr: 2113 :breaking: The deprecated ``litestar.partial`` has been removed. It can be replaced with DTOs, making use of the :class:`~litestar.dto.config.DTOConfig` option ``partial=True``. .. change:: Removal of deprecated ``dto/factory`` module :type: misc :pr: 2114 :breaking: The deprecated module ``litestar.dto.factory`` has been removed. .. change:: Removal of deprecated ``contrib/msgspec`` module :type: misc :pr: 2114 :breaking: The deprecated module ``litestar.contrib.msgspec`` has been removed. .. changelog:: 2.0.0beta4 :date: 2023/07/21 .. change:: Fix extra package dependencies :type: bugfix :pr: 2029 A workaround for a `bug in poetry `_ that caused development / extra dependencies to be installed alongside the package has been added. .. changelog:: 2.0.0beta3 :date: 2023/07/20 .. change:: :class:`SQLAlchemyDTO (Advanced Alchemy) `: column/relationship type inference :type: feature :pr: 1879 :issue: 1853 If type annotations aren't available for a given column/relationship, they may be inferred from the mapped object. For columns, the :attr:`~sqlalchemy.engine.interfaces.ReflectedColumn.type`\ 's :attr:`~sqlalchemy.types.TypeEngine.python_type` will be used as the type of the column, and the :attr:`~sqlalchemy.engine.interfaces.ReflectedColumn.nullable` property to determine if the field should have a :obj:`None` union. For relationships, where the ``RelationshipProperty.direction`` is :attr:`~sqlalchemy.orm.RelationshipDirection.ONETOMANY` or :attr:`~sqlalchemy.orm.RelationshipDirection.MANYTOMANY`, ``RelationshipProperty.collection_class`` and ``RelationshipProperty.mapper.class_`` are used to construct an annotation for the collection. For one-to-one relationships, ``RelationshipProperty.mapper.class_`` is used to get the type annotation, and will be made a union with :obj:`None` if all of the foreign key columns are nullable. .. change:: DTO: Piccolo ORM :type: feature :pr: 1896 Add support for piccolo ORM with the :class:`~litestar.contrib.piccolo.PiccoloDTO`. .. change:: OpenAPI: Allow setting ``OpenAPIController.path`` from ```OpenAPIConfig`` :type: feature :pr: 1886 :attr:`~litestar.openapi.OpenAPIConfig.path` has been added, which can be used to set the ``path`` for :class:`~litestar.openapi.OpenAPIController` directly, without needing to create a custom instance of it. If ``path`` is set in both :class:`~litestar.openapi.OpenAPIConfig` and :class:`~litestar.openapi.OpenAPIController`, the path set on the controller will take precedence. .. change:: SQLAlchemy repository: ``auto_commit``, ``auto_expunge`` and ``auto_refresh`` options :type: feature :pr: 1900 .. currentmodule:: litestar.contrib.sqlalchemy.repository Three new parameters have been added to the repository and various methods: ``auto_commit`` When this :obj:`True`, the session will :meth:`~sqlalchemy.orm.Session.commit` instead of :meth:`~sqlalchemy.orm.Session.flush` before returning. Available in: - ``~SQLAlchemyAsyncRepository.add`` - ``~SQLAlchemyAsyncRepository.add_many`` - ``~SQLAlchemyAsyncRepository.delete`` - ``~SQLAlchemyAsyncRepository.delete_many`` - ``~SQLAlchemyAsyncRepository.get_or_create`` - ``~SQLAlchemyAsyncRepository.update`` - ``~SQLAlchemyAsyncRepository.update_many`` - ``~SQLAlchemyAsyncRepository.upsert`` (and their sync equivalents) ``auto_refresh`` When :obj:`True`, the session will execute :meth:`~sqlalchemy.orm.Session.refresh` objects before returning. Available in: - ``~SQLAlchemyAsyncRepository.add`` - ``~SQLAlchemyAsyncRepository.get_or_create`` - ``~SQLAlchemyAsyncRepository.update`` - ``~SQLAlchemyAsyncRepository.upsert`` (and their sync equivalents) ``auto_expunge`` When this is :obj:`True`, the session will execute :meth:`~sqlalchemy.orm.Session.expunge` all objects before returning. Available in: - ``~SQLAlchemyAsyncRepository.add`` - ``~SQLAlchemyAsyncRepository.add_many`` - ``~SQLAlchemyAsyncRepository.delete`` - ``~SQLAlchemyAsyncRepository.delete_many`` - ``~SQLAlchemyAsyncRepository.get`` - ``~SQLAlchemyAsyncRepository.get_one`` - ``~SQLAlchemyAsyncRepository.get_one_or_none`` - ``~SQLAlchemyAsyncRepository.get_or_create`` - ``~SQLAlchemyAsyncRepository.update`` - ``~SQLAlchemyAsyncRepository.update_many`` - ``~SQLAlchemyAsyncRepository.list`` - ``~SQLAlchemyAsyncRepository.upsert`` (and their sync equivalents) .. change:: Include path name in ``ImproperlyConfiguredException`` message for missing param types :type: feature :pr: 1935 The message of a :exc:`ImproperlyConfiguredException` raised when a path parameter is missing a type now contains the name of the path. .. change:: DTO: New ``include`` parameter added to ``DTOConfig`` :type: feature :pr: 1950 :attr:`~litestar.dto.config.DTOConfig.include` has been added to :class:`~litestar.dto.config.DTOConfig`, providing a counterpart to :attr:`~litestar.dto.config.DTOConfig.exclude`. If ``include`` is provided, only those fields specified within it will be included. .. change:: ``AbstractDTOFactory`` moved to ``dto.factory.base`` :type: misc :breaking: :pr: 1950 :class:`~litestar.dto.base_factory.AbstractDTOFactory` has moved from ``litestar.dto.factory.abc`` to ``litestar.dto.factory.base``. .. change:: SQLAlchemy repository: Rename ``_sentinel`` column to ``sa_orm_sentinel`` :type: misc :breaking: :pr: 1933 The ``_sentinel`` column of ``~litestar.contrib.sqlalchemy.base.UUIDPrimaryKey`` has been renamed to ``sa_orm_sentinel``, to support Spanner, which does not support tables starting with ``_``. .. change:: SQLAlchemy repository: Fix audit columns defaulting to app startup time :type: bugfix :pr: 1894 A bug was fixed where ``~litestar.contrib.sqlalchemy.base.AuditColumns.created_at`` and ``~litestar.contrib.sqlalchemy.base.AuditColumns.updated_at`` would default to the :class:`~datetime.datetime` at initialization time, instead of the time of the update. .. change:: :class:`SQLAlchemyDTO (Advanced Alchemy) `: Fix handling of ``Sequence`` with defaults :type: bugfix :pr: 1883 :issue: 1851 Fixes handling of columns defined with `Sequence `_ default values. The SQLAlchemy default value for a :class:`~sqlalchemy.schema.Column` will be ignored when it is a :class:`~sqlalchemy.schema.Sequence` object. This is because the SQLAlchemy sequence types represent server generated values, and there is no way for us to generate a reasonable default value for that field from it without making a database query, which is not possible deserialization. .. change:: Allow JSON as redirect response :type: bugfix :pr: 1908 Enables using redirect responses with a JSON media type. .. change:: DTO / OpenAPI: Fix detection of required fields for Pydantic and msgspec DTOs :type: bugfix :pr: 1946 A bug was fixed that would lead to fields of a Pydantic model or msgspec Structs being marked as "not required" in the generated OpenAPI schema when used with DTOs. .. change:: Replace ``Header``, ``CacheControlHeader`` and ``ETag`` Pydantic models with dataclasses :type: misc :pr: 1917 :breaking: As part of the removal of Pydantic as a hard dependency, the header models :class:`~litestar.datastructures.Header`, :class:`~litestar.datastructures.CacheControlHeader` and :class:`~litestar.datastructures.ETag` have been replaced with dataclasses. .. note:: Although marked breaking, this change should not affect usage unless you relied on these being Pydantic models in some way. .. change:: Pydantic as an optional dependency :breaking: :pr: 1963 :type: misc As of this release, Pydantic is no longer a required dependency of Litestar. It is still supported in the same capacity as before, but Litestar itself does not depend on it anymore in its internals. .. change:: Pydantic 2 support :type: feature :pr: 1956 Pydantic 2 is now supported alongside Pydantic 1. .. change:: Deprecation of ``partial`` module :type: misc :pr: 2002 The ``litestar.partial`` and ``litestar.partial.Partial`` have been deprecated and will be removed in a future release. Users are advised to upgrade to DTOs, making use of the :class:`~litestar.dto.config.DTOConfig` option ``partial=True``. .. changelog:: 2.0.0beta2 :date: 2023/06/24 .. change:: Support ``annotated-types`` :type: feature :pr: 1847 Extended support for the `annotated-types `_ library is now available. .. change:: Increased verbosity of validation error response keys :type: feature :pr: 1774 :breaking: The keys in validation error responses now include the full path to the field where the originated. An optional ``source`` key has been added, signifying whether the value is from the body, a cookie, a header, or a query param. .. code-block:: json :caption: before { "status_code": 400, "detail": "Validation failed for POST http://localhost:8000/some-route", "extra": [ {"key": "int_param", "message": "value is not a valid integer"}, {"key": "int_header", "message": "value is not a valid integer"}, {"key": "int_cookie", "message": "value is not a valid integer"}, {"key": "my_value", "message": "value is not a valid integer"} ] } .. code-block:: json :caption: after { "status_code": 400, "detail": "Validation failed for POST http://localhost:8000/some-route", "extra": [ {"key": "child.my_value", "message": "value is not a valid integer", "source": "body"}, {"key": "int_param", "message": "value is not a valid integer", "source": "query"}, {"key": "int_header", "message": "value is not a valid integer", "source": "header"}, {"key": "int_cookie", "message": "value is not a valid integer", "source": "cookie"}, ] } .. change:: ``TestClient`` default timeout :type: feature :pr: 1840 :breaking: A ``timeout`` parameter was added to - :class:`~litestar.testing.TestClient` - :class:`~litestar.testing.AsyncTestClient` - :class:`~litestar.testing.create_test_client` - :class:`~litestar.testing.create_async_test_client` The value is passed down to the underlying HTTPX client and serves as a default timeout for all requests. .. change:: SQLAlchemy DTO: Explicit error messages when type annotations for a column are missing :type: feature :pr: 1852 Replace the nondescript :exc:`KeyError` raised when a SQLAlchemy DTO is constructed from a model that is missing a type annotation for an included column with an :exc:`ImproperlyConfiguredException`, including an explicit error message, pointing at the potential cause. .. change:: Remove exception details from Internal Server Error responses :type: bugfix :pr: 1857 :issue: 1856 Error responses with a ``500`` status code will now always use `"Internal Server Error"` as default detail. .. change:: Pydantic v1 regex validation :type: bugfix :pr: 1865 :issue: 1860 A regression has been fixed in the Pydantic signature model logic, which was caused by the renaming of ``regex`` to ``pattern``, which would lead to the :attr:`~litestar.params.KwargDefinition.pattern` not being validated. .. changelog:: 2.0.0beta1 :date: 2023/06/16 .. change:: Expose ``ParsedType`` as public API :type: feature :pr: 1677 1567 Expose the previously private :class:`litestar.typing.ParsedType`. This is mainly indented for usage with :meth:`litestar.plugins.SerializationPluginProtocol.supports_type` .. change:: Improved debugging capabilities :type: feature :pr: 1742 - A new ``pdb_on_exception`` parameter was added to :class:`~litestar.app.Litestar`. When set to ``True``, Litestar will drop into a the Python debugger when an exception occurs. It defaults to ``None`` - When ``pdb_on_exception`` is ``None``, setting the environment variable ``LITESTAR_PDB=1`` can be used to enable this behaviour - When using the CLI, passing the ``--pdb`` flag to the ``run`` command will temporarily set the environment variable ``LITESTAR_PDB=1`` .. change:: OpenAPI: Add `operation_class` argument to HTTP route handlers :type: feature :pr: 1732 The ``operation_class`` argument was added to :class:`~litestar.handlers.HTTPRouteHandler` and the corresponding decorators, allowing to override the :class:`~litestar.openapi.spec.Operation` class, to enable further customization of the generated OpenAPI schema. .. change:: OpenAPI: Support nested ``Literal`` annotations :type: feature :pr: 1829 Support nested :class:`typing.Literal` annotations by flattening them into a single ``Literal``. .. change:: CLI: Add ``--reload-dir`` option to ``run`` command :type: feature :pr: 1689 A new ``--reload-dir`` option was added to the ``litestar run`` command. When used, ``--reload`` is implied, and the server will watch for changes in the given directory. .. change:: Allow extra attributes on JWTs via ``extras`` attribute :type: feature :pr: 1695 Add the ``litestar.contrib.jwt.Token.extras`` attribute, containing extra attributes found on the JWT. .. change:: Add default modes for ``Websocket.iter_json`` and ``WebSocket.iter_data`` :type: feature :pr: 1733 Add a default ``mode`` for :meth:`~litestar.connection.WebSocket.iter_json` and :meth:`~litestar.connection.WebSocket.iter_data`, with a value of ``text``. .. change:: SQLAlchemy repository: Synchronous repositories :type: feature :pr: 1683 Add a new synchronous repository base class: ``litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository``, which offer the same functionality as its asynchronous counterpart while operating on a synchronous :class:`sqlalchemy.orm.Session`. .. change:: SQLAlchemy repository: Oracle Database support :type: feature :pr: 1694 Add support for Oracle Database via `oracledb `_. .. change:: SQLAlchemy repository: DuckDB support :type: feature :pr: 1744 Add support for `DuckDB `_. .. change:: SQLAlchemy repository: Google Spanner support :type: feature :pr: 1744 Add support for `Google Spanner `_. .. change:: SQLAlchemy repository: JSON check constraint for Oracle Database :type: feature :pr: 1780 When using the :class:`litestar.contrib.sqlalchemy.types.JsonB` type with an Oracle Database engine, a JSON check constraint will be created for that column. .. change:: SQLAlchemy repository: Remove ``created`` and ``updated`` columns :type: feature :pr: 1816 :breaking: The ``created`` and ``updated`` columns have been superseded by ``created_at`` and ``updated_at`` respectively, to prevent name clashes. .. change:: SQLAlchemy repository: Add timezone aware type :type: feature :pr: 1816 :breaking: A new timezone aware type ``litestar.contrib.sqlalchemy.types.DateTimeUTC`` has been added, which enforces UTC timestamps stored in the database. .. change:: SQLAlchemy repository: Exclude unloaded columns in ``to_dict`` :type: feature :pr: 1802 When exporting models using the ``~litestar.contrib.sqlalchemy.base.CommonTableAttributes.to_dict`` method, unloaded columns will now always be excluded. This prevents implicit I/O via lazy loading, and errors when using an asynchronous session. .. change:: DTOs: Nested keyword arguments in ``.create_instance()`` :type: feature :pr: 1741 :issue: 1727 The :meth:`DTOData.create_instance ` method now supports providing values for arbitrarily nested data via kwargs using a double-underscore syntax, for example ``data.create_instance(foo__bar="baz")``. .. seealso:: :ref:`usage/dto/1-abstract-dto:Providing values for nested data` .. change:: DTOs: Hybrid properties and association proxies in :class:`SQLAlchemyDTO (Advanced Alchemy) ` :type: feature :pr: 1754 1776 The :class:`SQLAlchemyDTO (Advanced Alchemy) ` now supports `hybrid attribute `_ and `associationproxy `_. The generated field will be marked read-only. .. change:: DTOs: Transfer to generic collection types :type: feature :pr: 1764 :issue: 1763 DTOs can now be wrapped in generic collection types such as :class:`typing.Sequence`. These will be substituted with a concrete and instantiable type at run time, e.g. in the case of ``Sequence`` a :class:`list`. .. change:: DTOs: Data transfer for non-generic builtin collection annotations :type: feature :pr: 1799 Non-parametrized generics in annotations (e.g. ``a: dict``) will now be inferred as being parametrized with ``Any``. ``a: dict`` is then equivalent to ``a: dict[Any, Any]``. .. change:: DTOs: Exclude leading underscore fields by default :type: feature :pr: 1777 :issue: 1768 :breaking: Leading underscore fields will not be excluded by default. This behaviour can be configured with the newly introduced :attr:`~litestar.dto.factory.DTOConfig.underscore_fields_private` configuration value, which defaults to ``True``. .. change:: DTOs: Msgspec and Pydantic DTO factory implementation :type: feature :pr: 1712 :issue: 1531, 1532 DTO factories for `msgspec `_ and `Pydantic `_ have been added: - :class:`~litestar.contrib.msgspec.MsgspecDTO` - :class:`~litestar.contrib.pydantic.PydanticDTO` .. change:: DTOs: Arbitrary generic wrappers :pr: 1801 :issue: 1631, 1798 When a handler returns a type that is not supported by the DTO, but: - the return type is generic - it has a generic type argument that is supported by the dto - the type argument maps to an attribute on the return type the DTO operations will be performed on the data retrieved from that attribute of the instance returned from the handler, and return the instance. The constraints are: - the type returned from the handler must be a type that litestar can natively encode - the annotation of the attribute that holds the data must be a type that DTOs can otherwise manage .. code-block:: python from dataclasses import dataclass from typing import Generic, List, TypeVar from typing_extensions import Annotated from litestar import Litestar, get from litestar.dto import DTOConfig from litestar.dto.factory.dataclass_factory import DataclassDTO @dataclass class User: name: str age: int T = TypeVar("T") V = TypeVar("V") @dataclass class Wrapped(Generic[T, V]): data: List[T] other: V @get(dto=DataclassDTO[Annotated[User, DTOConfig(exclude={"age"})]]) def handler() -> Wrapped[User, int]: return Wrapped( data=[User(name="John", age=42), User(name="Jane", age=43)], other=2, ) app = Litestar(route_handlers=[handler]) # GET "/": {"data": [{"name": "John"}, {"name": "Jane"}], "other": 2} .. change:: Store and reuse state `deep_copy` directive when copying state :type: bugfix :issue: 1674 :pr: 1678 App state can be created using ``deep_copy=False``, however state would still be deep copied for dependency injection. This was fixed memoizing the value of ``deep_copy`` when state is created, and reusing it on subsequent copies. .. change:: ``ParsedType.is_subclass_of(X)`` ``True`` for union if all union types are subtypes of ``X`` :type: bugfix :pr: 1690 :issue: 1652 When :class:`~litestar.typing.ParsedType` was introduced, :meth:`~litestar.typing.ParsedType.is_subclass_of` any union was deliberately left to return ``False`` with the intention of waiting for some use-cases to arrive. This behaviour was changed to address an issue where a handler may be typed to return a union of multiple response types; If all response types are :class:`~litestar.response.Response` subtypes then the correct response handler will now be applied. .. change:: Inconsistent template autoescape behavior :type: bugfix :pr: 1718 :issue: 1699 The mako template engine now defaults to autoescaping expressions, making it consistent with config of Jinja template engine. .. change:: Missing ``ChannelsPlugin`` in signature namespace population :type: bugfix :pr: 1719 :issue: 1691 The :class:`~litestar.channels.plugin.ChannelsPlugin` has been added to the signature namespace, fixing an issue where using ``from __future__ import annotations`` or stringized annotations would lead to a :exc:`NameError`, if the plugin was not added to the signatured namespace manually. .. change:: Gzip middleware not sending small streaming responses :type: bugfix :pr: 1723 :issue: 1681 A bug was fixed that would cause smaller streaming responses to not be sent at all when the :class:`~litestar.middleware.compression.CompressionMiddleware` was used with ``gzip``. .. change:: Premature transfer to nested models with `DTOData` :type: bugfix :pr: 1731 :issue: 1726 An issue was fixed where data that should be transferred to builtin types on instantiation of :class:`~litestar.dto.factory.DTOData` was being instantiated into a model type for nested models. .. change:: Incorrect ``sync_to_thread`` usage warnings for generator dependencies :type: bugfix :pr: 1716 1740 :issue: 1711 A bug was fixed that caused incorrect warnings about missing ``sync_to_thread`` usage were issues when asynchronous generators were being used as dependencies. .. change:: Dependency injection custom dependencies in ``WebSocketListener`` :type: bugfix :pr: 1807 :issue: 1762 An issue was resolved that would cause failures when dependency injection was being used with custom dependencies (that is, injection of things other than ``state``, ``query``, path parameters, etc.) within a :class:`~litestar.handlers.WebsocketListener`. .. change:: OpenAPI schema for ``Dict[K, V]`` ignores generic :type: bugfix :pr: 1828 :issue: 1795 An issue with the OpenAPI schema generation was fixed that would lead to generic arguments to :class:`dict` being ignored. An type like ``dict[str, int]`` now correctly renders as ``{"type": "object", "additionalProperties": { "type": "integer" }}``. .. change:: ``WebSocketTestSession`` not timing out without when connection is not accepted :type: bugfix :pr: 1696 A bug was fixed that caused :class:`~litestar.testing.WebSocketTestSession` to block indefinitely when if :meth:`~litestar.connection.WebSocket.accept` was never called, ignoring the ``timeout`` parameter. .. change:: SQLAlchemy repository: Fix alembic migrations generated for models using ``GUID`` :type: bugfix :pr: 1676 Migrations generated for models with a ``~litestar.contrib.sqlalchemy.types.GUID`` type would erroneously add a ``length=16`` on the input. Since this parameter is not defined in the type's the ``__init__`` method. This was fixed by adding the appropriate parameter to the type's signature. .. change:: Remove ``state`` parameter from ``AfterExceptionHookHandler`` and ``BeforeMessageSendHookHandler`` :type: misc :pr: 1739 :breaking: Remove the ``state`` parameter from ``AfterExceptionHookHandler`` and ``BeforeMessageSendHookHandler``. ``AfterExceptionHookHandler``\ s will have to be updated from .. code-block:: python async def after_exception_handler( exc: Exception, scope: Scope, state: State ) -> None: ... to .. code-block:: python async def after_exception_handler(exc: Exception, scope: Scope) -> None: ... The state can still be accessed like so: .. code-block:: python async def after_exception_handler(exc: Exception, scope: Scope) -> None: state = scope["app"].state ``BeforeMessageSendHookHandler``\ s will have to be updated from .. code-block:: python async def before_send_hook_handler( message: Message, state: State, scope: Scope ) -> None: ... to .. code-block:: python async def before_send_hook_handler(message: Message, scope: Scope) -> None: ... where state can be accessed in the same manner: .. code-block:: python async def before_send_hook_handler(message: Message, scope: Scope) -> None: state = scope["app"].state .. change:: Removal of ``dto.exceptions`` module :pr: 1773 :breaking: The module ``dto.exceptions`` has been removed, since it was not used anymore internally by the DTO implementations, and superseded by standard exceptions. .. change:: ``BaseRouteHandler`` no longer generic :pr: 1819 :breaking: :class:`~litestar.handlers.BaseRouteHandler` was originally made generic to support proper typing of the ``ownership_layers`` property, but the same effect can now be achieved using :class:`typing.Self`. .. change:: Deprecation of ``Litestar`` parameter ``preferred_validation_backend`` :pr: 1810 :breaking: The following changes have been made regarding the ``preferred_validation_backend``: - The ``preferred_validation_backend`` parameter of :class:`~litestar.app.Litestar` has been renamed to ``_preferred_validation_backend`` and deprecated. It will be removed completely in a future version. - The ``Litestar.preferred_validation_backend`` attribute has been made private - The ``preferred_validation_backend`` attribute has been removed from :class:`~litestar.config.app.AppConfig` In addition, the logic for selecting a signature validation backend has been simplified as follows: If the preferred backend is set to ``attrs``, or the signature contains attrs types, ``attrs`` is selected. In all other cases, Pydantic will be used. .. change:: ``Response.get_serializer`` moved to ``serialization.get_serializer`` :pr: 1820 :breaking: The ``Response.get_serializer()`` method has been removed in favor of the :func:`~litestar.serialization.get_serializer` function. In the previous :class:`~litestar.response.Response` implementation, ``get_serializer()`` was called on the response inside the response's ``__init__``, and the merging of class-level ``type_encoders`` with the ``Response``\ 's ``type_encoders`` occurred inside its ``get_serializer`` method. In the current version of ``Response``, the response body is not encoded until after the response object has been returned from the handler, and it is converted into a low-level :class:`~litestar.response.base.ASGIResponse` object. Due to this, there is still opportunity for the handler layer resolved ``type_encoders`` object to be merged with the ``Response`` defined ``type_encoders``, making the merge inside the ``Response`` no longer necessary. In addition, the separate ``get_serializer`` function greatly simplifies the interaction between middlewares and serializers, allowing to retrieve one independently from a ``Response``. .. change:: Remove response containers and introduce ``ASGIResponse`` :pr: 1790 :breaking: Response Containers were wrapper classes used to indicate the type of response returned by a handler, for example ``File``, ``Redirect``, ``Template`` and ``Stream`` types. These types abstracted the interface of responses from the underlying response itself. Response containers have been removed and their functionality largely merged with that of :class:`~litestar.response.Response`. The predefined response containers still exist functionally, as subclasses of :class:`Response <.response.Response>` and are now located within the :mod:`litestar.response` module. In addition to the functionality of Response containers, they now also feature all of the response's functionality, such as methods to add headers and cookies. The :class:`~litestar.response.Response` class now only serves as a wrapper and context object, and does not handle the data sending part, which has been delegated to a newly introduced :class:`ASGIResponse <.response.base.ASGIResponse>`. This type (and its subclasses) represent the response as an immutable object and are used internally by Litestar to perform the I/O operations of the response. These can be created and returned from handlers like any other ASGI application, however they are low-level, and lack the utility of the higher-level response types. .. changelog:: 2.0.0alpha7 :date: 2023/05/14 .. change:: Warn about sync callables in route handlers and dependencies without an explicit ``sync_to_thread`` value :type: feature :pr: 1648 1655 A warning will now be raised when a synchronous callable is being used in an :class:`~.handlers.HTTPRouteHandler` or :class:`~.di.Provide`, without setting ``sync_to_thread``. This is to ensure that synchronous callables are handled properly, and to prevent accidentally using callables which might block the main thread. This warning can be turned off globally by setting the environment variable ``LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD=0``. .. seealso:: :doc:`/topics/sync-vs-async` .. change:: Warn about ``sync_to_thread`` with async callables :type: feature :pr: 1664 A warning will be raised when ``sync_to_thread`` is being used in :class:`~.handlers.HTTPRouteHandler` or :class:`~.di.Provide` with an asynchronous callable, as this will have no effect. This warning can be turned off globally by setting the environment variable ``LITESTAR_WARN_SYNC_TO_THREAD_WITH_ASYNC=0``. .. change:: WebSockets: Dependencies in listener hooks :type: feature :pr: 1647 Dependencies can now be used in the :class:`~litestar.handlers.websocket_listener` hooks ``on_accept``, ``on_disconnect`` and the ``connection_lifespan`` context manager. The ``socket`` parameter is therefore also not mandatory anymore in those callables. .. change:: Declaring dependencies without ``Provide`` :type: feature :pr: 1647 Dependencies can now be declared without using :class:`~litestar.di.Provide`. The callables can be passed directly to the ``dependencies`` dictionary. .. change:: Add ``DTOData`` to receive unstructured but validated DTO data :type: feature :pr: 1650 :class:`~litestar.dto.factory.DTOData` is a datastructure for interacting with DTO validated data in its unstructured form. This utility is to support the case where the amount of data that is available from the client request is not complete enough to instantiate an instance of the model that would otherwise be injected. .. change:: Partial DTOs :type: feature :pr: 1651 Add a ``partial`` flag to :class:`~litestar.dto.factory.DTOConfig`, making all DTO fields options. Subsequently, any unset values will be filtered when extracting data from transfer models. This allows for example to use a to handle PATCH requests more easily. .. change:: SQLAlchemy repository: ``psycopg`` asyncio support :type: feature :pr: 1657 Async `psycopg `_ is now officially supported and tested for the SQLAlchemy repository. .. change:: SQLAlchemy repository: ``BigIntPrimaryKey`` mixin :type: feature :pr: 1657 ``~litestar.contrib.sqlalchemy.base.BigIntPrimaryKey`` mixin, providing a ``BigInt`` primary key column, with a fallback to ``Integer`` for sqlite. .. change:: SQLAlchemy repository: Store GUIDs as binary on databases that don't have a native GUID type :type: feature :pr: 1657 On databases without native support for GUIDs, ``~litestar.contrib.sqlalchemy.types.GUID`` will now fall back to ``BINARY(16)``. .. change:: Application lifespan context managers :type: feature :pr: 1635 A new ``lifespan`` argument has been added to :class:`~litestar.app.Litestar`, accepting an asynchronous context manager, wrapping the lifespan of the application. It will be entered with the startup phase and exited on shutdown, providing functionality equal to the ``on_startup`` and ``on_shutdown`` hooks. .. change:: Unify application lifespan hooks: Remove ``before_`` and ``after_`` :breaking: :type: feature :pr: 1663 The following application lifespan hooks have been removed: - ``before_startup`` - ``after_startup`` - ``before_shutdown`` - ``after_shutdown`` The remaining hooks ``on_startup`` and ``on_shutdown`` will now receive as their optional first argument the :class:`~litestar.app.Litestar` application instead of the application's state. .. change:: Trio-compatible event emitter :type: feature :pr: 1666 The default :class:`~litestar.events.emitter.SimpleEventEmitter` is now compatible with `trio `_. .. change:: OpenAPI: Support ``msgspec.Meta`` :type: feature :pr: 1669 :class:`msgspec.Meta` is now fully supported for OpenAPI schema generation. .. change:: OpenAPI: Support Pydantic ``FieldInfo`` :type: feature :pr: 1670 :issue: 1541 Pydantic's ``FieldInfo`` (``regex``, ``gt``, ``title``, etc.) now have full support for OpenAPI schema generation. .. change:: OpenAPI: Fix name collision in DTO models :type: bugfix :pr: 1649 :issue: 1643 A bug was fixed that would lead to name collisions in the OpenAPI schema when using DTOs with the same class name. DTOs now include a short 8 byte random string in their generated name to prevent this. .. change:: Fix validated attrs model being injected as a dictionary :type: bugfix :pr: 1668 :issue: 1643 A bug was fixed that would lead to an attrs model used to validate a route handler's ``data`` not being injected itself but as a dictionary representation. .. change:: Validate unknown media types :breaking: :type: bugfix :pr: 1671 :issue: 1446 An unknown media type in places where Litestar can't infer the type from the return annotation, an :exc:`ImproperlyConfiguredException` will now be raised. .. changelog:: 2.0.0alpha6 :date: 2023/05/09 .. change:: Relax typing of ``**kwargs`` in ``ASGIConnection.url_for`` :type: bugfix :pr: 1610 Change the typing of the ``**kwargs`` in :meth:`ASGIConnection.url_for ` from ``dict[str, Any]`` to ``Any`` .. change:: Fix: Using ``websocket_listener`` in controller causes ``TypeError`` :type: bugfix :pr: 1627 :issue: 1615 A bug was fixed that would cause a type error when using a :class:`websocket_listener ` in a ``Controller`` .. change:: Add ``connection_accept_handler`` to ``websocket_listener`` :type: feature :pr: 1572 :issue: 1571 Add a new ``connection_accept_handler`` parameter to :class:`websocket_listener `, which can be used to customize how a connection is accepted, for example to add headers or subprotocols .. change:: Testing: Add ``block`` and ``timeout`` parameters to ``WebSocketTestSession`` receive methods :type: feature :pr: 1593 Two parameters, ``block`` and ``timeout`` have been added to the following methods: - :meth:`receive ` - :meth:`receive_text ` - :meth:`receive_bytes ` - :meth:`receive_json ` .. change:: CLI: Add ``--app-dir`` option to root command :type: feature :pr: 1506 The ``--app-dir`` option was added to the root CLI command, allowing to set the run applications from a path that's not the current working directory. .. change:: WebSockets: Data iterators :type: feature :pr: 1626 Two new methods were added to the :class:`WebSocket ` connection, which allow to continuously receive data and iterate over it: - :meth:`iter_data ` - :meth:`iter_json ` .. change:: WebSockets: MessagePack support :type: feature :pr: 1626 Add support for `MessagePack `_ to the :class:`WebSocket ` connection. Three new methods have been added for handling MessagePack: - :meth:`send_msgpack ` - :meth:`receive_msgpack ` - :meth:`iter_msgpack ` In addition, two MessagePack related methods were added to :class:`WebSocketTestSession `: - :meth:`send_msgpack ` - :meth:`receive_msgpack ` .. change:: SQLAlchemy repository: Add support for sentinel column :type: feature :pr: 1603 This change adds support for ``sentinel column`` feature added in ``sqlalchemy`` 2.0.10. Without it, there are certain cases where ``add_many`` raises an exception. The ``_sentinel`` value added to the declarative base should be excluded from normal select operations automatically and is excluded in the ``to_dict`` methods. .. change:: DTO: Alias generator for field names :type: feature :pr: 1590 A new argument ``rename_strategy`` has been added to the :class:`DTOConfig `, allowing to remap field names with strategies such as "camelize". .. change:: DTO: Nested field exclusion :type: feature :pr: 1596 :issue: 1197 This feature adds support for excluding nested model fields using dot-notation, e.g., ``"a.b"`` excludes field ``b`` from nested model field ``a`` .. change:: WebSockets: Managing a socket's lifespan using a context manager in websocket listeners :type: feature :pr: 1625 Changes the way a socket's lifespan - accepting the connection and calling the appropriate event hooks - to use a context manager. The ``connection_lifespan`` argument was added to the :class:`WebSocketListener `, which accepts an asynchronous context manager, which can be used to handle the lifespan of the socket. .. change:: New module: Channels :type: feature :pr: 1587 A new module :doc:`channels ` has been added: A general purpose event streaming library, which can for example be used to broadcast messages via WebSockets. .. change:: DTO: Undocumented ``dto.factory.backends`` has been made private :breaking: :type: misc :pr: 1589 The undocumented ``dto.factory.backends`` module has been made private .. changelog:: 2.0.0alpha5 .. change:: Pass template context to HTMX template response :type: feature :pr: 1488 Pass the template context to the :class:`Template ` returned by :class:`htmx.Response `. .. change:: OpenAPI support for attrs and msgspec classes :type: feature :pr: 1487 Support OpenAPI schema generation for `attrs `_ classes and `msgspec `_ ``Struct``\ s. .. change:: SQLAlchemy repository: Add ``ModelProtocol`` :type: feature :pr: 1503 Add a new class ``contrib.sqlalchemy.base.ModelProtocol``, serving as a generic model base type, allowing to specify custom base classes while preserving typing information .. change:: SQLAlchemy repository: Support MySQL/MariaDB :type: feature :pr: 1345 Add support for MySQL/MariaDB to the SQLAlchemy repository, using the `asyncmy `_ driver. .. change:: SQLAlchemy repository: Support MySQL/MariaDB :type: feature :pr: 1345 Add support for MySQL/MariaDB to the SQLAlchemy repository, using the `asyncmy `_ driver. .. change:: SQLAlchemy repository: Add matching logic to ``get_or_create`` :type: feature :pr: 1345 Add a ``match_fields`` argument to ``litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.get_or_create``. This lets you lookup a model using a subset of the kwargs you've provided. If the remaining kwargs are different from the retrieved model's stored values, an update is performed. .. change:: Repository: Extend filter types :type: feature :pr: 1345 Add new filters ``litestar.contrib.repository.filters.OrderBy`` and ``litestar.contrib.repository.filters.SearchFilter``, providing ``ORDER BY ...`` and ``LIKE ...`` / ``ILIKE ...`` clauses respectively .. change:: SQLAlchemy repository: Rename ``SQLAlchemyRepository`` > ``SQLAlchemyAsyncRepository`` :breaking: :type: misc :pr: 1345 ``SQLAlchemyRepository`` has been renamed to ``litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository``. .. change:: DTO: Add ``AbstractDTOFactory`` and backends :type: feature :pr: 1461 An all-new DTO implementation was added, using ``AbstractDTOFactory`` as a base class, providing Pydantic and msgspec backends to facilitate (de)serialization and validation. .. change:: DTO: Remove ``from_connection`` / extend ``from_data`` :breaking: :type: misc :pr: 1500 The method ``DTOInterface.from_connection`` has been removed and replaced by ``DTOInterface.from_bytes``, which receives both the raw bytes from the connection, and the connection instance. Since ``from_bytes`` now does not handle connections anymore, it can also be a synchronous method, improving symmetry with ``DTOInterface.from_bytes``. The signature of ``from_data`` has been changed to also accept the connection, matching ``from_bytes``' signature. As a result of these changes, :meth:`DTOInterface.from_bytes ` no longer needs to receive the connection instance, so the ``request`` parameter has been dropped. .. change:: WebSockets: Support DTOs in listeners :type: feature :pr: 1518 Support for DTOs has been added to :class:`WebSocketListener ` and :class:`WebSocketListener `. A ``dto`` and ``return_dto`` parameter has been added, providing the same functionality as their route handler counterparts. .. change:: DTO based serialization plugin :breaking: :type: feature :pr: 1501 :class:`SerializationPluginProtocol ` has been re-implemented, leveraging the new :class:`DTOInterface `. If a handler defines a plugin supported type as either the ``data`` kwarg type annotation, or as the return annotation for a handler function, and no DTO has otherwise been resolved to handle the type, the protocol creates a DTO implementation to represent that type which is then used to de-serialize into, and serialize from instances of that supported type. .. important:: The `Piccolo ORM `_ and `Tortoise ORM `_ plugins have been removed by this change, but will be re-implemented using the new patterns in a future release leading up to the 2.0 release. .. change:: SQLAlchemy 1 contrib module removed :breaking: :type: misc :pr: 1501 As a result of the changes introduced in `#1501 `_, SQLAlchemy 1 support has been dropped. .. note:: If you rely on SQLAlchemy 1, you can stick to Starlite *1.51* for now. In the future, a SQLAlchemy 1 plugin may be released as a standalone package. .. change:: Fix inconsistent parsing of unix timestamp between Pydantic and cattrs :type: bugfix :pr: 1492 :issue: 1491 Timestamps parsed as :class:`date ` with Pydantic return a UTC date, while cattrs implementation return a date with the local timezone. This was corrected by forcing dates to UTC when being parsed by attrs. .. change:: Fix: Retrieve type hints from class with no ``__init__`` method causes error :type: bugfix :pr: 1505 :issue: 1504 An error would occur when using a callable without an :meth:`object.__init__` method was used in a placed that would cause it to be inspected (such as a route handler's signature). This was caused by trying to access the ``__module__`` attribute of :meth:`object.__init__`, which would fail with .. code-block:: 'wrapper_descriptor' object has no attribute '__module__' .. change:: Fix error raised for partially installed attrs dependencies :type: bugfix :pr: 1543 An error was fixed that would cause a :exc:`MissingDependencyException` to be raised when dependencies for `attrs `_ were partially installed. This was fixed by being more specific about the missing dependencies in the error messages. .. change:: Change ``MissingDependencyException`` to be a subclass of ``ImportError`` :type: misc :pr: 1557 :exc:`MissingDependencyException` is now a subclass of :exc:`ImportError`, to make handling cases where both of them might be raised easier. .. change:: Remove bool coercion in URL parsing :breaking: :type: bugfix :pr: 1550 :issue: 1547 When defining a query parameter as ``param: str``, and passing it a string value of ``"true"``, the value received by the route handler was the string ``"True"``, having been title cased. The same was true for the value of ``"false"``. This has been fixed by removing the coercing of boolean-like values during URL parsing and leaving it up to the parsing utilities of the receiving side (i.e. the handler's signature model) to handle these values according to the associated type annotations. .. change:: Update ``standard`` and ``full`` package extras :type: misc :pr: 1494 - Add SQLAlchemy, uvicorn, attrs and structlog to the ``full`` extra - Add uvicorn to the ``standard`` extra - Add ``uvicorn[standard]`` as an optional dependency to be used in the extras .. change:: Remove support for declaring DTOs as handler types :breaking: :type: misc :pr: 1534 Prior to this, a DTO type could be declared implicitly using type annotations. With the addition of the ``dto`` and ``return_dto`` parameters, this feature has become superfluous and, in the spirit of offering only one clear way of doing things, has been removed. .. change:: Fix missing ``content-encoding`` headers on gzip/brotli compressed files :type: bugfix :pr: 1577 :issue: 1576 Fixed a bug that would cause static files served via ``StaticFilesConfig`` that have been compressed with gripz or brotli to miss the appropriate ``content-encoding`` header. .. change:: DTO: Simplify ``DTOConfig`` :type: misc :breaking: :pr: 1580 - The ``include`` parameter has been removed, to provide a more accessible interface and avoid overly complex interplay with ``exclude`` and its support for dotted attributes - ``field_mapping`` has been renamed to ``rename_fields`` and support to remap field types has been dropped - experimental ``field_definitions`` has been removed. It may be replaced with a "ComputedField" in a future release that will allow multiple field definitions to be added to the model, and a callable that transforms them into a value for a model field. See .. changelog:: 2.0.0alpha4 .. change:: ``attrs`` and ``msgspec`` support in :class:`Partial ` :type: feature :pr: 1462 :class:`Partial ` now supports constructing partial models for attrs and msgspec .. change:: :class:`Annotated ` support for route handler and dependency annotations :type: feature :pr: 1462 :class:`Annotated ` can now be used in route handler and dependencies to specify additional information about the fields. .. code-block:: python @get("/") def index(param: int = Parameter(gt=5)) -> dict[str, int]: ... .. code-block:: python @get("/") def index(param: Annotated[int, Parameter(gt=5)]) -> dict[str, int]: ... .. change:: Support ``text/html`` Media-Type in ``Redirect`` response container :type: bugfix :issue: 1451 :pr: 1474 The media type in :class:`Redirect ` won't be forced to ``text/plain`` anymore and now supports setting arbitrary media types. .. change:: Fix global namespace for type resolution :type: bugfix :pr: 1477 :issue: 1472 Fix a bug where certain annotations would cause a :exc:`NameError` .. change:: Add uvicorn to ``cli`` extra :type: bugfix :issue: 1478 :pr: 1480 Add the ``uvicorn`` package to the ``cli`` extra, as it is required unconditionally .. change:: Update logging levels when setting ``Litestar.debug`` dynamically :type: bugfix :issue: 1476 :pr: 1482 When passing ``debug=True`` to :class:`Litestar `, the ``litestar`` logger would be set up in debug mode, but changing the ``debug`` attribute after the class had been instantiated did not update the logger accordingly. This lead to a regression where the ``--debug`` flag to the CLI's ``run`` command would no longer have the desired affect, as loggers would still be on the ``INFO`` level. .. changelog:: 2.0.0alpha3 .. change:: SQLAlchemy 2.0 Plugin :type: feature :pr: 1395 A :class:`SQLAlchemyInitPlugin ` was added, providing support for managed synchronous and asynchronous sessions. .. seealso:: :doc:`/usage/databases/sqlalchemy/index` .. change:: Attrs signature modelling :type: feature :pr: 1382 Added support to model route handler signatures with attrs instead of Pydantic .. change:: Support setting status codes in ``Redirect`` container :type: feature :pr: 1412 :issue: 1371 Add support for manually setting status codes in the :class:`RedirectResponse ` response container. This was previously only possible by setting the ``status_code`` parameter on the corresponding route handler, making dynamic redirect status codes and conditional redirects using this container hard to implement. .. change:: Sentinel value to support caching responses indefinitely :type: feature :pr: 1414 :issue: 1365 Add the :class:`CACHE_FOREVER ` sentinel value, that, when passed to a route handlers ``cache argument``, will cause it to be cached forever, skipping the default expiration. Additionally, add support for setting :attr:`ResponseCacheConfig.default_expiration ` to ``None``, allowing to cache values indefinitely by default when setting ``cache=True`` on a route handler. .. change:: `Accept`-header parsing and content negotiation :type: feature :pr: 1317 Add an :attr:`accept ` property to :class:`Request `, returning the newly added :class:`Accept ` header wrapper, representing the requests ``Accept`` HTTP header, offering basic content negotiation. .. seealso:: :ref:`usage/responses:Content Negotiation` .. change:: Enhanced WebSockets support :type: feature :pr: 1402 Add a new set of features for handling WebSockets, including automatic connection handling, (de)serialization of incoming and outgoing data analogous to route handlers and OOP based event dispatching. .. seealso:: :doc:`/usage/websockets` .. change:: SQLAlchemy 1 plugin mutates app state destructively :type: bugfix :pr: 1391 :issue: 1368 When using the SQLAlchemy 1 plugin, repeatedly running through the application lifecycle (as done when testing an application not provided by a factory function), would result in a :exc:`KeyError` on the second pass. This was caused be the plugin's ``on_shutdown`` handler deleting the ``engine_app_state_key`` from the application's state on application shutdown, but only adding it on application init. This was fixed by adding performing the necessary setup actions on application startup rather than init. .. change:: Fix SQLAlchemy 1 Plugin - ``'Request' object has no attribute 'dict'`` :type: bugfix :pr: 1389 :issue: 1388 An annotation such as .. code-block:: python async def provide_user(request: Request[User, Token, Any]) -> User: ... would result in the error ``'Request' object has no attribute 'dict'``. This was fixed by changing how ``get_plugin_for_value`` interacts with :func:`typing.get_args` .. change:: Support OpenAPI schema generation with stringized return annotation :type: bugfix :pr: 1410 :issue: 1409 The following code would result in non-specific and incorrect information being generated for the OpenAPI schema: .. code-block:: python from __future__ import annotations from starlite import Starlite, get @get("/") def hello_world() -> dict[str, str]: return {"hello": "world"} This could be alleviated by removing ``from __future__ import annotations``. Stringized annotations in any form are now fully supported. .. change:: Fix OpenAPI schema generation crashes for models with ``Annotated`` type attribute :type: bugfix :issue: 1372 :pr: 1400 When using a model that includes a type annotation with :class:`typing.Annotated` in a route handler, the interactive documentation would raise an error when accessed. This has been fixed and :class:`typing.Annotated` is now fully supported. .. change:: Support empty ``data`` in ``RequestFactory`` :type: bugfix :issue: 1419 :pr: 1420 Add support for passing an empty ``data`` parameter to a :class:`RequestFactory `, which would previously lead to an error. .. change:: ``create_test_client`` and ``crate_async_test_client`` signatures and docstrings to to match ``Litestar`` :type: misc :pr: 1417 Add missing parameters to :func:`create_test_client ` and :func:`create_test_client `. The following parameters were added: - ``cache_control`` - ``debug`` - ``etag`` - ``opt`` - ``response_cache_config`` - ``response_cookies`` - ``response_headers`` - ``security`` - ``stores`` - ``tags`` - ``type_encoders`` .. changelog:: 2.0.0alpha2 .. change:: Repository contrib & SQLAlchemy repository :type: feature :pr: 1254 Add a a ``repository`` module to ``contrib``, providing abstract base classes to implement the repository pattern. Also added was the ``contrib.repository.sqlalchemy`` module, implementing a SQLAlchemy repository, offering hand-tuned abstractions over commonly used tasks, such as handling of object sessions, inserting, updating and upserting individual models or collections. .. change:: Data stores & registry :type: feature :pr: 1330 :breaking: The ``starlite.storage`` module added in the previous version has been renamed ``starlite.stores`` to reduce ambiguity, and a new feature, the ``starlite.stores.registry.StoreRegistry`` has been introduced; It serves as a central place to manage stores and reduces the amount of configuration needed for various integrations. - Add ``stores`` kwarg to ``Starlite`` and ``AppConfig`` to allow seeding of the ``StoreRegistry`` - Add ``Starlite.stores`` attribute, containing a ``StoreRegistry`` - Change ``RateLimitMiddleware`` to use ``app.stores`` - Change request caching to use ``app.stores`` - Change server side sessions to use ``app.stores`` - Move ``starlite.config.cache.CacheConfig`` to ``starlite.config.response_cache.ResponseCacheConfig`` - Rename ``Starlite.cache_config`` > ``Starlite.response_cache_config`` - Rename ``AppConfig.cache_config`` > ``response_cache_config`` - Remove ``starlite/cache`` module - Remove ``ASGIConnection.cache`` property - Remove ``Starlite.cache`` attribute .. attention:: ``starlite.middleware.rate_limit.RateLimitMiddleware``, ``starlite.config.response_cache.ResponseCacheConfig``, and ``starlite.middleware.session.server_side.ServerSideSessionConfig`` instead of accepting a ``storage`` argument that could be passed a ``Storage`` instance now have to be configured via the ``store`` attribute, accepting a string key for the store to be used from the registry. The ``store`` attribute has a unique default set, guaranteeing a unique ``starlite.stores.memory.MemoryStore`` instance is acquired for every one of them from the registry by default .. seealso:: :doc:`/usage/stores` .. change:: Add ``starlite.__version__`` :type: feature :pr: 1277 Add a ``__version__`` constant to the ``starlite`` namespace, containing a :class:`NamedTuple `, holding information about the currently installed version of Starlite .. change:: Add ``starlite version`` command to CLI :type: feature :pr: 1322 Add a new ``version`` command to the CLI which displays the currently installed version of Starlite .. change:: Enhance CLI autodiscovery logic :type: feature :breaking: :pr: 1322 Update the CLI :ref:`usage/cli:autodiscovery` to only consider canonical modules app and application, but every ``starlite.app.Starlite`` instance or application factory able to return a ``Starlite`` instance within those or one of their submodules, giving priority to the canonical names app and application for application objects and submodules containing them. .. seealso:: :ref:`CLI autodiscovery ` .. change:: Configurable exception logging and traceback truncation :type: feature :pr: 1296 Add three new configuration options to ``starlite.logging.config.BaseLoggingConfig``: ``starlite.logging.config.LoggingConfig.log_exceptions`` Configure when exceptions are logged. ``always`` Always log exceptions ``debug`` Log exceptions in debug mode only ``never`` Never log exception ``starlite.logging.config.LoggingConfig.traceback_line_limit`` Configure how many lines of tracback are logged ``starlite.logging.config.LoggingConfig.exception_logging_handler`` A callable that receives three parameters - the ``app.logger``, the connection scope and the traceback list, and should handle logging .. seealso:: ``starlite.logging.config.LoggingConfig`` .. change:: Allow overwriting default OpenAPI response descriptions :type: bugfix :issue: 1292 :pr: 1293 Fix https://github.com/litestar-org/litestar/issues/1292 by allowing to overwrite the default OpenAPI response description instead of raising :exc:`ImproperlyConfiguredException`. .. change:: Fix regression in path resolution that prevented 404's being raised for false paths :type: bugfix :pr: 1316 :breaking: Invalid paths within controllers would under specific circumstances not raise a 404. This was a regression compared to ``v1.51`` .. note:: This has been marked as breaking since one user has reported to rely on this "feature" .. change:: Fix ``after_request`` hook not being called on responses returned from handlers :type: bugfix :pr: 1344 :issue: 1315 ``after_request`` hooks were not being called automatically when a ``starlite.response.Response`` instances was returned from a route handler directly. .. seealso:: :ref:`after_request` .. change:: Fix ``SQLAlchemyPlugin`` raises error when using SQLAlchemy UUID :type: bugfix :pr: 1355 An error would be raised when using the SQLAlchemy plugin with a `sqlalchemy UUID `_. This was fixed by adding it to the provider map. .. change:: Fix ``JSON.parse`` error in ReDoc and Swagger OpenAPI handlers :type: bugfix :pr: 1363 The HTML generated by the ReDoc and Swagger OpenAPI handlers would cause `JSON.parse `_ to throw an error. This was fixed by removing the call to ``JSON.parse``. .. change:: Fix CLI prints application info twice :type: bugfix :pr: 1322 Fix an error where the CLI would print application info twice on startup .. change:: Update ``SimpleEventEmitter`` to use worker pattern :type: misc :pr: 1346 ``starlite.events.emitter.SimpleEventEmitter`` was updated to using an async worker, pulling emitted events from a queue and subsequently calling listeners. Previously listeners were called immediately, making the operation effectively "blocking". .. change:: Make ``BaseEventEmitterBackend.emit`` synchronous :type: misc :breaking: :pr: 1376 ``starlite.events.emitter.BaseEventEmitterBackend``, and subsequently ``starlite.events.emitter.SimpleEventEmitter`` and ``starlite.app.Starlite.emit`` have been changed to synchronous function, allowing them to easily be used within synchronous route handlers. .. change:: Move 3rd party integration plugins to ``contrib`` :type: misc :breaking: :pr: 1279 1252 - Move ``plugins.piccolo_orm`` > ``contrib.piccolo_orm`` - Move ``plugins.tortoise_orm`` > ``contrib.tortoise_orm`` .. change:: Remove ``picologging`` dependency from the ``standard`` package extra :type: misc :breaking: :pr: 1313 `picologging `_ has been removed form the ``standard`` package extra. If you have been previously relying on this, you need to change ``pip install starlite[standard]`` to ``pip install starlite[standard,picologging]`` .. change:: Replace ``Starlite()`` ``initial_state`` keyword argument with ``state`` :type: misc :pr: 1350 :breaking: The ``initial_state`` argument to ``starlite.app.Starlite`` has been replaced with a ``state`` keyword argument, accepting an optional ``starlite.datastructures.state.State`` instance. Existing code using this keyword argument will need to be changed from .. code-block:: python from starlite import Starlite app = Starlite(..., initial_state={"some": "key"}) to .. code-block:: python from starlite import Starlite from starlite.datastructures.state import State app = Starlite(..., state=State({"some": "key"})) .. change:: Remove support for 2 argument form of ``before_send`` :type: misc :pr: 1354 :breaking: ``before_send`` hook handlers initially accepted 2 arguments, but support for a 3 argument form was added later on, accepting an additional ``scope`` parameter. Support for the 2 argument form has been dropped with this release. .. seealso:: :ref:`before_send` .. change:: Standardize module exports :type: misc :pr: 1273 :breaking: A large refactoring standardising the way submodules make their names available. The following public modules have changed their location: - ``config.openapi`` > ``openapi.config`` - ``config.logging`` > ``logging.config`` - ``config.template`` > ``template.config`` - ``config.static_files`` > ``static_files.config`` The following modules have been removed from the public namespace: - ``asgi`` - ``kwargs`` - ``middleware.utils`` - ``cli.utils`` - ``contrib.htmx.utils`` - ``handlers.utils`` - ``openapi.constants`` - ``openapi.enums`` - ``openapi.datastructures`` - ``openapi.parameters`` - ``openapi.path_item`` - ``openapi.request_body`` - ``openapi.responses`` - ``openapi.schema`` - ``openapi.typescript_converter`` - ``openapi.utils`` - ``multipart`` - ``parsers`` - ``signature`` .. changelog:: 2.0.0alpha1 .. change:: Validation of controller route handler methods :type: feature :pr: 1144 Starlite will now validate that no duplicate handlers (that is, they have the same path and same method) exist. .. change:: HTMX support :type: feature :pr: 1086 Basic support for HTMX requests and responses. .. change:: Alternate constructor ``Starlite.from_config`` :type: feature :pr: 1190 ``starlite.app.Starlite.from_config`` was added to the ``starlite.app.Starlite`` class which allows to construct an instance from an ``starlite.config.app.AppConfig`` instance. .. change:: Web concurrency option for CLI ``run`` command :pr: 1218 :type: feature A ``--wc`` / --web-concurrency` option was added to the ``starlite run`` command, enabling users to specify the amount of worker processes to use. A corresponding environment variable ``WEB_CONCURRENCY`` was added as well .. change:: Validation of ``state`` parameter in handler functions :type: feature :pr: 1264 Type annotations of the reserved ``state`` parameter in handler functions will now be validated such that annotations using an unsupported type will raise a ``starlite.exceptions.ImproperlyConfiguredException``. .. change:: Generic application state :type: feature :pr: 1030 ``starlite.connection.base.ASGIConnection`` and its subclasses are now generic on ``State`` which allow to to fully type hint a request as ``Request[UserType, AuthType, StateType]``. .. change:: Dependency injection of classes :type: feature :pr: 1143 Support using classes (not class instances, which were already supported) as dependency providers. With this, now every callable is supported as a dependency provider. .. change:: Event bus :pr: 1105 :type: feature A simple event bus system for Starlite, supporting synchronous and asynchronous listeners and emitters, providing a similar interface to handlers. It currently features a simple in-memory, process-local backend .. change:: Unified storage interfaces :type: feature :pr: 1184 :breaking: Storage backends for server-side sessions ``starlite.cache.Cache``` have been unified and replaced by the ``starlite.storages``, which implements generic asynchronous key/values stores backed by memory, the file system or redis. .. important:: This is a breaking change and you need to change your session / cache configuration accordingly .. change:: Relaxed type annotations :pr: 1140 :type: misc Type annotations across the library have been relaxed to more generic forms, for example ``Iterable[str]`` instead of ``List[str]`` or ``Mapping[str, str]`` instead of ``Dict[str, str]``. .. change:: ``type_encoders`` support in ``AbstractSecurityConfig`` :type: misc :pr: 1167 ``type_encoders`` support has been added to ``starlite.security.base.AbstractSecurityConfig``, enabling support for customized ``type_encoders`` for example in ``starlite.contrib.jwt.jwt_auth.JWTAuth``. .. change:: Renamed handler module names :type: misc :breaking: :pr: 1170 The modules containing route handlers have been renamed to prevent ambiguity between module and handler names. - ``starlite.handlers.asgi`` > ``starlite.handlers.asgi_handlers`` - ``starlite.handlers.http`` > ``starlite.handlers.http_handlers`` - ``starlite.handlers.websocket`` > ``starlite.handlers.websocket_handlers`` .. change:: New plugin protocols :type: misc :pr: 1176 :breaking: The plugin protocol has been split into three distinct protocols, covering different use cases: ``starlite.plugins.InitPluginProtocol`` Hook into an application's initialization process ``starlite.plugins.SerializationPluginProtocol`` Extend the serialization and deserialization capabilities of an application ``starlite.plugins.OpenAPISchemaPluginProtocol`` Extend OpenAPI schema generation .. change:: Unify response headers and cookies :type: misc :breaking: :pr: 1209 :ref:`response headers ` and :ref:`response cookies ` now have the same interface, along with the ``headers`` and ``cookies`` keyword arguments to ``starlite.response.Response``. They each allow to pass either a `:class:`Mapping[str, str] `, e.g. a dictionary, or a :class:`Sequence ` of ``starlite.datastructures.response_header.ResponseHeader`` or ``starlite.datastructures.cookie.Cookie`` respectively. .. change:: Replace Pydantic models with dataclasses :type: misc :breaking: :pr: 1242 Several Pydantic models used for configuration have been replaced with dataclasses or plain classes. This change should be mostly non-breaking, unless you relied on those configuration objects being Pydantic models. The changed models are: - ``starlite.config.allowed_hosts.AllowedHostsConfig`` - ``starlite.config.app.AppConfig`` - ``starlite.config.response_cache.ResponseCacheConfig`` - ``starlite.config.compression.CompressionConfig`` - ``starlite.config.cors.CORSConfig`` - ``starlite.config.csrf.CSRFConfig`` - ``starlite.logging.config.LoggingConfig`` - ``starlite.openapi.OpenAPIConfig`` - ``starlite.static_files.StaticFilesConfig`` - ``starlite.template.TemplateConfig`` - ``starlite.contrib.jwt.jwt_token.Token`` - ``starlite.contrib.jwt.jwt_auth.JWTAuth`` - ``starlite.contrib.jwt.jwt_auth.JWTCookieAuth`` - ``starlite.contrib.jwt.jwt_auth.OAuth2Login`` - ``starlite.contrib.jwt.jwt_auth.OAuth2PasswordBearerAuth`` - ``starlite.contrib.opentelemetry.OpenTelemetryConfig`` - ``starlite.middleware.logging.LoggingMiddlewareConfig`` - ``starlite.middleware.rate_limit.RateLimitConfig`` - ``starlite.middleware.session.base.BaseBackendConfig`` - ``starlite.middleware.session.client_side.CookieBackendConfig`` - ``starlite.middleware.session.server_side.ServerSideSessionConfig`` - ``starlite.response_containers.ResponseContainer`` - ``starlite.response_containers.File`` - ``starlite.response_containers.Redirect`` - ``starlite.response_containers.Stream`` - ``starlite.security.base.AbstractSecurityConfig`` - ``starlite.security.session_auth.SessionAuth`` .. change:: SQLAlchemy plugin moved to ``contrib`` :type: misc :breaking: :pr: 1252 The ``SQLAlchemyPlugin` has moved to ``starlite.contrib.sqlalchemy_1.plugin`` and will only be compatible with the SQLAlchemy 1.4 release line. The newer SQLAlchemy 2.x releases will be supported by the ``contrib.sqlalchemy`` module. .. change:: Cleanup of the ``starlite`` namespace :type: misc :breaking: :pr: 1135 The ``starlite`` namespace has been cleared up, removing many names from it, which now have to be imported from their respective submodules individually. This was both done to improve developer experience as well as reduce the time it takes to ``import starlite``. .. change:: Fix resolving of relative paths in ``StaticFilesConfig`` :type: bugfix :pr: 1256 Using a relative :class:`pathlib.Path` did not resolve correctly and result in a ``NotFoundException`` .. change:: Fix ``--reload`` flag to ``starlite run`` not working correctly :type: bugfix :pr: 1191 Passing the ``--reload`` flag to the ``starlite run`` command did not work correctly in all circumstances due to an issue with uvicorn. This was resolved by invoking uvicorn in a subprocess. .. change:: Fix optional types generate incorrect OpenAPI schemas :type: bugfix :pr: 1210 An optional query parameter was incorrectly represented as .. code-block:: { "oneOf": [ { "type": null" }, { "oneOf": [] } ]} .. change:: Fix ``LoggingMiddleware`` is sending obfuscated session id to client :type: bugfix :pr: 1228 ``LoggingMiddleware`` would in some cases send obfuscated data to the client, due to a bug in the obfuscation function which obfuscated values in the input dictionary in-place. .. change:: Fix missing ``domain`` configuration value for JWT cookie auth :type: bugfix :pr: 1223 ``starlite.contrib.jwt.jwt_auth.JWTCookieAuth`` didn't set the ``domain`` configuration value on the response cookie. .. change:: Fix https://github.com/litestar-org/litestar/issues/1201: Can not serve static file in ``/`` path :type: bugfix :issue: 1201 A validation error made it impossible to serve static files from the root path ``/`` . .. change:: Fix https://github.com/litestar-org/litestar/issues/1149: Middleware not excluding static path :type: bugfix :issue: 1149 A middleware's ``exclude`` parameter would sometimes not be honoured if the path was used to serve static files using ``StaticFilesConfig`` litestar-2.16.0/docs/release-notes/index.rst000066400000000000000000000003301500564371300207660ustar00rootroot00000000000000:orphan: Release notes ============= .. toctree:: :titlesonly: whats-new-2 2.x Changelog 1.x Changelog litestar-2.16.0/docs/release-notes/whats-new-2.rst000066400000000000000000001156671500564371300217570ustar00rootroot00000000000000.. py:currentmodule:: litestar What's changed in 2.0? ====================== This document is an overview of the changes between version **1.51** and **2.0**. For a detailed list of all changes, including changes between versions leading up to the 2.0 release, consult the :doc:`/release-notes/changelog`. Starlite → Litestar ------------------- We're thrilled to introduce some exciting changes in our latest release, version 2! The most noteworthy transformation you will notice is the rebranding of our project, previously known as Starlite, now stepping into the limelight as Litestar. The name "Starlite" was chosen as an homage to `Starlette `_, the ASGI framework and toolkit Starlite was initially based on. Over the course of its development, Starlite grew more independent and relied less on Starlette, up to the point were Starlette was officially removed as a dependency in November 2022, with the release of `v1.39.0 `_. After careful consideration, it was decided that with the release of 2.0, Starlite would be renamed to Litestar. There were many factors contributing to this decision, but it was mainly driven by concerns from within and outside the community about the possible confusion of the names *Starlette* and *Starlite* which - not incidentally - bore a lot of resemblance, which now had outlived its purpose. **** Aside from the name, Litestar 2.0 is a direct successor of Starlite 1.x, and the regular release cycle will continue. It was determined that making the first 2.0 release under the new name and continuing with the version scheme from Starlite would cause the least friction. Following that decision, the first release under the new name was `v2.0.0alpha3 `_, following the last alpha release of Starlite 2.0, `v2.0.0alpha2 `_. .. note:: The **1.51** release line is unaffected by this change Imports ------- +----------------------------------------------------+------------------------------------------------------------------------+ | ``1.51`` | ``2.x`` | +====================================================+========================================================================+ | ``starlite.ASGIConnection`` | :class:`.connection.ASGIConnection` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.Partial`` | replaced with DTOs | +----------------------------------------------------+------------------------------------------------------------------------+ | **Enums** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.RequestEncodingType`` | :class:`.enums.RequestEncodingType` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.ScopeType`` | :class:`.enums.ScopeType` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.OpenAPIMediaType`` | :class:`.enums.OpenAPIMediaType` | +----------------------------------------------------+------------------------------------------------------------------------+ | **Datastructures** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.BackgroundTask`` | :class:`.background_tasks.BackgroundTask` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.BackgroundTasks`` | :class:`.background_tasks.BackgroundTasks` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.State`` | :class:`.datastructures.State` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.ImmutableState`` | :class:`.datastructures.ImmutableState` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.Cookie`` | :class:`.datastructures.Cookie` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.FormMultiDict`` | :class:`.datastructures.FormMultiDict` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.ResponseHeader`` | :class:`.datastructures.ResponseHeader` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.UploadFile`` | :class:`.datastructures.UploadFile` | +----------------------------------------------------+------------------------------------------------------------------------+ | **Configuration** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.AllowedHostsConfig`` | :class:`.config.allowed_hosts.AllowedHostsConfig` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.AbstractSecurityConfig`` | :class:`.security.AbstractSecurityConfig` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.CacheConfig`` | :class:`.config.response_cache.ResponseCacheConfig` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.CompressionConfig`` | :class:`.config.compression.CompressionConfig` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.CORSConfig`` | :class:`.config.cors.CORSConfig` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.CSRFConfig`` | :class:`.config.csrf.CSRFConfig` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.OpenAPIConfig`` | :class:`.openapi.OpenAPIConfig` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.StaticFilesConfig`` | :class:`.static_files.config.StaticFilesConfig` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.TemplateConfig`` | :class:`.template.TemplateConfig` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.BaseLoggingConfig`` | :class:`.logging.config.BaseLoggingConfig` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.LoggingConfig`` | :class:`.logging.config.LoggingConfig` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.StructLoggingConfig`` | :class:`.logging.config.StructLoggingConfig` | +----------------------------------------------------+------------------------------------------------------------------------+ | **Provide** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.datastructures.Provide`` | :class:`.di.Provide` | +----------------------------------------------------+------------------------------------------------------------------------+ | **Pagination** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.AbstractAsyncClassicPaginator`` | :class:`.pagination.AbstractAsyncClassicPaginator` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.AbstractAsyncCursorPaginator`` | :class:`.pagination.AbstractAsyncCursorPaginator` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.AbstractAsyncOffsetPaginator`` | :class:`.pagination.AbstractAsyncOffsetPaginator` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.AbstractSyncClassicPaginator`` | :class:`.pagination.AbstractSyncClassicPaginator` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.AbstractSyncCursorPaginator`` | :class:`.pagination.AbstractSyncCursorPaginator` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.AbstractSyncOffsetPaginator`` | :class:`.pagination.AbstractSyncOffsetPaginator` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.ClassicPagination`` | :class:`.pagination.ClassicPagination` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.CursorPagination`` | :class:`.pagination.CursorPagination` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.OffsetPagination`` | :class:`.pagination.OffsetPagination` | +----------------------------------------------------+------------------------------------------------------------------------+ | **Response Containers** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.File`` | :class:`.response.File` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.Redirect`` | :class:`.response.Redirect` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.ResponseContainer`` | :class:`.response.Response` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.Stream`` | :class:`.response.Stream` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.Template`` | :class:`.response.Template` | +----------------------------------------------------+------------------------------------------------------------------------+ | **Exceptions** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.HTTPException`` | :class:`.exceptions.HTTPException` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.ImproperlyConfiguredException`` | :class:`.exceptions.ImproperlyConfiguredException` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.InternalServerException`` | :class:`.exceptions.InternalServerException` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.MissingDependencyException`` | :class:`.exceptions.MissingDependencyException` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.NoRouteMatchFoundException`` | :class:`.exceptions.NoRouteMatchFoundException` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.NotAuthorizedException`` | :class:`.exceptions.NotAuthorizedException` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.NotFoundException`` | :class:`.exceptions.NotFoundException` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.PermissionDeniedException`` | :class:`.exceptions.PermissionDeniedException` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.ServiceUnavailableException`` | :class:`.exceptions.ServiceUnavailableException` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.StarliteException`` | :class:`.exceptions.LitestarException` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.TooManyRequestsException`` | :class:`.exceptions.TooManyRequestsException` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.ValidationException`` | :class:`.exceptions.ValidationException` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.WebSocketException`` | :class:`.exceptions.WebSocketException` | +----------------------------------------------------+------------------------------------------------------------------------+ | **Testing** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.TestClient`` | :class:`.testing.TestClient` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.AsyncTestClient`` | :class:`.testing.AsyncTestClient` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.create_test_client`` | :class:`.testing.create_test_client` | +----------------------------------------------------+------------------------------------------------------------------------+ | **OpenAPI** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.OpenAPIController`` | :class:`.openapi.controller.OpenAPIController` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.ResponseSpec`` | :class:`.openapi.datastructures.ResponseSpec` | +----------------------------------------------------+------------------------------------------------------------------------+ | **Middleware** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.AbstractAuthenticationMiddleware`` | :class:`.middleware.authentication.AbstractAuthenticationMiddleware` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.AuthenticationResult`` | :class:`.middleware.authentication.AuthenticationResult` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.AbstractMiddleware`` | :class:`.middleware.AbstractMiddleware` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.DefineMiddleware`` | :class:`.middleware.DefineMiddleware` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.MiddlewareProtocol`` | :class:`.middleware.MiddlewareProtocol` | +----------------------------------------------------+------------------------------------------------------------------------+ | **Security** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.AbstractSecurityConfig`` | :class:`.security.AbstractSecurityConfig` | +----------------------------------------------------+------------------------------------------------------------------------+ | **Route Handlers** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.handlers.asgi`` | :mod:`.handlers` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.handlers.http`` | :mod:`.handlers` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.handlers.websocket`` | :class:`.handlers` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.ASGIRouteHandler`` | :class:`.handlers.ASGIRouteHandler` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.BaseRouteHandler`` | :class:`.handlers.BaseRouteHandler` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.HTTPRouteHandler`` | :class:`.handlers.HTTPRouteHandler` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.WebsocketRouteHandler`` | :class:`.handlers.WebsocketRouteHandler` | +----------------------------------------------------+------------------------------------------------------------------------+ | **Routes** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.ASGIRoute`` | :class:`.routes.ASGIRoute` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.BaseRoute`` | :class:`.routes.BaseRoute` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.HTTPRoute`` | :class:`.routes.HTTPRoute` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.WebSocketRoute`` | :class:`.routes.WebSocketRoute` | +----------------------------------------------------+------------------------------------------------------------------------+ | **Parameters** | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.Body`` | :class:`.params.Body` | +----------------------------------------------------+------------------------------------------------------------------------+ | ``starlite.Parameter`` | :class:`.params.Parameter` | +----------------------------------------------------+------------------------------------------------------------------------+ Response headers ---------------- Response header can now be set using either a :class:`Sequence ` of :class:`ResponseHeader <.datastructures.response_header.ResponseHeader>`, or by using a plain :class:`Mapping[str, str] `. The typing of :class:`ResponseHeader <.datastructures.response_header.ResponseHeader>` was also changed to be more strict and now only allows string values. .. code-block:: python :caption: 1.51 from starlite import ResponseHeader, get @get(response_headers={"my-header": ResponseHeader(value="header-value")}) async def handler() -> str: ... .. code-block:: python :caption: 2.x from litestar import ResponseHeader, get @get(response_headers=[ResponseHeader(name="my-header", value="header-value")]) async def handler() -> str: ... # or @get(response_headers={"my-header": "header-value"}) async def handler() -> str: ... Response cookies ---------------- Response cookies might now also be set using a :class:`Mapping[str, str] `, analogous to `Response headers`_. .. code-block:: python @get("/", response_cookies=[Cookie(key="foo", value="bar")]) async def handler() -> None: ... is equivalent to .. code-block:: python @get("/", response_cookies={"foo": "bar"}) async def handler() -> None: ... SQLAlchemy Plugin ----------------- Support for SQLAlchemy 1 has been dropped and the new plugin will now support SQLAlchemy 2 only. TODO: Migration instructions .. seealso:: The :doc:`/usage/databases/sqlalchemy/index` usage documentation and the :doc:`/reference/contrib/sqlalchemy/index` API reference Removal of Pydantic models -------------------------- Several Pydantic models used for configuration have been replaced with dataclasses or plain classes. If you relied on implicit data conversion from these models or subclassed them, you might need to adjust your code accordingly. .. seealso:: :ref:`change:2.0.0alpha1-replace pydantic models with dataclasses` Plugin protocols ---------------- The plugin protocol has been split into three distinct protocols, covering different use cases: :class:`litestar.plugins.InitPluginProtocol` Hook into an application's initialization process :class:`litestar.plugins.SerializationPluginProtocol` Extend the serialization and deserialization capabilities of an application :class:`litestar.plugins.OpenAPISchemaPluginProtocol` Extend OpenAPI schema generation Plugins that made use of all features of the previous API should simply inherit from all three base classes. Remove 2 argument ``before_send`` --------------------------------- The 2 argument for of ``before_send`` hook handlers has been removed. Existing handlers should be changed to include an additional ``scope`` parameter. .. code-block:: python :caption: 1.51 async def before_send(message: Message, state: State) -> None: ... .. code-block:: python :caption: 2.x async def before_send(message: Message, state: State, scope: Scope) -> None: ... .. seealso:: :ref:`change:2.0.0alpha2-remove support for 2 argument form of` ``before_send`` and the :ref:`before_send` API reference ``initial_state`` application parameter --------------------------------------- The ``initial_state`` argument to :class:`~litestar.app.Litestar` has been replaced with a ``state`` keyword argument, accepting an optional :class:`~litestar.datastructures.state.State` instance. Existing code using this keyword argument will need to be changed from .. code-block:: python :caption: 1.51 app = Starlite(..., initial_state={"some": "key"}) to .. code-block:: python :caption: 2.x app = Litestar(..., state=State({"some": "key"})) Stores ------ A new module, ``litestar.stores`` has been introduced, which replaces the previously used ``starlite.cache.Cache`` and server-side session storage backends. These stores provide a low-level, asynchronous interface for common key/value stores such as Redis and an in-memory implementation. They are currently used for server-side sessions, caching and rate limiting. Stores are integrated into the :class:`~app.Litestar` application object via the :class:`~.stores.registry.StoreRegistry`, which can be used to register and access stores as well as provide defaults. .. literalinclude:: /examples/stores/get_set.py :language: python .. literalinclude:: /examples/stores/namespacing.py :language: python :caption: Using namespacing .. literalinclude:: /examples/stores/registry.py :language: python :caption: Using the registry .. seealso:: The :doc:`/usage/stores` usage documentation Usage of the ``stores`` for caching and other integrations ----------------------------------------------------------- The newly introduced :doc:`stores ` have superseded the removed ``starlite.cache`` module in various places. The following now make use of stores: - :class:`~litestar.middleware.rate_limit.RateLimitMiddleware` - :class:`~litestar.config.response_cache.ResponseCacheConfig` - :class:`~litestar.middleware.session.server_side.ServerSideSessionConfig` The following attributes have been renamed to reduce ambiguity: - ``Starlite.cache_config`` > ``Litestar.response_cache_config`` - ``AppConfig.cache_config`` > :attr:`~litestar.config.app.AppConfig.response_cache_config` In addition, the ``ASGIConnection.cache`` property has been removed. It can be replaced by accessing the store directly as described in :doc:`stores ` DTOs ---- Data Transfer Objects are now defined using the ``dto`` and ``return_dto`` arguments to handlers/controllers/routers and the application. A DTO is any type that inherits from :class:`litestar.dto.base_dto.AbstractDTO`. Litestar provides a suite of types that implement the ``AbstractDTO`` abstract class and can be used to define DTOs: - :class:`litestar.dto.dataclass_dto.DataclassDTO` - :class:`litestar.dto.msgspec_dto.MsgspecDTO` - :class:`advanced_alchemy.extensions.litestar.dto.SQLAlchemyDTO` - :class:`litestar.contrib.pydantic.PydanticDTO` - :class:`litestar.contrib.piccolo.PiccoloDTO` For example, to define a DTO from a dataclass: .. code-block:: python from dataclasses import dataclass from litestar import get from litestar.dto import DTOConfig, DataclassDTO @dataclass class MyType: some_field: str another_field: int class MyDTO(DataclassDTO[MyType]): config = DTOConfig(exclude={"another_field"}) @get(dto=MyDTO) async def handler() -> MyType: return MyType(some_field="some value", another_field=42) .. literalinclude:: /examples/data_transfer_objects/the_return_dto_parameter.py :language: python .. literalinclude:: /examples/data_transfer_objects/factory/renaming_fields.py :language: python :caption: Renaming fields .. literalinclude:: /examples/data_transfer_objects/factory/excluding_fields.py :language: python :caption: Excluding fields .. literalinclude:: /examples/data_transfer_objects/factory/marking_fields.py :language: python :caption: Marking fields .. seealso:: The :doc:`/usage/dto/index` usage documentation Application lifespan hooks -------------------------- All application lifespan hooks have been merged into ``on_startup`` and ``on_shutdown``. The following hooks have been removed: - ``before_startup`` - ``after_startup`` - ``before_shutdown`` - ``after_shutdown`` ``on_startup`` and ``on_shutdown`` now optionally receive the application instance as their first parameter. If your ``on_startup`` and ``on_shutdown`` hooks made use of the application state, they will now have to access it through the provided application instance. .. code-block:: python :caption: 1.51 def on_startup(state: State) -> None: print(state.something) .. code-block:: python :caption: 2.x def on_startup(app: Litestar) -> None: print(app.state.something) Dependencies without ``Provide`` -------------------------------- Dependencies may now be declared without :class:`~litestar.di.Provide`, by passing the callable directly. This can be advantageous in places where the configuration options of :class:`~litestar.di.Provide` are not needed. .. code-block:: python async def some_dependency() -> str: ... app = Litestar(dependencies={"some": Provide(some_dependency)}) is equivalent to .. code-block:: python async def some_dependency() -> str: ... app = Litestar(dependencies={"some": some_dependency}) ``sync_to_thread`` ------------------ The ``sync_to_thread`` option can be used to run a synchronous callable provided to a route handler or :class:`~litestar.di.Provide` inside a thread pool. Since synchronous functions may block the main thread when not used with ``sync_to_thread=True``, a warning will be raised in these cases. If the synchronous function should not be run in a thread pool, passing ``sync_to_thread=False`` will also silence the warning. .. tip:: The warning can be disabled entirely by setting the environment variable ``LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD=0`` .. code-block:: python :caption: 1.51 @get() def handler() -> None: ... .. code-block:: python :caption: 2.x @get(sync_to_thread=False) def handler() -> None: ... or .. code-block:: python :caption: 2.x @get(sync_to_thread=True) def handler() -> None: ... .. seealso:: The :doc:`/topics/sync-vs-async` topic guide HTMX ---- Basic support for HTMX requests and responses was added with the ``litestar.contrib.htmx`` module. .. seealso:: The :doc:`/usage/htmx` usage documentation Event bus --------- A simple event bus system for Litestar, supporting synchronous and asynchronous listeners and emitters, providing a similar interface to handlers. It currently features a simple in-memory, process-local backend. .. seealso:: The :doc:`/usage/events` usage documentation and the :doc:`/reference/events` API reference Enhanced WebSocket support -------------------------- A new set of features for handling WebSockets, including automatic connection handling, (de)serialization of incoming and outgoing data analogous to route handlers, OOP based event dispatching, data iterators and more. .. literalinclude:: /examples/websockets/listener_class_based.py :caption: Using a class based listener :language: python .. literalinclude:: /examples/websockets/mode_send_text.py :caption: Echo text :language: python .. literalinclude:: /examples/websockets/sending_json_dataclass.py :caption: Wrapping data in a dataclass :language: python .. literalinclude:: /examples/websockets/with_dto.py :language: python .. code-block:: python :caption: Receiving JSON and sending it back as MessagePack from litestar import websocket, WebSocket @websocket("/") async def handler(socket: WebSocket) -> None: await socket.accept() async for message in socket.iter_data(mode): await socket.send_msgpack(message) .. seealso:: * :ref:`change:2.0.0alpha3-enhanced websockets support` * :ref:`change:2.0.0alpha6-websockets: managing a socket's lifespan using a context manager in websocket listeners` * :ref:`change:2.0.0alpha6-websockets: messagepack support` * :ref:`change:2.0.0alpha6-websockets: data iterators` * The :doc:`/usage/websockets` usage documentation Attrs signature modelling ------------------------- TBD :class:`~typing.Annotated` support in route handlers ---------------------------------------------------- :class:`Annotated ` can now be used in route handler and dependencies to specify additional information about the fields .. code-block:: python @get("/") def index(param: int = Parameter(gt=5)) -> dict[str, int]: ... .. code-block:: python @get("/") def index(param: Annotated[int, Parameter(gt=5)]) -> dict[str, int]: ... Channels --------- :doc:`/usage/channels` are a general purpose event streaming module, which can for example be used to broadcast messages via WebSockets and includes functionalities such as automatically generating WebSocket route handlers to broadcast messages. .. literalinclude:: /examples/channels/run_in_background.py :language: python .. seealso:: The :doc:`channels ` usage documentation Application lifespan context managers -------------------------------------- A new ``lifespan`` argument has been added to :class:`~litestar.app.Litestar`, accepting an asynchronous context manager, wrapping the lifespan of the application. It will be entered with the startup phase and exited on shutdown, providing functionality equal to the ``on_startup`` and ``on_shutdown`` hooks. .. literalinclude:: /examples/application_hooks/lifespan_manager.py :language: python Response types -------------- Starlite had the concept of Response Containers, which were datatypes used to indicate the type of response returned by a handler. These included ``File``, ``Redirect``, ``Template`` and ``Stream`` types. These types abstracted the interface of responses from the underlying response itself. In Litestar, these types still exist, however they are now subclasses of :class:`Response <.response.Response>` and are imported from the ``litestar.response`` module. In contrast to Starlite's Response Containers, these types have more utility for interacting with the outgoing response, such as methods to add headers and cookies. Otherwise, their usage should remain very similar to Starlite. Litestar also introduces a new layer of ASGI response type, based on :class:`ASGIResponse <.response.base.ASGIResponse>`. These types represent the response as an immutable object and are used internally by Litestar to perform the I/O operations of the response. These can be created and returned from handlers, however they are low-level, and lack the utility of the higher-level response types. litestar-2.16.0/docs/topics/000077500000000000000000000000001500564371300156645ustar00rootroot00000000000000litestar-2.16.0/docs/topics/deployment/000077500000000000000000000000001500564371300200445ustar00rootroot00000000000000litestar-2.16.0/docs/topics/deployment/docker.rst000066400000000000000000000147541500564371300220600ustar00rootroot00000000000000Docker ====== Docker is a containerization platform that allows you to package your application and all its dependencies together. It is useful for creating consistent environments for your application to run in, irrespective of the host system and its own configuration or dependencies - which is especially helpful in preventing dependency conflicts. This guide uses the `Docker official Python container `_ as a base image. Use When -------- Docker is ideal for deploying Python web applications in scenarios where: - **Isolation:** You require a consistent, isolated environment for your application, independent of the host system. - **Scalability:** Your application needs to be easily scaled up or down based on demand. - **Portability:** The need to run your application consistently across different environments (development, testing, production) is crucial. - **Microservices Architecture:** You are adopting a microservices architecture, where each service can be containerized and managed independently. - **Continuous Integration/Continuous Deployment (CI/CD):** You are implementing CI/CD pipelines, and Docker facilitates the building, testing, and deployment of applications. - **Dependency Management:** Ensuring that your application has all its dependencies bundled together without conflicts with other applications. Alternatives ~~~~~~~~~~~~ For different deployment scenarios, consider these alternatives: - `systemd `_: A system and service manager, integrated into many Linux distributions for managing system processes. .. note:: Official documentation coming soon - :doc:`Supervisor `: A process control system that can be used to automatically start, stop and restart processes; includes a web UI. - :doc:`Manually with an ASGI server `: Direct control by running the application with an ASGI server like Uvicorn, Hypercorn, Daphne, etc. This guide assumes that you have Docker installed and running on your system, and that you have the following files in your project directory: .. code-block:: shell :caption: ``requirements.txt`` litestar[standard]>=2.4.0,<3.0.0 .. code-block:: python :caption: ``app.py`` """Minimal Litestar application.""" from asyncio import sleep from typing import Any, Dict from litestar import Litestar, get @get("/") async def async_hello_world() -> Dict[str, Any]: """Route Handler that outputs hello world.""" await sleep(0.1) return {"hello": "world"} @get("/sync", sync_to_thread=False) def sync_hello_world() -> Dict[str, Any]: """Route Handler that outputs hello world.""" return {"hello": "world"} app = Litestar(route_handlers=[sync_hello_world, async_hello_world]) Dockerfile ---------- .. code-block:: docker :caption: Example Dockerfile # Set the base image using Python 3.12 and Debian Bookworm FROM python:3.12-slim-bookworm # Set the working directory to /app WORKDIR /app # Copy only the necessary files to the working directory COPY . /app # Install the requirements RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt # Expose the port the app runs on EXPOSE 80 # Run the app with the Litestar CLI CMD ["litestar", "run", "--host", "0.0.0.0", "--port", "80"] This copies your local project folder to the ``/app`` directory in the Docker container and runs your app via ``uvicorn`` utilizing the ``litestar run`` command. ``uvicorn`` is provided by the ``litestar[standard]`` extra, which is installed in the ``requirements.txt`` file. You can also launch the application with an :doc:`ASGI server ` directly, if you prefer. Once you have your ``Dockerfile`` defined, you can build the image with ``docker build`` and run it with ``docker run``. .. dropdown:: Useful Dockerfile Commands .. code-block:: shell :caption: Useful Docker commands # Build the container docker build -t exampleapp . # Run the container docker run -d -p 80:80 --name exampleapp exampleapp # Stop the container docker stop exampleapp # Start the container docker start exampleapp # Remove the container docker rm exampleapp Docker Compose -------------- Compose is a tool for defining and running multi-container Docker applications. Read more about Compose in the `official Docker documentation `_. If you want to run the container as part of a Docker Compose setup then you can simply use this compose file: .. code-block:: yaml :caption: ``docker-compose.yml`` version: "3.9" services: exampleapp: build: context: ./ dockerfile: Dockerfile container_name: "exampleapp" depends_on: - database ports: - "80:80" environment: - DB_HOST=database - DB_PORT=5432 - DB_USER=litestar - DB_PASS=r0cks - DB_NAME=exampleapp database: image: postgres:latest container_name: "exampledb" environment: POSTGRES_USER: exampleuser POSTGRES_PASSWORD: examplepass POSTGRES_DB: exampledb ports: - "5432:5432" volumes: - db_data:/var/lib/postgresql/data volumes: db_data: This compose file defines two services: ``exampleapp`` and ``database``. The ``exampleapp`` service is built from the Dockerfile in the current directory, and exposes port 80. The ``database`` service uses the official PostgreSQL image, and exposes port ``5432``. The ``exampleapp`` service depends on the ``database`` service, so the database will be started before the app. The ``exampleapp`` service also has environment variables set for the database connection details, which are used by the app. Once you have your ``docker-compose.yml`` defined, you can run ``docker compose up`` to start the containers. You can also run ``docker compose up -d`` to run the containers in the background, or "detached" mode. .. dropdown:: Useful Compose Commands .. code-block:: shell :caption: Useful Docker Compose commands # Build the containers docker compose build # Run the containers docker compose up # Run the containers in the background docker compose up -d # Stop the containers docker compose down litestar-2.16.0/docs/topics/deployment/index.rst000066400000000000000000000005001500564371300217000ustar00rootroot00000000000000Deployment ========== This section contains articles about deploying Litestar applications to various platforms and environments; this includes deploying with ``systemd``, Docker, Kubernetes, serverless, and more. Contents -------- .. toctree:: nginx-unit manually-with-asgi-server docker supervisor litestar-2.16.0/docs/topics/deployment/manually-with-asgi-server.rst000066400000000000000000000125441500564371300256240ustar00rootroot00000000000000Manually with ASGI server ========================= ASGI (Asynchronous Server Gateway Interface) is intended to provide a standard interface between async Python web frameworks like Litestar, and async web servers. There are several popular ASGI servers available, and you can choose the one that best fits your application's needs. Use When -------- Running your application manually with an ASGI server is usually only ideal in development and testing environments. It is generally recommended to run your production workloads inside a containerized environment, such as :doc:`Docker ` or Kubernetes or via a process control system such as :doc:`Supervisor ` or ``systemd``. Alternatives ~~~~~~~~~~~~ For different deployment scenarios, consider these alternatives: - :doc:`NGINX Unit `: A dynamic web and application server, suitable for running and managing multiple applications. - `systemd `_: A system and service manager, integrated into many Linux distributions for managing system processes. .. note:: Official documentation coming soon - :doc:`Supervisor `: A process control system that can be used to automatically start, stop and restart processes; includes a web UI. - :doc:`Docker `: Ideal for containerized environments, offering isolation and scalability. Choosing an ASGI Server ----------------------- .. tab-set:: .. tab-item:: Uvicorn :sync: uvicorn `Uvicorn `_ is an ASGI server that supports ``HTTP/1.1`` and WebSocket. .. tab-item:: Hypercorn :sync: hypercorn `Hypercorn `_ is an ASGI server that was initially part of `Quart `_, and supports ``HTTP/1.1``, ``HTTP/2``, and WebSocket. .. tab-item:: Daphne :sync: daphne `Daphne `_ is an ASGI server that was originally developed for `Django Channels `_, and supports ``HTTP/1.1``, ``HTTP/2``, and WebSocket. .. tab-item:: Granian :sync: granian `Granian `_ is a Rust-based ASGI server that supports ``HTTP/1.1``, ``HTTP/2``, and WebSocket. Install the ASGI Server ----------------------- .. tab-set:: .. tab-item:: Uvicorn :sync: uvicorn .. code-block:: shell :caption: Install Uvicorn with pip pip install uvicorn .. tab-item:: Hypercorn :sync: hypercorn .. code-block:: shell :caption: Install Hypercorn with pip pip install hypercorn .. tab-item:: Daphne :sync: daphne .. code-block:: shell :caption: Install Daphne with pip pip install daphne .. tab-item:: Granian :sync: granian .. code-block:: shell :caption: Install Granian with pip pip install granian Run the ASGI Server ------------------- Assuming your app is defined in the same manner as :ref:`Minimal Example `, you can run the ASGI server with the following command: .. tab-set:: .. tab-item:: Uvicorn :sync: uvicorn .. code-block:: shell :caption: Run Uvicorn with the default configuration uvicorn app:app .. code-block:: console :caption: Console Output INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) .. tab-item:: Hypercorn :sync: hypercorn .. code-block:: shell :caption: Run Hypercorn with the default configuration hypercorn app:app .. code-block:: console :caption: Console Output [2023-11-12 23:31:26 -0800] [16748] [INFO] Running on http://127.0.0.1:8000 (CTRL + C to quit) .. tab-item:: Daphne :sync: daphne .. code-block:: shell :caption: Run Daphne with the default configuration daphne app:app .. code-block:: console :caption: Console Output INFO - 2023-11-12 23:31:51,571 - daphne.cli - cli - Starting server at tcp:port=8000:interface=127.0.0.1 INFO - 2023-11-12 23:31:51,572 - daphne.server - server - Listening on TCP address 127.0.0.1:8000 .. tab-item:: Granian :sync: granian .. code-block:: shell :caption: Run Granian with the default configuration granian --interface asgi app:app .. code-block:: console :caption: Console Output [INFO] Starting granian [INFO] Listening at: 127.0.0.1:8000 Gunicorn with Uvicorn workers ----------------------------- .. important:: **Deprecation Notice** The Gunicorn+Uvicorn pattern is considered legacy for ASGI deployments since `Uvicorn 0.30.0+ `_ includes native worker management. Uvicorn added a new multiprocess manager, that is meant to replace Gunicorn entirely. Refer to the pull request `#2183 `_ for implementation details. For new deployments, use Uvicorn directly. litestar-2.16.0/docs/topics/deployment/nginx-unit.rst000066400000000000000000000216021500564371300226770ustar00rootroot00000000000000NGINX Unit ========== ``nginx-unit`` is a dynamic web application server, designed to run applications in multiple languages, serve static files, and more. Use When -------- Running your application with ``nginx-unit`` is preferable when you need to run your application in a production environment, with a high level of control over the process. For detailed understanding and further information, refer to the official `nginx-unit documentation `_. Alternatives ++++++++++++ - :doc:`Manually with an ASGI server `: Direct control by running the application with an ASGI server like Uvicorn, Hypercorn, Daphne, etc. - `systemd `_: A system and service manager, integrated into many Linux distributions for managing system processes. .. note:: Official documentation coming soon - :doc:`Supervisor `: A process control system that can be used to automatically start, stop and restart processes; includes a web UI. - :doc:`Docker `: Ideal for containerized environments, offering isolation and scalability. .. note:: You can deploy ``nginx-unit`` with Docker using the `official NGINX image `_. Install ``nginx-unit`` ---------------------- To install ``nginx-unit``, refer to the `official documentation `_ .. tab-set:: .. tab-item:: macOS (`Brew `_) .. literalinclude:: /examples/deployment/nginx-unit/install-macos.sh :language: sh .. tab-item:: Ubuntu To be done Start the process, replace ``user`` by your system user. .. code-block:: sh :caption: Start ``nginx-unit`` unitd --user 3. Create a ``run.py`` file containing the reference of your Litestar app .. literalinclude:: /examples/todo_app/hello_world.py :language: python :caption: ``run.py`` Configuration ------------- Create a file called ``unit.json``, put it at the root of the your project .. literalinclude:: /examples/deployment/nginx-unit/unit.json :language: json :caption: ``unit.json`` Listeners +++++++++ To accept requests, add a listener object in the ``config/listeners`` API section; the object’s name can be: - A unique IP socket: ``127.0.0.1:80``, ``[::1]:8080`` - A wildcard that matches any host IPs on the port: ``*:80`` - On Linux-based systems, abstract UNIX sockets can be used as well: ``unix:@abstract_socket``. Applications ++++++++++++ Each app that Unit runs is defined as an object in the ``/config/applications`` section of the control API; it lists the app’s language and settings, runtime limits, process model, and various language-specific options. +------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | Option | Value | Description | +------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``type`` (required) | ``python 3.12`` | Application type: ``python`` ``"type": "python 3"``, ``"type": "python 3.12"`` | | | | | | | | Unit searches its modules and uses the latest matching one, reporting an error if none match. | +------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``home`` | ``venv`` | String; path to the app’s virtual environment. Absolute or relative to ``working_directory``. | +------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``path`` | ``src/app`` | String or an array of strings; additional Python module lookup paths. These values are prepended to ``sys.path``. | +------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``module`` (required) | ``run`` | String; app’s module name. This module is imported by Unit the usual Python way. | +------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``callable`` | ``app`` | String; name of the module-based callable that Unit runs as the app. The default is `application`. | +------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``working_directory`` | ```` | String; the app’s working directory. The default is the working directory of Unit’s main process. | +------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``stderr``, ``stdout`` | ``log_error.log`` | Strings; filenames where Unit redirects the application’s output. | | | | | | | | The default is ``/dev/null``. | +------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``user`` | | String; username that runs the app process. | +------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``group`` | | String; group name that runs the app process. The default is the ``user``’s primary group. | +------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``processes`` | ``1`` | Integer or object; integer sets a static number of app processes, and object options `max`, `spare`, and `idle_timeout` enable dynamic management. | | | | | | | | The default is ``1``. | +------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ Configuration update -------------------- To update the ``nginx-unit`` service already running, use ``PUT`` method to send the ``unit.json`` file on the ``/config`` endpoint .. code-block:: sh :caption: Update ``nginx-unit`` configuration curl -X PUT --data-binary @unit.json --unix-socket /opt/homebrew/var/run/unit/control.sock http://localhost/config litestar-2.16.0/docs/topics/deployment/supervisor.rst000066400000000000000000000220321500564371300230160ustar00rootroot00000000000000Supervisor (Linux) ================== ``supervisor`` is a process control system for Linux, which allows you to monitor and control a number of processes on UNIX-like operating systems. It is particularly useful for managing processes that need to be running continuously, and for monitoring and controlling processes that are running in the background. Use When -------- ``supervisor`` is ideal for managing Python web applications that need continuous uptime and robust process control. It's particularly useful when you need extensive process management, monitoring, log management, and customized control over the start, stop, and restart of application processes. For detailed understanding and further information, refer to the official `Supervisor documentation `_. Alternatives ~~~~~~~~~~~~ For different deployment scenarios, consider these alternatives: - :doc:`Docker `: Ideal for containerized environments, offering isolation and scalability. - :doc:`NGINX Unit `: A dynamic web and application server, suitable for running and managing multiple applications. - `systemd `_: A system and service manager, integrated into many Linux distributions for managing system processes. .. note:: Official documentation coming soon - :doc:`Manually with an ASGI server `: Direct control by running the application with an ASGI server like Uvicorn, Hypercorn, Daphne, etc. This resource provides comprehensive guidance on installation, configuration, and usage of ``supervisor`` for service management. .. _conf_file: Setup ----- ``supervisor`` uses a config file for defining services. You can read more about the config file in the `Supervisor configuration documentation `_. .. code-block:: ini :caption: Example supervisor config file [program:exampleapp] # Defines the service name. directory=/opt/exampleapp/src # Specifies the directory where the service should run. command=/opt/exampleapp/venv/bin/litestar app.py # Specifies the command to run the service. redirect_stderr=true # Redirects stderr to stdout. stdout_logfile=/var/log/exampleapp.log # Specifies the log file to write to. stdout_logfile_backups=10 # Specifies the number of backups to keep. autostart=true # Specifies that the service should start automatically when ``supervisor`` starts. autorestart=true # Specifies that the service should restart automatically if it exits unexpectedly. You can place the above config file in ``/etc/supervisor/conf.d/exampleapp.conf``. After you have created the config file, you will need to reload the ``supervisor`` config to load your new service file. .. dropdown:: Helpful Commands .. code-block:: shell :caption: Reload supervisor config sudo supervisorctl reread sudo supervisorctl update .. code-block:: shell :caption: Start/Stop/Restart/Status sudo supervisorctl start exampleapp sudo supervisorctl stop exampleapp sudo supervisorctl restart exampleapp sudo supervisorctl status exampleapp .. code-block:: shell :caption: View logs sudo supervisorctl tail -f exampleapp Now you are ready to start your application. #. Start the service if it's not already running: ``sudo supervisorctl start exampleapp``. #. Ensure it's operating correctly by checking the output for errors: ``sudo supervisorctl status exampleapp``. #. Once confirmed, your Litestar application should be accessible (By default at ``http://0.0.0.0:8000``). After that, you are done! You can now use ``supervisor`` to manage your application. The following sections were written to provide suggestions for making things easier to manage and they are not required. Suggestions ----------- .. tip:: This follows onto the setup above, but provides some suggestions for making things easier to manage. Aliases ~~~~~~~ Create an alias file: ``/etc/profile.d/exampleapp.sh``. This is where the magic happens to let us simply use ``exampleapp start`` instead of ``sudo supervisorctl start exampleapp``. .. dropdown:: Alias Examples .. code-block:: shell :caption: Example commands provided by the alias file exampleapp start exampleapp stop exampleapp restart exampleapp status exampleapp watch .. code-block:: shell :caption: Example alias file :linenos: exampleapp() { case $1 in start) echo "Starting exampleapp..." sudo supervisorctl start exampleapp ;; stop) echo "Stopping exampleapp..." sudo supervisorctl stop exampleapp ;; restart) echo "Restarting exampleapp..." sudo supervisorctl restart exampleapp ;; status) echo "Checking status of exampleapp..." sudo supervisorctl status exampleapp ;; watch) echo "Tailing logs for exampleapp..." sudo supervisorctl tail -f exampleapp ;; help) cat << EOF Available options: exampleapp start - Start the exampleapp service exampleapp stop - Stop the exampleapp service exampleapp restart - Restart the exampleapp service exampleapp status - Check the status of the exampleapp service exampleapp watch - Tail the logs for the exampleapp service EOF ;; *) echo "Unknown command: $1" echo "Use 'exampleapp help' for a list of available commands." ;; esac } To activate the alias without restarting your session use ``source /etc/profile.d/exampleapp.sh``. Using the ``watch`` command lets you monitor the realtime output of your application. Update Script ~~~~~~~~~~~~~ The ``exampleapp`` function can be extended to include an ``update`` command, facilitating the complete update process of the application: .. dropdown:: Update Script Example .. code-block:: shell :caption: Example update command :linenos: exampleapp() { case $1 in # ... other cases ... # update) echo "Updating exampleapp..." # Stop the service echo " > Stopping service..." sudo supervisorctl stop exampleapp # Update application files echo " > Pulling latest changes from repository..." cd /opt/exampleapp git fetch --all git reset --hard origin/master # Update Supervisor configuration and alias echo " > Updating Supervisor and shell configurations..." sudo ln -sf /opt/exampleapp/server/service.conf /etc/supervisor/conf.d/exampleapp.conf sudo ln -sf /opt/exampleapp/server/alias.sh /etc/profile.d/exampleapp.sh source /etc/profile.d/exampleapp.sh # Update Supervisor to apply new configurations echo " > Reloading Supervisor configuration..." sudo supervisorctl reread sudo supervisorctl update # Update Python dependencies using requirements.txt # Here you could replace with poetry, pdm, etc., alleviating the need for # a requirements.txt file and virtual environment activation. source venv/bin/activate echo " > Installing updated dependencies..." python3 -m pip install -r requirements.txt deactivate # ... other update processes like docs building, cleanup, etc. ... # echo "Update process complete." # Prompt to start the service read -p "Start the service? (y/n) " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]] then echo " > Starting service..." sudo supervisorctl start exampleapp fi ;; # ... # esac } This update process includes the following steps: #. **Stop the Service:** Safely halts the application before making changes. #. **Git Operations:** Ensures the latest code is pulled from the repository. #. **Configuration Symlinking:** Updates ``supervisor`` configuration and shell alias to reflect any changes. #. **Supervisor Reload:** Applies new configuration settings to ``supervisor`` service. #. **Dependency Update:** Installs or updates Python dependencies as defined in lockfiles or ``requirements.txt``. #. **User Prompt:** Offers a choice to immediately start the service after updating. Execution ~~~~~~~~~ Run the ``exampleapp update`` command to execute this update process. It streamlines the deployment of new code and configuration changes, ensuring a smooth and consistent application update cycle. litestar-2.16.0/docs/topics/index.rst000066400000000000000000000006071500564371300175300ustar00rootroot00000000000000Topics ====== Topics are a collection of articles about technical concepts, background information, design decisions and similar topics on a higher level. This includes things like the difference between synchronous and asynchronous, API design, deployment strategies, ASGI and WSGI, and so on. .. toctree:: :titlesonly: :caption: Articles sync-vs-async deployment/index litestar-2.16.0/docs/topics/sync-vs-async.rst000066400000000000000000000144361500564371300211430ustar00rootroot00000000000000Sync vs. Async ============== Litestar supports synchronous as well as asynchronous callables in almost all places where it's possible to do so. In general, three different modes of execution are supported: - Running asynchronous callables directly - Running synchronous callables directly - Running synchronous callables in a thread pool This article gives an overview of important differences between these modes. Blocking and non-blocking ------------------------- In the context of asynchronous programming, the terms *blocking* and *non-blocking* are often used to describe a particular quality of a function: Blocking the flow of execution. Asynchronous functions are not inherently non-blocking. What they do instead is allow a programmer to control exactly *where* they unblock and return control to the main loop, allowing other asynchronous tasks to run, which is usually indicated by the use of the ``await`` keyword. This is a very important aspect to note, since an async function that never calls ``await`` and, for example, performs a computationally intensive task, *will* block the main thread for its entire runtime, just like a synchronous function would. More importantly, anything that happens inside an asynchronous function *between* the parts where it waits will also be blocked. Technically speaking, this means that there are no non-blocking functions in Python, since async functions only "unblock" at each ``await``. As this is not a useful definition of the term, "blocking" usually refers to callables that *block for a long time*. .. note:: Since "blocking" is about the flow of execution, one might think of ``await`` as blocking as well; the execution will not proceed past it until the awaitable has been resolved, which fits the definition of the term. However, since at the same time *other parts of the program* (more precisely, other coroutines which had given up control at an ``await`` that has since completed) are allowed to proceed, this is not considered blocking and usually referred to as "waiting". I/O bound vs. CPU bound ----------------------- Another important aspect to consider when trying to determine whether a function should be asynchronous or not is *why* it might block. In general, there are two different categories of blocking operations: I/O bound Calls to the file system or network are typical examples of I/O-bound blocking. Their execution speed is largely limited by the time it takes for the external resource to complete, which makes them "bound" to that resource. They are a good fit for async because most of the time is spent waiting for these operations to complete. This means that other tasks can be executed during this time, "waiting in parallel", greatly reducing the overall runtime. CPU bound Operations that do not wait on I/O can be usually considered CPU bound. Since they don't wait for external resources, their execution speed is bound to the CPU, i.e. how fast it can execute the instructions given. They do not benefit from asynchronous execution like I/O bound tasks, since they don't spend a significant amount of time waiting. Asynchronous CPU-bound tasks ++++++++++++++++++++++++++++ In some cases, CPU bound tasks can be made asynchronous, by introducing points for the event loop to switch to other tasks. An example of this would be an inner loop which awaits :func:`asyncio.sleep(0) ` at the beginning of each iteration. This technique is mostly useful for long-running tasks, where each individual step does not block for an extended period of time. When to use an asynchronous function ------------------------------------ Asynchronous functions should be used when they can benefit from a concurrent execution, that is, they themselves perform asynchronous operations, such as calling other asynchronous functions or iterating asynchronously, and do not perform any blocking operations, such as calling synchronous functions that are I/O bound. **Why not use async by default?** It might be tempting to look at this and think a function should be async by default, unless it's performing blocking operations synchronously, but this is not the case. Async itself has an overhead attached to it which, while very small, can in some situations become non negligible. A synchronous function performing non-blocking operations will outperform an asynchronous function doing the same. When to use a synchronous function ---------------------------------- As an inverse of the previous paragraph, it follows that synchronous functions should be used for non-io intensive tasks. The synchronous execution model allows for the smallest amount of overhead and should therefore be preferred in such situations where no asynchronous functionality is made use of. When to use a thread pool ------------------------- If a function performs computationally intense or I/O bound operations, which can not be replaced by asynchronous equivalents, ``sync_to_thread=True`` can be used to run it in a thread pool. **Why not make this the default for synchronous functions?** Running in a thread pool has a very high overhead, greatly reducing an application's performance. Its use should therefore be restricted to cases where it's absolutely necessary. Limitations +++++++++++ While running a CPU bound function in a thread pool does allow it to be run in a non-blocking way, it does not speed up its execution. Computationally intensive tasks that are performed regularly should be offloaded into a different process, to make use of multiple CPU cores. This can for example be achieve by using :func:`anyio.to_process.run_sync`. Warnings about the mode of execution ------------------------------------ Since a synchronous function might be blocking, Litestar will raise a warning about its use in places where it might block the main event loop and impact the application's performance. If a synchronous function is non-blocking, setting ``sync_to_thread=False`` will tell Litestar that the function can be treated as such. This warning was introduced to prevent accidentally using blocking functions, by having to make a deliberate decision about whether or not to run the function in a thread pool. The warning can be disabled globally by setting the environment variable ``LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD=0``. litestar-2.16.0/docs/tutorials/000077500000000000000000000000001500564371300164115ustar00rootroot00000000000000litestar-2.16.0/docs/tutorials/dto-tutorial/000077500000000000000000000000001500564371300210405ustar00rootroot00000000000000litestar-2.16.0/docs/tutorials/dto-tutorial/01-simple-dto-exclude.rst000066400000000000000000000040351500564371300255160ustar00rootroot00000000000000Our first DTO ------------- In this section we will create our first DTO by extending our script to include a DTO that will ensure we don't expose the user's email in the response. .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/simple_dto_exclude.py :language: python :caption: ``app.py`` :emphasize-lines: 6,16,17,20 :linenos: Here we introduce a new DTO class (``ReadDTO``) and configure it to exclude the ``Person.email`` field. The route handler is also instructed to use the DTO to handle the response. Lets look at these changes in more detail. Firstly, we add two additional imports. The :class:`DTOConfig ` class is used to configure DTOs. In this case, we are using it to exclude the ``email`` field from the DTO, but there are many other configuration options available and we'll cover most of them in this tutorial. The :class:`DataclassDTO ` class is a factory class that specializes in creating DTOs from dataclasses. It is also a :class:`Generic ` class, which means that it accepts a type parameter. When we provide a type parameter to a generic class it makes that class a specialized version of the generic class. In this case, we create a DTO type that specializes in transferring data to and from instances of the ``Person`` class (``DataclassDTO[Person]``). .. note:: It is not necessary to subclass ``DataclassDTO`` to create a specialized DTO type. For instance, ``ReadDTO = DataclassDTO[Person]`` also creates a valid, specialized DTO. However, subclassing ``DataclassDTO`` allows us to add the configuration object, as well as specialize the type. Finally, we instruct the route handler to use the DTO (``return_dto=ReadDTO``) to transfer data from the handler response. Lets try it out, again visit ``_ and you should see the following response: .. image:: images/simple_exclude.png :align: center That's better, now we are not exposing the user's email address! litestar-2.16.0/docs/tutorials/dto-tutorial/02-nested-exclude.rst000066400000000000000000000021761500564371300247300ustar00rootroot00000000000000Excluding from nested models ---------------------------- The ``exclude`` option can be used to exclude fields from models that are related to our data model by using dotted paths. For example, ``exclude={"a.b"}`` would exclude the ``b`` attribute of an instance nested on the ``a`` attribute. To demonstrate, let's adjust our script to add an ``Address`` model, that is related to the ``Person`` model: .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/nested_exclude.py :language: python :linenos: :emphasize-lines: 9-13,21,25,32,33 The ``Address`` model has three attributes, ``street``, ``city``, and ``country``, and we've added an ``address`` attribute to the ``Person`` model. The ``ReadDTO`` class has been updated to exclude the ``street`` attribute of the nested ``Address`` model using the dotted path syntax ``"address.street"``. Inside the handler, we create an ``Address`` instance and assign it to the ``address`` attribute of the ``Person``. When we call our handler, we can see that the ``street`` attribute is not included in the response: .. image:: images/nested_exclude.png :align: center litestar-2.16.0/docs/tutorials/dto-tutorial/03-nested-collection-exclude.rst000066400000000000000000000037161500564371300270630ustar00rootroot00000000000000Excluding from collections of nested models ------------------------------------------- In Python, generic types can accept one or more type parameters (types that are enclosed in square brackets). This pattern is often seen when representing a collection of some type, such as ``List[Person]``, where ``List`` is a generic container type, and ``Person`` specializes the contents of the collection to contain only instances of the ``Person`` class. Given a generic type, with an arbitrary number of type parameters (e.g., ``GenericType[Type0, Type1, ..., TypeN]``), we use the index of the type parameter to indicate which type the exclusion should refer to. For example, ``a.0.b``, excludes the ``b`` field from the first type parameter of ``a``, ``a.1.b`` excludes the ``b`` field from the second type parameter of ``a``, and so on. To demonstrate, lets add a self-referencing ``children`` relationship to our ``Person`` model: .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/nested_collection_exclude.py :language: python :linenos: :emphasize-lines: 22,26,34,35,41 Now, a ``Person`` can have one or many ``children``, and each ``child`` can have one or many ``children``, and so on. We have explicitly excluded the ``email`` and ``address`` fields of all represented ``children`` (``"children.0.email", "children.0.address"``). In our handler we add ``children`` to the ``Person``, and each child has no ``children`` of their own. Here's the output: .. image:: images/nested_collection_exclude.png :align: center Fantastic! Our ``children`` are now represented in the output, and their emails and addresses are excluded. However, astute readers may have noticed that we didn't exclude the ``children`` field of ``Person.children`` (e.g., ``children.0.children``), yet that field is not represented in the output. To understand why, we'll next look at the :attr:`max_nested_depth ` configuration option. litestar-2.16.0/docs/tutorials/dto-tutorial/04-max-nested-depth.rst000066400000000000000000000030621500564371300251630ustar00rootroot00000000000000Max nested depth ---------------- As we saw in the previous section, even though we didn't explicitly exclude the ``children`` from the nested ``Person.children`` representations, they were not included in the response. Here's a reminder of the output: .. image:: images/nested_collection_exclude.png :align: center Given that we didn't explicitly exclude it from the response, each of the ``Person`` objects in the ``children`` collection should have an empty ``children`` collection. The reason they do not is due to :attr:`max_nested_depth ` and its default value of ``1``. The ``max_nested_depth`` attribute is used to limit the depth of nested objects that are included in the response. In this case, the ``Person`` object has a ``children`` collection, which is a collection of nested ``Person`` objects, so this represents a nested depth of 1. The ``children`` collections of the items in the ``Person.children`` collection are at a 2nd level of nesting, and so are excluded due to the default value of ``max_nested_depth``. Let's now modify our script to include the children of children in the response: .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/max_nested_depth.py :language: python :linenos: :emphasize-lines: 28 We now see those empty collections in our output: .. image:: images/max_nested_depth.png :align: center Now that we've seen how to use the ``max_nested_depth`` configuration, we'll revert to using the default value of ``1`` for the remainder of this tutorial. litestar-2.16.0/docs/tutorials/dto-tutorial/05-renaming-fields.rst000066400000000000000000000033721500564371300250650ustar00rootroot00000000000000Renaming fields --------------- The name of a field that is used for serialization can be changed by either explicitly declaring the new name or by declaring a renaming strategy. Explicitly renaming fields ========================== We can rename fields explicitly using the :attr:`rename_fields ` attribute. This attribute is a dictionary that maps the original field name to the new field name. In this example, we rename the ``address`` field to ``location``: .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/explicit_field_renaming.py :language: python :linenos: :emphasize-lines: 28 Notice how the ``address`` field is renamed to ``location``. .. image:: images/explicit_field_renaming.png :align: center Field renaming strategies ========================= Instead of explicitly renaming fields, we can also use a field renaming strategy. The field renaming strategy is specified using the :attr:`rename_strategy ` config. Litestar supports the following strategies: - ``lower``: Converts the field name to lowercase - ``upper``: Converts the field name to uppercase - ``camel``: Converts the field name to camel case - ``pascal``: Converts the field name to pascal case .. note:: You can also define your own strategies by passing a callable that receives the field name, and returns the new field name to the ``rename_strategy`` config. Let's modify our example to use the ``upper`` strategy: .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/field_renaming_strategy.py :language: python :linenos: :emphasize-lines: 28 And the result: .. image:: images/field_renaming_strategy.png :align: center litestar-2.16.0/docs/tutorials/dto-tutorial/06-receiving-data.rst000066400000000000000000000023731500564371300247040ustar00rootroot00000000000000Receiving data -------------- So far, we've only returned data to the client, however, this is only half of the story. We also need to be able to control the data that we receive from the client. Here's the code we'll use to start: .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/simple_receiving_data.py :language: python :linenos: To simplify our demonstration, we've reduced our data model back down to a single ``Person`` class, with ``name`` ``age`` and ``email`` attributes. As before, ``ReadDTO`` is configured for the handler, and excludes the ``email`` attribute from return payloads. Our handler is now a :class:`@post() ` handler, that is annotated to both accept and return an instance of ``Person``. Litestar can natively decode request payloads into Python :func:`dataclasses `, so we don't *need* a DTO defined for the inbound data for this script to work. Now that we need to send data to the server to test our program, you can use a tool like `Postman `_ or `Posting `_. Here's an example of a request/response payload: .. image:: images/simple_receive_data.png :align: center litestar-2.16.0/docs/tutorials/dto-tutorial/07-read-only-fields.rst000066400000000000000000000031421500564371300251540ustar00rootroot00000000000000.. _read-only-fields: Read only fields ---------------- Sometimes, fields should never be able to be specified by the client. For example, upon creation of a new resource instance, the ``id`` field of a model should be generated by the server and not specified by the client. .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/read_only_fields_error.py :language: python :linenos: :emphasize-lines: 14 In this adjustment, we add an ``id`` field to the ``Person`` model. We also create a new class, ``WriteDTO`` that is instructed to ignore the ``id`` attribute. The ``WriteDTO`` class is assigned to the handler via the ``dto`` kwarg (``dto=WriteDTO``) meaning that the ``id`` field will be ignored from any data received from the client when creating a new ``Person`` instance. When we try to create a new ``Person`` instance with an ``id`` field specified, we get an error: .. image:: images/read_only_fields_error.png :align: center What's happening? The DTO is trying to construct an instance of the ``Person`` model but we have excluded the ``id`` field from the accepted client data. The ``id`` field is required by the ``Person`` model, so the model constructor raises an error. There is more than one way to address this issue, for example we could assign a default value to the ``id`` field and overwrite the default in the handler, or we could create an entirely separate model that has no ``id`` field, and transfer the data from that to the ``Person`` model in the handler. However, Litestar has a built-in solution for this: :class:`DTOData `. litestar-2.16.0/docs/tutorials/dto-tutorial/08-dto-data.rst000066400000000000000000000037121500564371300235170ustar00rootroot00000000000000Accessing the data ------------------ Sometimes, it doesn't make sense for data to be immediately parsed into an instance of the target class. We just saw an example of this in the previous section, :ref:`read-only-fields`. When required fields are excluded from, or do not exist in the client submitted data we will get an error upon instantiation of the class. The solution to this is the :class:`DTOData ` type. .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/dto_data.py :language: python :linenos: :emphasize-lines: 6,26,28 The :class:`DTOData ` type is a container for data that can be used to create instances, and access the underlying parsed and validated data. In our latest adjustments, we import that from ``litestar.dto.factory``. The handler function's data parameter type is changed to ``DTOData[Person]`` instead of ``Person``, and accordingly, the value injected to represent the inbound client data will be an instance of :class:`DTOData `. In the handler, we produce a value for the ``id`` field, and create an instance of ``Person`` using the :meth:`create_instance ` method of the ``DTOData`` instance. And our app is back to a working state: .. image:: images/dto_data.png :align: center .. tip:: To provide values for nested attributes you can use the "double-underscore" syntax as a keyword argument to the :meth:`create_instance() ` method. For example, ``address__id=1`` will set the ``id`` attribute of the ``address`` attribute of the created instance. See :ref:`dto-create-instance-nested-data` for more information. The :class:`DTOData ` type has some other useful methods, and we'll take a look at those in the next section: :ref:`updating`. litestar-2.16.0/docs/tutorials/dto-tutorial/09-updating.rst000066400000000000000000000037371500564371300236450ustar00rootroot00000000000000.. _updating: Updating instances ------------------ In this section we'll see how to update existing instances using :class:`DTOData `. PUT handlers ============ `PUT `_ requests are characterized by requiring the full data model to be submitted for update. .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/put_handlers.py :language: python :linenos: :emphasize-lines: 25,28,29 This script defines a ``PUT`` handler with path ``/person/{person_id:int}`` that includes a route parameter, ``person_id`` to specify which person should be updated. In the handler, we create an instance of ``Person``, simulating a database lookup, and then pass it to the :meth:`DTOData.update_instance() ` method, which returns the same instance after modifying it with the submitted data. .. image:: images/put_handlers.png :align: center PATCH handlers ============== `PATCH `_ requests are characterized by allowing any subset of the properties of the data model to be submitted for update. This is in contrast to `PUT `_ requests, which require the entire data model to be submitted. .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/patch_handlers.py :language: python :linenos: :emphasize-lines: 21,22,25 In this latest update, the handler has been changed to a :class:`@patch() ` handler. This script introduces the ``PatchDTO`` class that has a similar configuration to ``WriteDTO``, with the ``id`` field excluded, but it also sets :attr:`partial=True `. This setting allows for partial updates of the resource. And here's a demonstration of use: .. image:: images/patch_handlers.png :align: center litestar-2.16.0/docs/tutorials/dto-tutorial/10-layered-dto-declarations.rst000066400000000000000000000022441500564371300266710ustar00rootroot00000000000000Declaring DTOs on app layers ----------------------------- So far we've seen DTO declared per handler. Let's have a look at a script that declares multiple handlers - something more typical of a real application. .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/multiple_handlers.py :language: python :linenos: DTOs can be defined on any :ref:`layer ` of the application which gives us a chance to tidy up our code a bit. Let's move the handlers into a controller and define the DTOs there. .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/controller.py :language: python :linenos: :emphasize-lines: 30,31,44 The previous script had separate handler functions for each route, whereas the new script organizes these into a ``PersonController`` class, allowing us to move common configuration to the controller layer. We have defined both ``dto=WriteDTO`` and ``return_dto=ReadDTO`` on the ``PersonController`` class, removing the need to define these on each handler. We still define ``PatchDTO`` directly on the ``patch_person`` handler, to override the controller level ``dto`` setting for that handler. litestar-2.16.0/docs/tutorials/dto-tutorial/images/000077500000000000000000000000001500564371300223055ustar00rootroot00000000000000litestar-2.16.0/docs/tutorials/dto-tutorial/images/dto_data.png000066400000000000000000000672721500564371300246100ustar00rootroot00000000000000PNG  IHDRj pHYs+ IDATx\W?wM2@"H!+^X!XP(6T-h+w_ѻ݂ ޭcn- Zh`PRP%h!ML$A$̜sf5g&ə M U!0>B>011133h=dG3.B7>LLLMn0~I,ӯyKKdfffz~Kz^2e fBh$!@Q!DB"E!0>Ba| (@Q+Fx^H`i|^p!a51%z*k4Om)y>vn]ܼg4CK \iKp^:fADl8={6ڙ>6{#]%ͻzj]XzJPxռi#7 ćvBRjAJaD'ͻz )]zV"yClʅ«ռ]olK ]8!7nwd岀(/P> aa w>@r%˂Oj_ O{-^b`EkLm)F\+,KI>Жf$''<3+%]8uC"bbl7l`<g.T8tHLxXԇ_U 3jM3OۡiӦm޼yƌ}1c͛-A.tǃ-YU!صϒ̍ZEM页B'CBNU)χ-[U@Sp&dٲeCJ,@]{1@I.]vY0;a74j/*/~3N%ǻˆ--{qIP2lٻmnw(}qIʥ ߥ%/. =~t'.zx, ="+Nx[xeiy:{6sUV#QQ' ?^{'Af̘k}eJO/m"jyM8MǓ~+ڊ31Gr`Rguioz,a^<~ laԡ&lݠC: K6%|A@]|B |F)X u4qWڥ֊ o ;Y1[xR_>RLN활$I6&${gLph!ڐpٰT}ЙZ d5}ZY$\6Y~ćZԼ³Ied>ANZr;a33' : htZtMܿYjC=owZQvg5V'Ȑ gNA}ϼQǼ-IٙIWpοw-h5Z`r}ieN%@[ʲ%A;Kw'`5P= 2AU V\Bݨ"م{ʸ]TMj-W[WnZ|~$,}HвU:o)C#j-=oj49"eUۻ˰\|O/h$l*=u#je7y.]5rƊ*r7PmUkzt{#L"C6o/oΝ;C.{%FB 6w_S#]8˥Zձ)) ȦܓY d²$IKy+6ߞj> ջ*_7D \ V,]\ϸ<#WbvKKVhs3I #e{6Vd`@Vo Sd|tپӊؗ*%6$^HfR5@q+6} V( 6nq$tްUMl `.wf@խصŒ` mT|ui5ːmHd\ڼ/ ߜ0h7"C7&u,?~U;?,ܷ116rg4/\M|ge'A^>ujC||3AGn(/˻""3>o/h o_{ګ/o}DL"xġ-mNYzw3` >0tv~6hw_48TcBa| (@Q!DB"EI,B/> $!l鍏VsMbBϊ>dzss.BpAAO^Ba| (@Q!DB"E!0>Ba| (@Q!DB"E!0>Ba|gLMEbh6A0`>n<T5Ҍtc5=Bw OZ9Kຈ^]Xf;fL\$dzagV%ޱr|"7+b?p 8rbal^`H>=x$+3Os=g2[߻m:l& `\6vPjy6?)*"X;7߅$OC ]뵷i%w ~ϣf7+.ğ%#)g3@j5,r4-`mRma›Q)GΪ%o p125gV9Ļ6YTf; ch=$ִ:Gmvoڇt(@guͅe,B>'z{]Q3< +YγEJ> -®OnZetY6\zFj6TE) |TƖ8Dd}lRxNXn<۳۳ZZ}and.*\Xv5)xvwCă[s%;xɏ|]ٳXE+m^o;vLedzT|xXs%;V+\J"qԏk/*(XS q-'vlee>[f,c7qr xy Ԭ/ollIw̦:bpwvAŵ-֚ Fư ہ80 fu9BŅ;liW y jم[0u6C}'e4c蕍o-hSl:aG'6BýӘ$0cꪾ823 oMZL\ 2'.Q&m o*5.U.+!v2?o bw^Q={R 6SLuX<|t( Kx"-ӧ,d ܬ27z3+rhViA@@kIz:w͊{$)k6gZ@jW:kyznaFoUu ͭl.@g xGGnbuT++,Y;y%E2a$gsVl,S>caX6/w?f,AuYڞCk_36=7`_T@ rրF33Nl@ov1#{@ӧٵGNE߻gW|'Qb[ȣ'KҬ9LkmmquP5t?jPiYEab["׺(qPz cư ہ@^&z{{n}0iY^p]IJzAEGh05@@:In\S0]Ȯ9{nub,V 7L[` 39]dQc%]HwZ|GN1ؼK2M>IYT;IЪ`s-ZffVV+x߁cVHOHn>>r VÌ @<]Oz)^ƽs)A\ud_Y#W/=W1`_EIu=Es;};:(|](:w7nWH6Ɨ[*N RrRIXТ(I?]2Nmق "f|\Od.8nWVS*ٳG.ht� `tܮ͸ PuN ٴuZ_"hqْ`vŅgknhHOpJSް" NSZwh i.)dC0cbtڢb@3)6hN%KH£D(15_= (w»(qVvax䗩'c=~}xN Sa52Sz܅'oߝ}mlò>]2,kF~i= uY!>4e`sm}$_Cv>(Wܷ'Ę̌Ȭ'z5q0>B B"E!0>Ba| (@Q4?DרnͰX-{!kc!!DB"E!0>Ba| (@Q!DB"E!F1އgjJecc+ظyF`k=>h4 >} FZ'Jm% Yryq/d GH"}Ap&~~Wϫ#s4 ͫihpV SK0@֪ u`8ڮQRiӳFt*U(\ȵ"NРsiFTv=?tkX~Tl,_Cʫ+d92^AX+Unƀ͑bY\qAe!ݖ'Y\ZtQ稓E  S&*n?Y[z_- I/ŭ$W}]q-wώJ3(tX4Nj.&huZ6*Ѩz*Ԡxp#ϕ\09jމT H'24[>?*6\|||hG6'xL@PӔH*TJ*?⥰1#c-Jሬ9j5GSŁFF>&T5 LkkἜW_J#M.lЌEBnJvPjЍjkCms s?EQ,VSIEH_ZȋqdQu<'$ZSd<˗i$A<^eReJ pd $ h:>}9Jó&@I!iTѵ:IbfrqB@jT̈}7Mb5BT5rͷeܨHJ陒Oj5*F&(ogͣrX%H/Sz~WxX[O^Gw&?Y&M":?1,ז uHʊ±[.D@G ҳ꾏)׉Z$2n:dp xu:K]S0| eSk ~Pt3-bkT&',R2*IkEkAVmQ;[#> rd.EE$ 2=""i\m*)i{Nؙ \^i83ugĐ'AD$O˿ee7ԙq ɧ%rH~댆OJj> OKc}Q./4L'QY4`F dYB\cxO^91:_KI+u:TF4E9=:))@H @ʤ2R B㿕˿wW639mf\\(NU1i](\\2)x%*ƞ+W%D$ȇۨET+mٵ|mNT;%DLe|Z"/XRyTYsNq|p Yom|DyhC~P|޽k׮uvv -zBBa| (@Q!DB"E!0>Ba| (@Q>mڴ.BDd!L“E!0>Ba| (@Q!DQ{N2dԩ.Bǔ)S>|:O8qbKoYo|<>ܾud߬kCޱ!x!4* B"'rt?+T<͗GVh-y"Fl"z4S:g4y\;ρ{:cHLɋ q֙+6MN:uϷw6?g<ӗNwdfBa| (@QD0fZY 1k+n _t^`gӭ}a^Sȧ7] /۷j~Ÿx;1*orWT(a^@+X)k,Znz+O\oD|B{ϱ|7w:b}2nA#1yҿY9Ln|Ж}_}tfe%6sx"!λOm8%_s8Yz힭 N nE6LhJ:G-Bm%bu{eM{U fTUǾŮY A۝2ݬ:щ &zW}MO0t sz' |yGLs|jF;6lgf=: Gg`c4Vy:9m:U߷N310Ims究456YnKz,M'OtSu@<+JR _&|NCBPrmfOG}'°yJ˒nY쬬kl:ZS䴫dg42Ci[*u3z+lY֜ zϱ۱arZvM'"'%e:%V̰|_>,a*e"6F.\û\017gV}=GsLu-Pi}rpWVEbk93ka`k::ӵm 8"{'lS0 [>[ds-M~@vs;@GIusɕ3fX{3b`_qo$:P֓M;bBP Y7kS]c~yp1Q~696m=5p@`]jbhS;KUVfۭwK:< n ViYO ϟ)=rR 9U76ģgm-7jgsCS;<;> g)bwO;[*k'a!-yt݆e:f)_}&FY/AwdyTɰ6ՍKRx5ί?{iu QodtLfb؆t zeh9SWuLʒNз)dwR`aӊ{<S\|wv嗨߱n&t{B0h,)xS総s*JU@> VVV[k779WWĵY#Ba| (@Q!DBml ֬t],rv[?iįc1n魯f@9!UWqͨ=-ɕSX ŭEI7셯{צW>0=^>$o̯Zm&`g{ѿuo^[eKsԂwknЭͧjxBlަW~&s,Ъ0g2{/nd[kSO*Tɭ6~uf52oTɦ}@gO ]!g.9/S]Mn흯,ޝm8~1zJLN>ދgw nlԱ`&ܺ!F\Cb=Ͷ>6VԴ&5M_xTEt]:5b-e9[v,c @6WtAOfW0>S\_||xµݓUq_e?v϶]M𵏧՝[o5褴ӑ \0x4B" B"E!0>Ba| (@QhbFZ,o{Ѽ|_Z:F1mi9i?^s {r6L\&fp[yq-cٺj/Tke~;_*iMT9kǦmW:?SݼwV^_OPL/{-]_KZ'x9"k\yMgk'g,s\"[ǾMN-f5V6~M_t\=y['OsMZQЗ|bhޜ;_̃)~̕3c.(Rs61wwq{0,kݔI'rOքl'ߩ Q fM"8<@gs4>t9! E+Ywgz愽SƶZ#fz _f<݄#{@x4 ;ߡKŮY A۝슲v߿:NϚUs㎉uSEc/f@s5Z?0eʨ)U'rkt`‹-k"׃^vRʝʚ)ul`t~lG'f2^7U<3`h/ȿ=0;a5،;ZW˗>+6Lo6jC0PZ)}Vt^ݟ>T8zuW@p/_a5•8|{Ţ@BV=0-׋_\fATʎZ``+&(>;7']iRg?3hc8O?}'G`*hkī9 F gmڿf9W@WЖ}~ p;',N5 YYP0`^K.{ئ?f\7s<揙Z체@9};-yЩv=LjO/ȽX0Sc UG`ڿ[S,ݼ'[~[<19Uoe+ڡU֜ zZccN?EOJtc)J aq;z,sZGgT(<[׆-%'Wn%fC!|?зs/ 92dná 0L0pfyp(1@ghA@g n,,MfB5ǐwf|U#:ۻ!SϘaA S>h::MG z=th@{ݧ@'0,X:O2_hqu9L:U:JC,M U/r/8[SL |S(tQ^ӳdZaW^,M^gݿ IC=tm:hӑ6LCp묶lcdw`:$]~mВQ1f Ptm;2G>W6C>{L lpf-i[umqxbaGaz (?/CMf<Ş[ŻR'hPvb1;;bbΧ{ee1L! )I+nֳGߠׂ^]RL_ߟAfY1K-%',\w oS3e%1M&VEK~ݢyx  Xz؞l,闉=y4ST+t*e߿?=yTKR/hVgcc4`ޠJ>]QZeh9SWuL^R`aӊ{<S\|wv嗨߱n&t{B0h,)hUv{Έ/G!\73e(>ª+~S!0y4#&gs`R*&W9le,b'YQCϻxE ^ ?;@_{e"Uc>NbD$;v刾84~k&ZyS-P=m'^@3Omkh/xBa| (@Qf> IDAThbc[olڲeӖ-70]흅45/̋ =\8{Q>z#G풣Kl(9q̡vJS&~uyrj2ddBM)?%!4&f ʎy}Mi|e]mI[Xgb,sxoUqJ0ӯ]z*LL|0@|l6Ӡ.!hcP;x5o;G9˿ب'Lʼno{Gѥ=z@8I>a:C׵0.pbjSgBS-*+ K>и  эz jZ{#lλuqg39m9;>hd֍;3[c'ĘF 3BO m|qԦYYۓsPξ3B[ǧLmۖQ0gބ>?UkX4Rw녡܌3hKz?EƝwIL7m\3 سG]:;pjn{#A3V$>BNBa| (@Qig*ASܙ( B]d|WR>\*xJo (@Qh}D~>69[<3ƋW%+ơxњqq_<.?w_=s`ꃛmL@T_ Mܜqʎ/ؚozrwJG=ʄ>iMvlLh-(ucifQBsF@*n|fqnw?Ʀڱ!4:Q0Nqtu#![M }ymb@g0N2ԏR?k!FkN^{~Xew-B\eMg4>6DEh?jAh=7j@= !DB"E!0>Ba| (@Q!DB"E!0>Ba| (@Q!DB"E!0>Ba| (@Q!DB"E!0>Ba| (@Q!DQo|I,B@=s==|pʔ),`LbBz>jZvJz'/!0>Ba| (@QD6md!L߿ˀz& B"E!0>Ba| (@Q!DB"ߎ K:&Y8^]H?u}\ghyTߟ5}b6_~Ll<~6yyY|!n_xȶ:y٨ ew yyO4߮f dNoF-uKຈ^]X2m t`~waM\?6CI GDע5.XRt# Uju/ZEv*~˶xW]B"KEjP]bC\!F! $LG@!3g3gf\:qc^YCj#_r-#䀡AzC~\&X`[`h}zUp =EϾإkeՀq@n~ @=:wj2U:a$b#C >[U^*/#t T"ٶkd禿6+c׶S@wH 0{n֥oД=@Ĵqn#90 5Q$ l0' l |F1,;YX8žv_ Dnk4%c`~Tav`O[$cɼK\q#^,IZqF#p#I ihԨ3J[m#Ak fbʷck!,ڣ P JilcSmz>"M8m 8bLD̪Bx6zY2wc7^d5rj$|iyYY4/0ewde> p/ʜڅęC}"1)`]Kũ"o^( ޶mZ"~8_qZrAvVVN$ @Ĉٖ-  -LIS}}qG)s$) 3l.l_$|*g۶mdDt? ^IW q7˭ 0nIޗvl۶@͋ƅ۪Cd[~]!o@n϶y*a1cSGnUZ>K"fosYKGɓ}/wt|u{~l۸$deUx6\0ݘՃ~P6ZZDH˙1nY06jΌse5DO|@K+h,?luMRϕJ$x~j".,->)ka";+|zNG||qiIA][Y\~!{}'W>BF߾W"鞃g̞r]/d 4Vt:.MM-hϏ(hʹt^o\PսoToiWnG_xx@FcCo7[ 0^:+XJPіnCwGmAW5edW$1}58,9;\aztl?u/m4w$\G_W*UԺ|@ .޽po?F}c3p9(?jcQ wڢU)gȠ6jE E}2 {oC#?cu>:-"qIrxY޳6nwʭ3gx+6pzȒ+)=7E$\9J!O\)e187uELfH, j en1 vxaN۷*`P\;xњtqW2_z3 $9<eJY 5Bo$ nĂ? f0}">${3Q+ I2L|~JOx?<nכ$͗>AouԹ@n0͔\l&I3+n$%}Ng &+Nc>P"iazq JhXt]ߺve%ٰZuCmGe\gD}eXϕ.ZzAEdw{c͑R5 my'_HIɌcQ(+o8O*Y$&e6iAypFJRsNk ْ7ih=U&.-_U$/,ul{[yީ$i؄dm;>ٰR$ 2ugw,ziu^o~$%ʔ|tG6VbFiOη .m{BYZN .wAvZ, AՊ$4WK;V'ݽO:DO]Ss}|moQzR2)cجɚR7>`*OHFg wDGN<{qIn!sOgj3؄w.h1;9A>qWͥz B詀W"(@Q!DB"EOZW-4+9yc4E9l} (Ba!DB"E!0>Ba| (@Q4`'l &LwZ7h ܽ{ݻVΝ;g&L_ĉ=<<ư*_D`2fyyg1yqssz Bq~|< 4Rb9C:5~ad (@Q!DB"ѳ >/'X[{瞿9%筜e-;f[[#Mvboޢ(Pƨ2ynzoAS_URۨYwA9C>i }Shau[;@յdcС DZEnz >ژbun+S|%qJ#[O]eG눪2dSIv@%.K+-cV0sB@c(@S$oWDŽq.-Tߩ[HYi:h5ڪ cF}?\Ѽ(YrU8k\0eorf27:+nm+ۻ=Wa_?7Yoђv0# /ػl֡ܫq#O:1Vl Tƥ^F$$ bTuD'ʜ|7E7؀ܪU59'iZ5Mi-pP2L~.1AMۛjJKZO|z*,pE12&QWu',!1& eEQʹ+|΅:κNxVӠdL,1ƟM&Ff JLNx=;=#pβ. -%U\~l,q&C{o݇&`&Gq*)U:69a{ZKDŽp Ҡ?[V KfТ<[qZ-}+9TS?_ 6Z{Q}N72ˌYOx+yAm\@MϪx썄չJcٖչu'%m)8_)gGx~s$ NXtS`Dr>pwKK'ğ@ "'AMClDG ~hCο@'@h0 iH/_'vyW}Q·/'&Fz3mMN8Peg}}ѡdx K 7=s*xfWرcǎ;v`Q8Qe~STǍ_:[h! Ph(;zX\NU??G[Sk}Ssuh@0PoA5zg:m`+j́>`ϏHK~u,#YLkZ|CcEF__0'`{yqhЩ,ͯj**;a3׋;Dl6RV`i:]j\pFL/. -V, E'G dIW)JEfucO8XjPJyfڼ@z,Y_央VvFi6hN, fء o W(ͩ4-7KSd&,wx[} ^ʹ(T0j>ZEꥥUUB.)k4Fh&٩'^A>* F&,aTz~3)k~^&\1IUT(Uz @{Tv Fe?Qícǎ%'';vƍ0i$_txܻۨdLi+BT;9M(RM4~ldM^n09>=>) 4eZ50&en9u:&O' JSjdhǫlFMzwݏfZP1&tie2`<,uھv {d&&{& 5?8Q9cE}dPlDGxP/BX?,edVѱQ> 'lɪ-k?4y>wh!K^_;@-$@z@Es1LL (jE^s-h4@3$nN  O[;y&p@ lC4[ء|h5zyCFޒ6r(^tMs/A#/C2'MdU _|QA摃Q+cŋwq7oU^)`r{t?>2Bh8zzzz0۷`"11[d߹sšݾ}"!yAIܹc6ًܹyNZGgΞ c] B"E!0>Ba| (@Q!DBwizzO'۷5A)7>>qzmj w^Ba| (@Q䔾K熺[5*/z`qP1áDjtharKbiN6DLuqp_Q_HIDATzRKF :i Bce<62;$ើ7 ɆBh.ћ걮BOя(IB+XJB/+#-%tyg߱Иh;) c^И;/-Dڈ!4m|wW"4&[ln $qTcÕBw^BTa| (@Q A/|iʗ~' sRut;'?N34_jdϥ~h#/ Ͽinjߦą_-wA +mƷQ[K?СkL{`'l E'N^M /8. Zo.Q̊?&'n]]*pty_m/tFI\3<5w=> ~ VwUw8*0]wi`LQz8AxUzDLvֺhqӟKodiaώy.OZX> >Dڌ^%ǿՀOzu =jM/mnY6,W ؗ">W[b?M=]a`?][ .,Kk*_LXu$ǰnW(w?Oښy=fqE =܂dm3.gfcWw6\fOѝyGnze) gې@O6F8^}EሲctwjL~Su& @H1]zb1e6YlEZ/ pONʵ0:E(U[Y`kY; AW]^]exƕ4bנ%z*=c:\I ꋱ+ c s-$7Y'7Oqr~q/{8d TA*˃j*$`O%ȎU>K,ozSbzQ􋯛 ϸ_f0}{FvUMxN6;ug^#;O<㣗V^%J3,l6S!upynSn(hBa!DB"E!0>Ba| (@QDUL .\|(0ږ8//MN.t#aO_YTᨫXˎ۠鵗l#ōF?VĘk*VvKΎfGN7W[u%vS?P/|iX, eo._nXw "{_KO`w07/@o.B9?Z wwu;qړ{~kd)ɬ0M4*Zw@'ke}iO0{>9.s"&`wy=o`M,f*|~fܖwTi)T=V.~o;2@-l?vzxT=}7jȯ% huZ۠#.IHM`2<Vj!<4ݝA2`LNh:6FL>ɿ])O~Ci!/n0B#]fk{}iƳ~c:">+_rf2vrd?)?ڹ* ,l1 qSc;8WwxK@+ĉnlz px~>KQ +۳]7G{Dw+"IW'W(PժNjWxU9]E/ҫQv",̇7%7fs 9ў2KJ-;0P( 铦͟G'ժ<;}{g.A]9y̝ƃ W*?ykv7{5^KOecEI +}Sx̘&2kh>/QNd`_it jy ,`ddG`_8rl} (k^Ba| (@Q!DB"EN(;kn]7z1΁x-me忦_nM[6~?w[Fɜ5U'ڛl.:,ݑJ\_si>_m~=sҺtT]7}><8/AxN!%vTZe[2Aoj.exWǑW;:g=lt˅KV66ސtuK1 V)WˬbԵ2m Bo0&C~3=m\f(U*VkCh|˷=+Bh|և_|1<\dߔ^r~NsE,׀ ǃ%3BhLJ/|7}Es*4NGw9 {̴twbܠ-St&;Oxr%."CN]d/]\Xnc%=;сcx!SE!0>Ba| (#.}p}Ƣ.=_|LJ>UUMz B"E!%|)Oܲ~S? \~$Ngѕ[q#z|NZO*S‚z 1;/4:Ǐ15}SkuxarpA Ѓbsc헤ҋmλDEj#_L3)55(Vdn7FBa| (@Q!DB"E!0>Ba| (@Q!DB"EJQ$nIENDB`litestar-2.16.0/docs/tutorials/dto-tutorial/images/explicit_field_renaming.png000066400000000000000000000534441500564371300276710ustar00rootroot00000000000000PNG  IHDRgD pHYs+ IDATxg@gfK&E5bQcL[ޘk1EcLbl1b*齗3UD X fg3gf!Ν ! K !ԃ ԝbX 0QU__]z?ihh" _b _E!E!E!E!E!{ժU&&&GFF---jBP$Dg?cb2 C*y<^XX5DEED"kk'㏽T4z||;w7onoo'`mmmoodPt 2uץg_kkիWtP==8d3 .脇d˗/CCC@pv4MwUa𨮮yc|ٳ;5|w݃zL...=<o·%gg)X {rSN]xvyyyv_СCNNN˖-p8_~%Yy6lHJJx>,$$dӦM?ܺu ZM￿~֭[ vK.;MfjĉcbbMLLƌCĕ+W`ر...ϟH$3gܳgML&sΜ9E=zTSSs$IFEEY/^8n8DZ^zz\d`0{U ŝ̙ FOwU}y<*$$dڴi[l:uj텅'OpMwwA^{nn~\._n]ӚFDDl۶ ɶo V T*MHHXdI]tٛogϞ 4bĈC:99ݸq\q崴4P*gvppuuu۳gOee%_zU"KǏ+"33Q___QQA |>A/_hkȐ!qqqjllR9@ 8vXUUK$+W( 0443fU*őmNNN,Kﯭr[ZZRSS_:XXX9D,]vmrʔ)/N<:c6瑝*=vޭƢsYUD_Pqqqqg̘aÆ>>>A\zUmbbL&m _tR~:*AI<<?|pcccսMMM++#F?~߾}ӳH1666008yB055 0119|ꥹs&%%Qk.,,@Djiw</00000D"˟m1w\\3eee :͵vfΜdɒvx㍢'Ov<T*v vU%T!t…YYYFFFuuu111(P% U555>*MM͎/577Ur322ZwTTNNŋCBB`0~ƍx(--0`@ii~H${.EQ#G #G@ssY T;f¸\jh.kooܹ*J?. SL-,,9rdIIURRbddCinnn5瑞&>>Xt髯﫺ojjzΎ(GT=RUUjՕ }Nب~d2q]]]1H$ ;wlllO°(Tgaaa?vرcǶn\j .= իW7n|7GW\rm۶UWW>{L/_V|P(Wgdp/"##UNT8p`ӦMIII/_VR[[{ᐐYf577={V4UPtȑ-nݺ% %U]v,--]\\444D"QAA:TWWaaa6YYYJFRibbKdNNNTT"8p 88xtE!O1cP(yfii7瑈s>amDM{.sDNcH*,,B~z7+oz{{ƝZ]]e˖N?Q@@@@@mZM#I>LLLToT,Xw`ι+N̿㡏`SvZrrrXXXPPũrѓs… ]gNNNkCMMMpvpND'Nt|^Z.!)))yV @t?׿@!E!E!EU}/꿬E"F0zQgwqq)..n /4R_R_RunB}} 7i ![p!ԃRwBe_R_R_R_R_RߗBߧF0٬&@z r9c꫊.CI{?M).sLUYnxǕ}LYˣDF{ݳk8> wc{c׏ގFjW  t'ގXԤi7VP ;qhJgHi J3z#ך{SLrmV.d[\ӕ8E?Yg1rƢxmZ,_I zOި_n*J H/!wM`J+G>pڏcBVDJP"9ˎw[{ز5|9ʆt iٶU?=Td8yg)H)]cu|'bP ]ٸ|k"f~b&C\pםR`x,abPS4؈G4*eO! Kfp3aԦ!<]o, w7$J5.ޫZDӕퟜ`Av: 跛NnSzi=t#*z,F.=}$Iֿܺ+ %m`[FRUS"@76`27H^BHΈk`\IVPQDPe}q̀{mسbsIޙmkV'|O݁V8d~'6~Nv`vh2FP}_ct| 7ړkW,'(7__qUϮ-ݗsY! .YW]Gٍ! t-Uw#lZ?o1oeowmYO?P4Vշsbnj2*m\؄^E`9Vfdv3VA77 'hQMi@((u@(iU%k9,cz!L"ڽݻ `PSхo6enBo2y x|vUmyvRN-@D36šziwC='n7XCDӏ5P&$F0idbD5`|Ȧ_}t1У}`e桯~'|4bJ%z1e2&# AFxqwObMsw(wud\>3 !zO_2{)O)5D;VaC 52`TJ=+6oZCH]=> Gth2E6ԵXhim{PAJz=AZ:JX`oO,:0̇M)ֽ!ސRv^k۽#<.v{$2g6׉b::?n<[4wmޖ6e B??P܁+j2Me ;ގM5lݓ9rG'6)nZ @SJ`293:bȲ\۔ڪ*`2!iTsS[b[_\B7vM(cCmɱY"iQjۓ>9cGh}nk޺#+Lv |u("UwotX=ܽkO!]ˬ;kcMl@dnէT*ZZ{/?=om3 ==v!K җe9/tubJޜ競|!///uW!|9Z.a#y)kf0n[y0ΞedhK5ͱoH0/:AC JoU+P }1=LxB\~|Iשּׁue2ŽuԲko#]i()7=8[:IJhx|oRA;GwFBz.fho'2bFVv.><4lZRIPsQi)7MJ` `'ۄQ:Io s7dKsh`9yXj(a}enjbĄ Wб$WϥphԵຍcUq%RS_K)&8fh3.h4 L^)#fO*pp:9[LVꄖ@:ji劧_fMwv^`]=cʔrjZ~~Ξm8HtqSk8+61. D橓@C=dWO^2ugb2;|)C}oxe٤\"Q@S-u @h{M=ԜÂ2؁K/]Lmy)2>f)H8F#{̄gs]7iו sv2K9Z5bH'm޽q!v^) ;I6"K9uH*Ӳ:J 9׀Ť3"O^maڏ;COƂċn] JzgA:l)7DtFヘ" =rNHB{[UPDި"9%m2V)9ΣqXSIU AӦXY dDjq ȋ YpA=]ںEn_S[Vky L=q]J؎gj.^c i&U)g5RvAf|&HIbPrL2@@;W%ίnrј 㜫5Tujϣrj-z>iaI ^HMLHL-j-H;3a+[dFҡFdqiigZ[p Íq C썫{o#ԠAݨ>l˕ 1co?I!+趨SG˧ŕ s+ryfF[ P:+TCn Y6(?N׫ñZ)|эljYzKJPi8Ϋ À瞼{?^dɲj IDATizGԽg?ea4ݐxh+Ã]yI(Bno94Mh̜v,c OZ Z=_ 7+9Ӂrfco#KssKs 47klj8"4U3"u--|f9@h{xQKyv'zihQ$RUwSN )*S}=n}q2E]eSO-,uC;<ؽT_ xD7;{:*^E1J ׻Rwj{Md6Fx r)C!MYK3 4TT[qˀ&hէ ogWrȱNFWjXf ŏw@P{uZVE#YF>!  heS;s͈| w+E.< XD6p<쬺@OGnVCEW>]OYl<:ɀ$#4 89HKۚgDKhM"x({睿ucn?6 6`z./74ws51|l>DKJn'XJ><Y HbZO 2=7@Y w6Ҵkʼ.?lxuz<^ E(eyZ&5"_aݒ2:Qֶ~( [j#w|i hi?Z ';h:.zH[5o?2554o zIu";R"z@ÕrI4EKen}4MJNtHn%Dkەڒ+CFt7j u&z|! )|1e 9TՓ ̷5IYUzt!a8d$в 2ډkL'E+ZO*p9ךUeWx9餴+aGh0":SC#`QN9)j`܌u0{@ZziQfI1a MbRJY=[dR#uJ^_g_v4-aD eC[ Of%-IF A# 5HYKYR|^ 01st0b"fJ-Ew/҃'yj(Q}ݨk9 C5Ԕ>zt>CoVXm'-7G;0tLj~g߹O=cYS ioۧn;^=ҳ4֧ 󼼼RSS|GaO"H,-񧾩!B}uh&^z4=LA By۲ޜ'"wPIMMUw_0BH=:_o)uEןG!ua>wwBŀ_R|!!!!!'1 EE̲YplkNݭSrg|yn֫8BH0 jw'Ic!z!?`^#t +2.ϭTN\8ˤ GN٬0af59T֭>/i/ē Uo/;UVVS_ݑ~ї$J% %珿*$1~XIa|MoE^ۺN\c1ΦėWmR~Fo@Um_p;i&|T ESrwzY=뤁:) ֧E3@+E"]=}yT@Zvu;_x:+`.H-wckgr47 d<ڴ)^)Ki0Ց1TPmq'B])Gؐ@jR^(=&]Wt|ռ}+%M.{c=xm`"% CÀW<㿝Ŧr27lQqyIH7&DsSgi}͕W)$O$mI z֫>`a|!iBH=B_R_R_R_R_R_R_R_R_Re6 a ¤];,A*?] {B045]-oύߙOlUUzD#|脀o^OF|Az"gp!hnJ ,[og f+ʾUGc|^{BPu|uB\! ؉w!B#".PJW$ Iih$/. s'o3#3?4Vָ!+H4ɪtiD l~>꩔ZZst_إD|W"_e#̴3(i5h 9\tF̈Eh+B=cU#شx$ie@TUO1E=@+ ecSZVt,lJŻ-]=PLKX82sZAă/7l^Z$yҞ9'ZI?/[Hh X<ր$-N˻L1; a~ֿ`rHJJQ@,)U.!^= `z{ 42T!\ !@!E!E!E!E!E!#,Z[ ^4E'2iD2N*1SHBMI#$1R z׳?i0ySΖ:^YQLC9õU(aZ4*HB>zN"Ԩm133qN?ٷg? ӝ2lFi4o7^ ?AJL4=)MR ?=0j|EvG,j{.¹ CrYX#uA7.H̚Ү_N|eVO <)E}R),_Bf8Be9[222bbb}>BѬ6:VJ4DlB{p@+@h[jH(i,C04P\RF$f+b  hY} e@]FFF*322T>q^ y#4 ,ׯy>@є*`Q{=_?`!ԳȌG/-ki04tcmn( p+|1NW2+-= Z(FwRܽMx1h[bLE~ 1}[`z~ѐ]c Pahjjur~~~~~~gIiJ,[-.H!kg2Stw)%qs,5dVrSe֘=rN\vz{vCͬQ~M?Ωo7ِ`ZPuv݊IOP/ۻ'~5aә)~ BoҐꈟ}|?=<ڃ!aK&ӓOT]3jڗFe%-i etY Ȅi" 4-;0;GGe8mga\ec~=2ar&&"Nm#g}1]52=QXOZaQ3]q`_Y۷!+e9oCސ<-3]n8qkE^{/%oь81DD+kf_.շW{?"U7adڗ~o EYΫ6h(ޭ9egWMeNg3J\}22瑆jCfYWڜb) ,6]#Ab"OXzILqn|Q)Sw}KKAS&()5uƥ e[G^Z]p8oiyAUUdXsju]Њ=P~ysx糺RA Դu[e|oˮ C#lH uy0[O C} Q3L¬*[l`?ILebLuV$gkNv8t\ʃ$a:˦w!\iqYs[DݖO n@}[O݅eq9rj鎣/R@*V \K{]W7g?nJi`g;s=m Ke\CG-?KvVG;-E<)mr1B. \X%@xZi(QYm;qE~, qQ%`Z"9ofsmJ%?K]ۯ@0̧ t$fեCd4?>e Ԓ[lTTdۯ$g0kO&Tտ jٯ$>)dhY|V|jG|i޾C=B7##Ƿ;v\ H 竈#6iu&.v{ݖn+1ɲdթ7 u^yh=7%W֮hu&69̾<.C_4s i. B7&&渻nȈp{]7빉HJqQ "IIBzqH3n2~8xN @XYM}M'wGj7!oFFFǍ׋#̹͕ؾѴJkUM02 ^`c4!6+YK >(VHMByc㿪ŊT_wdP.2J +hSvcׄGz#׋+/Wxalr;V%g~cB/>^`9B=.suϽϘ~s,vc9x !BH=o!!!!!!!!!!!=i,*b΂c_s7fi'o>}sږE gl!'IG3O0pV? ~2Fu[$}e?rG}Ke@fktwS(">SBR[X4BOely=01ܺ~{J_?,׬n6V+6d3۹hnn==\k/ :'[ǧ'-ښ+wXkCqsF'>׸l\ل4c/?2ޮ}ȸciJk.HږkjɊ_O "倦Zˁ>R?K|"^s,anfVas*^4ػ߿x\>DbPG鉈y q^)I^>~6qv\kk7[HC߁ G`%)9SO$])1*UYFHTq,4,pg.$I-k#"_cҚ.S~vYTDFXKq"Ke GyAj0sqCQkTR(\}w%po,e"lPeX*vzVADo?֊kN񓱻^m>&m~7f/YVUrnWo}t[߯ ~'*T#vB{哈3^w05u "Tg\ *_:b$蟋J"J]p5H}p9:f?FnPeV~~[q2I`E%5 ,YwK%u)}_1|Mg􉫌e 8ddL}I{sjfD O.lDOdRxSY~Z}QP bԌ% _A}\$p@A0O9ՌH""AX"RH4wRPHd/MK r6}!F+qeƙqc9K'7:Uq' 7 b?8b${SӻP-xhߘ\$ H*5]RSH01""ˉҘ^J"b޾5]2*ٮ@/<*:xue@aφ}dWn::'2:zΠϧG_ /<@ /<@ /<@'~BZu6&|{}''|4=@AvS.D5vw;|׺~{J_?,׬nj57I3ۃH$r4v.naOOKDqvV }f'6s>x쩅Dܜ25.W6!̭l2Xڶ }bד2o9-VrԧaTꇒqIz]Tċ6#tuNhofOj73a0m?0&r+tzDt`v4hw.΋Ef"n1UDzr}"6OdWOI[KVU ىH}V`HYH>3.VZ=A7y /<@ /<@ /8usZS^'ߐ+BuV˵K/Jy!*]ڧ >s_p.dDLEm~ ToJQR['tFX`ߟKFƄn=^ޗ7kFľN$D&\K?xm!xSɑՁ?.(/iB@\<GQs|ý+nl?eDW3#|?3nócH"IA "|CĶ>rϥ٩E0ssskv,oF; _f7FtbySqBp{ _n)!2 =Ddʼ!.3j} ¥1/?IL#Tj*u"`cDD1 Dļ}{EkZ/KdU3[:"޻lF ;;WE-vXDd7YT6Ql%Iį_bWQ(dӉ+DD$P?wakGwo:j# )=Y%,?N ;rnSsivh227/C<Đ@ /<@ /<@ /<@ /<@ /<@ /<@ 瀢Pd IENDB`litestar-2.16.0/docs/tutorials/dto-tutorial/images/field_renaming_strategy.png000066400000000000000000000533051500564371300277060ustar00rootroot00000000000000PNG  IHDRe;E pHYs+ IDATxg@5pޤJDE)^M7jO&11F'k, b)齗;+ϋS4 p̬ٹb̙BH-]z`"`"0wPuuu~~~ccB3y< B *??ihh< ԝ0s z0sBH}0sBH}0sBH}0sBH}0sBH}p~.z)XȨ[644>^,?ﺴ ]+W;lQzzzXXӧOލeekkxu=!&&&֭+..^rj A&M2eUmmmTTƍ666-dp-[6bu֭kVVV-_~˗/)++۵kɓ';d׬Y}utt)&`0rrr.^GKK+88W^2,))իC* @___$ݸq;sjj*MUsqq)//xc|iӎ?Bdnsܹk{ݽM*p_rppRPn '\.;iҤ VVVoCRSS,Yp+ϛ#ŵ=~fH_~Μ9͉bmmoݸqc/\Tg a5nܸqFFF#F ڵk0r޽{_pA*޽i&9}t;&Ndxx8M:5;;vvvGJř$''?E~. }n}骱]P5zZT?]  YnݤIZn͝0a\.[n9;;{yy^~n~\.o[xԨQ6l8s d7B٬Ybg}`xbE޽v//C:t޾͛P^^ncccmm}5Prդ$P*ӦMtttٽ{wii)hkk{{{GEEIRoo簾'N(<A\꒒ &O (..zjIIInnn ֮NMM@CCCt #?^VVR4))ڵk GrJe~~~XXX>=R+8::z{{ B.Аx yȨ1))o'NTp)թ9u:s_UTsXsΝ;V U144o)SADEE~d23&&7/^Z^UY |0㳦аb"ܜ LnyyyJ"33B,|||̲,,,S)''Ϗfd2 I)&&FU;;d OO9s? ^^^%%%&L/,,ҲrJqq1˭ իׅ D"*=Νs1>?dCCCս@`aa^QQ5t1c۷⸺i N:P(}||9zi̙qqqzzzAAAE5/] "詚K3wʕ<oРA d7Dtҧ=y+LkŲ,,,BvP'' [o:u!PUUUPԨ6ڭߊ ZenGQgccs禥TUUEDD&4O-(J,6 ST$ھT__OWe-MIIiޡTMسg2d͛7ܹ666}),,TE5wE"ѣGUW~ԩ999Tg`AAA\.We.kccs*J'+77߿@*((000pwwoGoS5#O?~ttt$wvFFFYYY]/I]/A/Yf۫VJOOo]OOo֭ DDw݃rzzzfff999D3TťKEeffz{{p|}}}||T2ۓ'O^xqBB½{Toff|sssi633kf7MMMhnO,33Su\SSS|T+TSg*jϟӃӝ7--M5E xxx4w[kb3fxU]Je˞TK3f0:::m;AͿ4lf{OI&;vƍ@5A$WMoUWUzկ*4Mr_tPT*iV MDFFo9bRAA-[\]]uЊ#-h"*U򼼼Ȗ;4-穚ӑgw5cѱ;7>>[C/1c̝;wmn޼J޽Kt}?^^^ͳlj U?4z;,**iV(((>ollzښ(TRYYj`ё սvYYYrd2aU ]1J111۶mׯYYY5'Kڥcjjɜ KKK TS***ZVTp8G=ms[x </88\nGЫ$ɥK^tI,;<_uttLHHhy=~jZII-Z4jԨ NѧOػw@ Xzu~-[᡺aTZb9dff:;;{xx;//O.եf}FRSSkjjƍJpm3a++aÆ75qqqnnر7nGigggcct7w#""BBBlllƎeqqqJvp8CUtܸq%%%ׯ PGG^zGd^^^666O՜Oש+HU,,,x<_կ7nt|`hh֭k||||||6l<$ɏ>(,,,66MtΜ9{iNC ߞcTov2_kaaѪg\iݻwoܹϻ#WW׌}ս8iؚD"9yd'yGell"P}Y#!!} DW!- u"BH}0sBH}0sBH}0s &#*E/(KKKD^18W tuu{ ^ť+BH}0sBH}0sBH}浩B4 $ryW!- B.BꃙBꃙBꃙBꃙBꃙZ 9ZB.k}Fzi2 7rw=BO WKL:H+.Oe-C{[%U޻ƪ,|בL/-' ?={޾qͪgA7[ڳw?mN6tt66ӣuo0VO+`ς̗WS??3ۜ~gܸ:u^R%!C 7|,N3Շv_n{SLr~/i W)xCtݳ8yG>[Vc?eY}<[N )zzO ?* r^?[@n?ۍ9g |Q  rC6XVOwPJ!외nK>Qd\K6L)C9J݌8ߕ &~ʼnidQ"ɑ{sj9?,6jЯ40bm ~uZg7 xD}nM˔G0<&/6ɈQxz3b{54nߚOVk+ ?[CMƏOxg-J?y=^7Okql#|9ؼgJ M( ~kT0(JiwW3^kcIckcecm@5Olj$9*VlT-_OMyЦ̞3ΔQ :[:ٓ=zv0 XNړ?|'24 %CK|Erc m/-r)Eo.fgt).B쩇U1&%En[2HBMVWQ-v**=KU]kV5v ܶcu'2ڒquF\6-Ԫ-а))t,[^)Cu" -(,Eŷ T-iR*)ZTVPY/͍24whwܵs箝;qaz^>yYXS~bٺl2 eqt-.UWKRҀO+J:B< t_eM[m |7'!9vI#G]ˑ~;n*S}>磑%7\*Up,Ξ;S&c22tqRd5Ӭg{bGiR3ZB7A}i<֐\n[}'PȀQE+Ute?>tOh\+:ZLٿ @PY#4hi5Ur=Z졸t1Ptخ³%/>zI=iZBec-|rށ75[8x&1Ive7 5nk(5U 7#8W;svuIi `n!J'x&d ?!=n0")kД̇=vg_SE XkRRYV&M4U_W/{bٺȆU:JPq6s,iҲHZse׎XdBݨO3ߟl8h{!5Ns=hƬa:y *o7wdwg{v̗noޣH'k`2poL?= H&$v7Y1w5NTw76n ͻ1ي֧ap5]΀Ӈ"*h Y, zSzzVF<$dHǻtE<3ikZN^Ξaޱ"̵&u鴟K&^|=4KvPХ~hEr` /LJ~WE緄10,4ZȽx:ǩo<|%? qmu_#j_CS:]/}#'PJ%M}R&K7 dgyw>C5WhAyp_*9˷g\4?c**=TML_MPni2-5^l̹\M{lMc˓-|d.d4UfF;\>!BꃙBꃙBꃙBꃙBꃙBꃙBꃙBtuxu@allBv h1/=RYSSPߣ,5xٛӪ|>~n`0ttZG:R9m/k{h=j(Eeޜ4BꃙyWf.BSAz 4{WPZ/sOaܫ}ıgTR8|xRf-F|}}rcC+Nj_ B4vVܙE,>^,ZPtFF ,n͵,J\]{/\0:ŨFpH/i.$s %G,]MeS\Sk&LNg$lHA6:mEŃW&:ZKtk^_E0Zˎ)mƷb1(TH$ mubdW@V{?_Ì <1Mߥjb%nQ':\Zr#uf1y52EI'%~frr^d~inm\st I?H-n܀{00֫)(x>z86d6 2eg8RSylCxCmL褡ód~)B4Xa!*v#=j{(Uy4,1ȎuuJ.~<ֲ6nxS5n OmRd0%&F_ܳP:mMaL**oJV#F IDAT7UwxlF$kb^Vp-,wvFܑJˮut?lGM}lgp ~԰9z \נ`(VxOmhV375575),.sSں 3<N tSճ)BB.nYnCK_JO,}㰠6,,VεJE!VBséoz &ȵHzNG4lJ=%RPPl/ZVt;gDe# U,Mj6>AAяnY%mdxx 4K#@4ʺgژyA#oK/ztcfEyT3\zWP4U r%5)(BS~Txi∝g) u# qrDYn.azpB^)aRQn.]~eێZ]{o u:}C WUuhh?>b}S'a!vi" ZZp7"лoY|gA[e5weUO]8%ihp heqj&3L`]zCDtܪyWʪjS3Q- Z(eqR*5,[aِ[2*QԲ~) &ݑX>[yi ){̜8#2/8h}:so6L]$仏"5 zV著T׍k(ȭp\AMwxQzM2Y'>&#@\rsJ2++ lVi)zeȫ4< }yIlBX U\6 5 Q$ rTAZY .#Xh"~6j<ɷ>骤|!UGi*5jô, MH(21Al/IM}|4h?k-o`EVSE[x`в&)HoA`?P"K+De1G-W@W%ޫSH4$]<;"pTcCGt}P 4Y*KWN5|dcYRZtXIa92Snghŵ3᫽65Y׎MlSM椔5v96#$P!@A{3H@ʒN' #߹-SJ>|wX!9ZqAiIVzIVBBmz5vԒe#Ug=pT!ޓ#\B3\7Vi %PϺ%atF]2I)ew{voې5 {߬]m&e ⡬Iv)vwPM aMDв e Eqwh [M6IJo%TR4}J${EIK_ϨyiOL(0 Gv۫mOȭ-?k0K`h7޾-gu'[;6Ndt}cu9\7rssKLL5\1=yDTjanۦ?SCu=o"ɜ:ԯI&wP?_ ]K}aR9OEՆϸAꐘ 3!ԧU*PsD)/K ^\Es'֠R_i[>B|n# !>8o!3!3!3!3!3t~ʞZ9 T_=oduS&I{s IU;esͷGNx$|T39',l ]G<˷H m:9d= Bjԥg9R(`?- d9H.^u [G_UY5EX wud%>WY2*=UHN1ۗ6 ޢkˊ3eJ4i[e I 27'+TˤJj %㯽%qRSE/` ?[g]w_f[;}Ξk=b4Z$PyWD@p' KL1OGg5PՐJJQ_-| LמY,ȻWzd+lוLK ܝ5A&D]`1%+RŞEΩ]hFwׄ!VMky[n`A5˸WJ44oܹ3<']r$QD#@zΞS$8\B %i+b|z:]\'n_5fɝBpSɵ y]uba:16ƼoRGVh;[aӵ*Pⲻ4}f]})emFXfei`  4(b)nzLiao "Na\}?M[enN5|KW蜲Rn\$A@2zA.|G F-B/PTG` sV~'V 97 <-J+?HXZ~a_T+vu JYlDMYv< $+9k@QV$Bm/w+0!kPe33W 40 4I|vcݘA"go aES•a";)mUht!.3/*]ouG@PGRD gDS[+S܋ǁ5e $%py7!u\pk3sȘTRO!>E!~.Bf.Bf.Bf.Bf.Bf.Bf.Bf.Bf.BOq 9?:1~ĔR 6Wp (JnXijH&S=<:9 F>p2aB~v@ Rј)z4>]2eև]7cZZDyonԗe!@zRloХ+UQ&*SLQ ;Զzk?yXϺz4@ #OE AU7+kfj}xWdZJP*HA,tA]DHϩ4vƑ^w]>)&{VZUmr,7·P@WJ|yIaw^}y8Ԥ('gHj4:)ib"h+Bt!s }Ӿ+#(9ߙ n4 ң΂ã/l3 +ⲯK(k㖟txPygQʺo.߯l0ztӮ`p 뎹 <}YTBOh,7}D T 8ҜZ WSW!xE:gr Z}!'V 9կ"`#vQ^oI&ej#㹄Y/6 LPBD)J+-u+Idt!ZxwCR,gޛU5|r,SBU溺R Su.~P~#@ҍIYR|BStNZ@赇D v!a"`"`"`"`"`"`"`"`"`"tzhQ%R(8)p& ݺΰ\A5ۿԚxfxCIdntk)udt&q-3=R@CI5[P 3k=90)%Uyo'9.ˡ{C]w!>X3M,_upƊۮ ߮y^qt% xO/BF urkWΪm"YP2k}睷lNMM-[lzh{øH_%-vzi;3}M)[6j\BjVۺGJi! tcc ?fz/F#7`۲&S@Z,]frsUqc^mKoAχΪ. E|u->g 9yյr/To>ޡuZT~ZkPwP؂>ǯI)ZT)xrO1UM݆qNXX|nB,CKeR%-6*} rvL$@`ށ;NfQ y?d֣c7,5ba4k ?9O;WFM6&HcS?H6sm=:n׿^YWc )~§<~3iB;/sIC;8[$yGm90D'sa`7FCEQs꥓C.˦@`]P[|C|u@Լ2#37xaQ@J7_Z-*! #z]쿯,#2<'rdjN|V zstz1E\ WCosGV=^jɜ1sS>g[ LY26Ŧ+x:$4R=!\"K!g> J^Phȩж`Bճ2a_gRodw"  UM^YY٭0 W-A.|NσVjϭ{ @hjY۲> KXZ~b綦Vb3x.NTkAqhF(6+.g=ι$j.tCLhh]tX<ʊRlj857sj6$sҦ}f Vg̰B%֍GnO;5rA?D?jV7l9:zX@%IM=A_<^˯>T<639y\,vЫcrwQ,/;a7{>p5d*c&^?6kʱȩN|!߹zzh/.qسyTBzy^v?ns)I.B"mR\R\R\R\R\R\R\R\ReuN(Fu_OJ)Us̐?甮Upv;+Ȩm3*c!紖Z=;l\\ 3WڊkR DY" {W];+V)(ITSqIixI܍?O{rUoS!y-5D_ʺiݯ+ATb=/[ 82? *@7D eޓG3XT9kvZyQMZ8kܑ-߷TI֚JΕJl4yZDas&N~j>1 {15vgXw iLHR~.!~}mnv.i (@SsACCJ6F^/mъz"ԗFubz=Õn̾fƲ2s^Rjs ]k7 g! nn#u/iI1v9?$;KYze 9Z9(=y[ec~03쇧w(h`i' 9zaᎪ-4M)A5B)Yus] *Kez34ma_RTDZ`gύuzl&e؉p˼UV&x״Se;ʘ 鰁8 EIjf$S_i*$ BGUtY~ ,;qyzh颺FVGAQoǀ`x볞 x)E>J^cG}YEw?'vsB*Z4+ `.Zz緌)FL-B {pvCKT9"#wQ@5\o:Lx<:(}۷8vRe_IDATvAW睻C3nj= n+p.FAmV]Wݔ_Jt40}f_FyHe\ޟJ7?}t}Z^5녜z@pyf~;л]v7kWSAG,bjZv]w!>Xוm7rՏycL3Ŭ>ooKA!wy$ݲG6Y7ԈiX߬u9l켍 "̴ӔPWEN榤xxxژjK;p^ SȮ}JQ }Cm(zf?}诣(y Q@YqLkN0eidPP3U!W;fsDDD> v o<# [?jnxT ]y ⑱xVvI-es=aB?o!,,,%%EsJJJXXX}Y߬*e.oB뛵*h2̟Ճ+? Pt8WL.@{uK|wSY-1DȓO4Wsܚg!4:˰V.6g#H_^ho?΃b\wTs!?P]cs ~,|Cw~sw{d>37S $+Wp/EbVT%HaJy@D BH}0sBH}0sBH}0sBH}0sBH}0sBH}0sBH}0sBH}0sBH}0sBH}0sBH}!Xo zkdYolI# @\뢹M_1)8sztC? O#MQI $Iݼ5`P+ssVDWD7sWhi)T)0n(k6%00ce O~k ,(Vt!'Y?)n4 sKh-cvmq#zt=ǮF^-r|wHj o!^y\`-\.3Fo`oD<{<!^mB~ !3!3!3!3!3!3!3!3!3!3!\){j؟?7P=7v)T}Mݛ<[Hǯ' 4$W;#tsͱS69C d/ 5Lk <$iilOȩ[ Y7O:/ :נ|j0 ǻFSAqyr*.ʺ}Q~U9T-6vY5R6ŁiMӫk~SIxoJ\WH]B/2Һ{*I/n?Syd3Fm}EVٲl +nn~n)KXX|3%2JUOcgu+ٹF 2qrj?ܿ! ͜a[WV'voo/L;yyҦ/,UʼnoÚ?$#}S-(KA\ZHA+ӟSvq3>Z8cЩQkRbqDBpAJ"ADbV?rI.|wxum4wz cKE|]i_h}mYi]'cy^9"JuC=gslٛD9ٌۖNh][[(ONyq?@yh 4444444444N%韚dk)z4")QBmNt>#A\WDr g }WsVTVVCFD2=i6""knYJ+v<(%1+irߞi{ 4&y`rrEq>yhwmrӟD&FjZϿWr[ypM/0}r\~k֎ʢI+?mvϜjĖp\Nٳg%HD>s rWt:]AAO}taݟݻwߜtݻwߗу?Ԧ}M褴 ~N={nك9WȌ#+gJ's\KH߸YԈ5[||kι=hlA3)[g>yv_c>,Y6gd:q3(y&<8Y-e'זtOp3jceGwFt}j~74A2٣mOus\2Dm4sg,NK;?mô޿'^m If8uq/sV2Lkm_;Hff]8Wʥ^. @~BծNiIÇ$>ID]ΜϦfnZ>'".qyzݖ(Y/'ǖmߴouu 7黈JU.bnb"撵qGWZ_[VZWf ^.SHFr9ͳcWI]s$uMQh7y۾sETA-=\Ѽs'0C[b]ǫRމPߑ :>=MQVWY,gW}ND@D\LM o'c>hO&sk!nxR?{xl&m4&DLgJ sE̋-:d_. ѯih[<喺 m.n![ W}w#W䣰&\"E'^H?|q̊99Wv/ W7UovX b"`V""ج7rQǿȕ.ȪmCqDN\"\-ȕ<$CF*V.]0aߑ7hkra#zY;:~v$""8Dqom{8k1:)爜#u[r'-ҽ A%akEDJD>.*g%. 'Ɉ_Y1mʜEF0dg/+|Ý*>9{sD놇a).Nٲ7͉r-9͝Ѻ&;ڈxs;&ӣ;g1_(# Y;h.;h.;h.;h.;h.;h.;h.;h.;h.;h.;"Q?5TRt6""N1yiDR,u9ڵ+jyFDd.._ed w9qQDW}\~o\_ >[_#`KE9 Ipten0~~] q.^q.D->>۷Wp@ J^[4 .eۋU8"'`(CڭDӅ]ͶT8jB'?`#^2'25ub =y;%*Q>yC~zcgT_pbVe s< q V5}{Z &X[8d#:iB+Ͽ[zz!a߫rv"^[P Qx<7uWfotvv_ullc=&ۏ9O?_?믿q r8L D"1111$$@ ן?uZj8gx{{'&&x<^5<醄(JG j`ڵ0 ̕><MDUUUPPf:k׮!9Q``V*U;'%%M0 h[8pf~[Rp! K/^xT*^+((^y}*Y'__w}7++wYdɋ/h0._۶mLmC7nܘR5kIKK]6((ܹsfyժU;vOp'w0d2WFQ… .sNJu%l6WWWV'9NyyD  b6_I78My&HJJf ݬ"""x Εm۶7|s֭ohhؼyf䄄8?qMOl/C+ ׭[{>}`Zgr?%///44<(~_'ߏYlٷ~hZV.Д2صk_mmBp8|I[[=...==l6Y,'NvZ--Z40 vww"rws\ HKKKJJJkkk fff*JYr%ϧo1 IIIrBǏ|UfsYYYZZnx5kp8p8H$3Q)86MJKK˗  W޲e'O:5͙%mqi`6Fp M3g8k!iN|>q2l#0** At|=$ DGG幆sULL˯\yfh/_Bz{{}||Ν;=<<:ۿ}}?`0.]LT*pBGG۲e֯_W_ ',,LV;w r'OB0>>^ |Ώ[PPp.aŋ ^S3|gZ7z):hѢE1dzGn!0HHH Ǐ0"""33%J\0b:Ď;!?Cjɓgtuu9_C&kс F 2//Y; ﯬJMMX,k0aF&8Yc2 A 3 h mmm겺_|@XtivvիWF.GFFj4\*jf2&0zcǜ%`0v[__@V;@&h4*h4\.?{+BἌٲeOCC˛&OOψN1 ojs 7=ẕ{ܦBN@(&y%t {?>[UU5}.z7|sv:`ۧL-VJe[[2~HM"ЖaV[[ BIHHw~J Frss8X,rpCCbؕ\b9]R[[\NCCBq p^]X279wNUUU}egtv ޢ&L6AjAshժUc`{s V xØ@ p8MEV\/===qa漿u)@~~֭[7l;{3L EQd0>pl0/X,ΪB\xqH0 s88;\2n>~AVp.EVSS3Fμf+W Y62/8rȁ1@PLU\ri duadž@bbbvvq92o<2<),,qYpaUUF"n8;H$6778H*&@SS Ώ|}}1 s>ilG!B 舼YPYV>5h%0yyy}Qoooll,ە 5"5(&L snGGP(t87B͹SL;no&pN_jhZFvAQGxh `>U(EEEghǏwTmmm={u-Y^p]@ܹ355522_2W_}566GrG1<P[[%jft/ uloowv*ʞ7: f3 77Bl޼{Ŋu.44TR  ߰aD"`vZ\7ژWBu<55m۶mr<<<|Æ jZR],fyd eٲe-ݸqckkg)##fرCPDFF:VkgggLL\.H$79w?>2L3FxS&yuUvu8`t0IIIu^'%%9[B,{xx]vڵ7x PXLVꫯƧzL&gggo~AR=c={ァj_yE'(%%y`0FJr"9sjZXrr7|zm۶l,}bbΝ; Ù3glT*a ,K;v50)77fĄlrgC{DDRM&S}}Zg}䌰ܵQc t8t8 ::E .8 oYx:ҥKYN>nݺ۷ƜF3͹ {M&]SݝH$4s~dpY*&%%GPfRVV?FO?jo_5ݢ{5Egy&999??o߾/5tw}66kW|_(,,LJJJHHJLWMzURR\V]]PXXN%7q2L?AsP(r󹶢MGͭv A4S} hNi hNi hNih49&L,%L&4Qh jllt}OAhcF!hN!9 9 9 ; A/`A; 4 4d ^ @i0 @i0 @i0 @i0 @i0 @i0 @i0 @i0 @i0 @i0 @i0 @$ ^X/t04JqȸlGJe|@Н8%K!I{e^㻫x,^vٵr~Cͽ3_t$Ye0#'lkItK:幗N|Ԧ+ld )cʢ>8gsyY& [|g]+DzMU^C`[gɪk%"5"#'pCK>pg;iT5?5Ξ p<ٮ.iDsz+ׇ'NguNfC{eƄ!(.^r̯sB}K H!}% xqzlӃ+ {41|ͳ14Cxh (C~fw߂ \0Bё/vDa]õtoUH힙ud}~#[cߎ_=A~Ӎi AH# `uy.x|^6=}UObFչ7٢+兯9k 1>[˥K=C8tU=+iF 쨝-qT*?qJviǩz"׿lbηP?ۻ?x }mK'+*37gF'֚KǾO,Mb}}q{D36g+/bKL"'rZ\M lʒ/۲u /w2fh-.gnB(^7lY(܇GÍi'~cNˢ~׍J̽-YI"r8KVCwܑ#imdq uvp=*NAy b}14;)ksk?Xx94:VCq!)ttc8"mtV]W\ʢ:WQ eFx\X-0" _DVVNzEP`eזQyw[G]YA^auArOo% J&5_m':D{,vTUteq[~'Vy@m}O/>.ZF54UVP]5DA_xmR\z5xz8޵W /L-5Eŕ#f1߽ b8;#^G-wBr~)joV~^󕽃:Qq>Dܜsȷ!%Ϊ ]I:z:`}9= omzߕm~lv @nj5)$('??4xro J2?lG?P>DQ@$?)죭s$8 u8O|z =?>weJ]FX{E<g!tݯw1s>x탴vvm#4ZԮVݹR4@--VC ֋oiarϬ۽bǰ%u5¿kik_(4seޓ޾Bos/Ž (BՍ(vDM?|xt?yJ0;=VZ/&_pƶb=)W1/*39۟ `B+Ko;;pr8পYMrg.嗃?p+Irɦpg~hG3NxK|=ަᦾ~0l&FMF_=ҚWO Woؔ?)|L3fTyQ\{Sچ#$'/?nk}ner/=@txL甥߾/>5W}o+$M&DP)VŎ;}:ul]y6o1HcAZ [pyA$[ev~M0{mT` e#s_-f53VFtd?,uz!w-u~DGG=[- ҩL/܎ o{3&uѢvfC O .3%l㽫WnܡVPg)0{bo >26rW`, G>ެQm8*]}JPHqU8P>$R(Wo|JF 0(T݃S¶=NμqR!dx:P ,8( 4c&)[oJw !7|ҿKJz\Sg'CxTh9/:(3忱k7z'K{_T׶\@' 5=shlF k IDATe#z&J^b.O}8ywmU_?{aH|VezVU~;^[LmK?+;8[T?1ٻpqbD$=- 5W+,P"/T*RU%m=fI HKl͐R!JnX4٧r5#ݕudX*xlAML*+@d{^4LߪRu9s$=Oc8|["2SkeiQQyܕCdz08"H/Q-i>:|a>!L&k _( A44A44A44A44A44A44A44A44A44A44A44A44A44A44A44A44A4¢nw Am^; !Nd"ˍv#H͸l6^3tcL3{uH`VF~<%F+Fh/irry3DH$g0*Zq5;,)1F14ƿEf@7efa.c6#B,|#Yر ,9 ` ` ` hT;h4A44A4M)b "D28 }}^f 6hD0 x! Ѿ / C"G(s@ 9\W݃OjLy B;s/t?>6Ԕ~0X8A$!kZS3ou,,X`o(,o~шh2>dh4iDlge s Zn11oH$$3\9C۠nwbovL@~GSM8@A{gI9iL. ~H|BTӍN&}W{SmINnm-~S/3ȈͤlkST Fz 8L*7tk+KK5}0Kؿ㟪3Wo{mX^y?*m3CAnUPӏ|1INYLVmJn!:`0<ʏظAv bNgшtӰ|7笆I "H/-DE/@i_1͠ljW**M7Bxʼ< 2uw6ꪪ"č Af|05dY= DK'39qhXWqN4-DTܪk$@W]B/ܲy5l()V,Z4};E8?̭:wƲ3nnUk´wlEDz睩d֮Ֆ+q .?}:rd2ix2s5sXOKmz2`m%N4R=#,@gs)$zuXzZ[Lw '!z-D}fDz$H2/ j!ႄ X4P /w1J%X3ᖭq"ԉKWt6>.֙Elڶ̇t\A<@tr Y2Ab%Ef7s/_d Jc$HgYE 8dkB@qiH-FGVT`P jT,ήUdt;fQ3DPc͹ 9(z}/Z.U*#"S6?>>oJk.צ80ҔxOj&"ou׃D8a.tgRE^_Uy6B v.2(V!d} 9Uݷ֚AEDgUSwM`p?;n-Nk4\HD\w8z8'Մpm4m"A<[yj&>oR9і|*7wQ0|a¼@'`hP^MQHu K㷬 xd`74W]MI-ks΍pÓ=ybcWs]iNvaɞ!K*<&'+JC\(. 'OTLШvz3@"[Wmv(oÍA!6cOk}̂ڞ9ڹ=h(ƢSI(flȺY9kaJb/ !]{CcxBR Egcpglk]Ha|X,VhWpL~obmnofW/ ;|^LĦSaB>ǍI!|fnbG"Bc0Pն[밸d:]1`}8I`TRɇ5)Iщ'-u*sx_WGE 嬑cvs{Mլ쫥OKﭯ ^>_^HxKXA"^ hkEnVAu/ᦽC7!ܲGԶ}w/ Y* - "qaU#$xiTWݳC 舘 Bz=R)r$r ;F[ B]{]>ڢ’Z]iΥR̎a&eanVvQ*v׆Psf+ 8k* JjMmAwkVSXa[rJ$:4[% vT0v.WEYCE@"-gGUe%.(|_X$rᣜ DqAgmYqA~iey@ G{w%EIuEUZD(TVp?ը,Лj+z(mb02DB/N'SȮ?:'' YY8[[V|jiq l\G%H_QT(bM55^Lz=ވ /R;,&UY '}wU7$,ѧ댘b͸\)[!־*XqQaa^WZz}|TA,nUqB{ݙ/~(H^Ö_m 'TL xhon%"W/cˎ}yabeXќ{LhBWm:4uu%{PJ3 K(ޠ=#Se61Q&4iExϞ ,>6EXsΏͮ5,@_?uYu;ȍ|QG{qT/c-)ǎeG7+߼]Yփnop^qƌcte.n̊Ny,_o _r&LsUҎZgKu j?bc ׷ U6;I} 1i5&FpoJvw>e-A=#"ez @SR^Ty;.4-J弨(@iiq/\ܫM1fzU ꮈWx[Of1R־dk4ߣ2ުW.%kD7U5zͮ@ MCqs; ogV[s0 d"zONl^wpXٰBvBg1`-5p}B1v |Eo(W;,b16&3o% dQ?XF45OW^ԎS塁֍[Zs.uT09 VYю}&A-!ZUV=2`1G rFeFv=ܬ @ѩI&{w\nH wh4.c^MJGԕqnVUوC∅Ż⃸׏fO\at\wr9r&cS2j(?$L4(Hl핵8/D(?rh8/ ^aSYGxO;QVQ@Ro̠MsK#c!Q_ bO6&CじT:NgiHO{'yhl;k.x)H/ްk]R{ҀH&ljq@"~lk) /N91Eܬr"UD-X8/|g*p@`/\8J.`P`VF% G'fDvj)FAG0M۠>#p=(HӸ" _l~̃FDnF׵堠ђs.Wi= k(*(,oܕ[Hd(~dUT"(}Қ ֝Qo㦪 )U\#b~טYp1ֹ0&qP8P;{:&!L vZSʯ_C:ei2OD{2`0DB!74p4M\nJ?6!,&A 7^FRG9oUYOHd "SEV`$ iwd >qV\)lXh;UAY-͍[T޷#*dILAujҝ܌~[ι6k٥08kRсp+w1[:uwtc@"ʛn@e ;@ຊb<"ȭ'w;3ntnZEvC;V@E.;*b/}A<"&z^؂uET}准!_fE/f ! pFy_M3 8 (\y\[d!4B`x? 秵BB ꌃNA7B aQ4i˓|"jˍ}?(HcAG7b.׺E(}dT=*ꍑQ\wJ ⠆%78d"ơiJ[eb~ )Y[b=$sv}rm]! T H|7bB<2ptֆBx<t:g7U_-[0/*ʫ +:oq+ 5jkL:0nd& wB b?݃ OP\&:zmKWuy^ 7m[Xv%A ML16i;dj%B]#%>9=f\w7}.ۭh'pF p?*QS5q2ۛacǁ@:L8mv F"tߨܙ(0:}~B0tB 1Gk}( PW KZENm/$ L.>p>,0p$h {ˋ65 LBuTWD>S{?-I+5[u, Ze&].#fUy&B&\CBXS=ۛ 1øXKq2j$[mmyGNMEU/`' }P$qTZ0P)>s8DXMH&6"M:uwbe; rS( v iiRR:q,LBoLPs5+H?./3V YLv$AL\ `salXG#įeHޠ@ADGS+1#-܃CD@(ԉ-p 8rvq<鱞&NfQ8\7vq$\.BH<n2cGWEƄ'.0!)K]UE5,u-v*c Qkw4Em`(-z >qQ+jl <x_UQ2@\_TXdGkvU=$UD\[- gl[`hGSsތ(,HGʎ~t^m(>wU&.Q^8X_5vͪrƣ9xwIfyH>R{_coguQ \Ԭi59!>d;vxW:TL E=$0!Lu^D{14z FdxC}P[Kw%'4UZxGUugB8,=dCL.ݺp9-{/X4)bAȊ~NKW.+o8[{ɆnNf|y̝+ c:reXAQ}߁x>ak"TLph5.,f"0'֦"iº;zF[zJFRX^yqqXIQy, ^HtYP+'`idѼ RC3p 8dX~~T:a|Ύ^ FsD< ue\TsPW(]P0 L&k)=a72%^lWK>}YًTJ}R/jhUU+$8tzPy\Jz֮kt_,<~mSwq3ڪʪ;mD*+Hң*T1N㈼}}%<2OkI:߭ԓg;EA\r{ĝ9ZأE;k*[ &%L]ZSVYerzTzŗ}eB7D*|\^!Ԯ )I[ ~iPG+=ݮUi\EI>^n>MYkV^<2m֛ؕ%ppOSFUhDgӵwK"j_L!C4Xͦ!hVE2n`F~<%(Fyd?>A[As>^A4N!9 v Ai0 @i0 @i0 @i0 @i0 @i0 @i0 @i0 ~5+Қe5 P\=%'LznL4W/ }~U]w{N  4 o`j?T^O_c!{K/֏_rwtHL[WU{JqNA\"N[ 4o]ȷ#u#\4> OU&c?z:3.56 ;!Ѵ{Zo\EG'=H;V|_; X eSҕeY|5KÙJˏTcҀǶAw+z}:K{%:s~9⏍ofp|~w+5Ug 2iu~:ɿWYց!{A(j:O-ip'qΊw3֮ͽKyr^T0Ok.qu7b"7i6Q3t XYPWT &+號J %z?Yߖ I,nŒT$+i-8hrןZHIڌGm>9r;/8R-Ͽ!~Z` \o2КmECwN5<i[Ecf?Ga,֟ }hBX^Ϸx;y;v`Ҧ|NgM>Ӧ 13X'0wц#/Ķ)=RL.no?q%8pZ4WNU`vA׌{NUf8ۗX[Jk1k9!2lO;T<$|$V;~тL Ev7]:j r(, ˃ĺ̄pQX0_e?i;6*8vM:5Sg(=ssg{`en]Ukb]AnNAv)~mLΟ*@Ѕ8Xجg/|> bI~ F Ц?tc[{] boQ 秢NBM n㦦Bc)5Ucey]$Ъ;j5&|K@Bk.0Kۗٹ.M! RotcFqnk$ c{`NĮGUPxҽtI~"O!^e*j`/a,Q/oҧ=c`Eiz 2\'*v1Q/h;Vfp:pqgrVܶ$ӻnj( =+lJɏhv7sM@P/v9>ɪߏsqV^fBKR3TTP,'h^)7Tg8pII=39i^WqVcلtTkRnTdoϱ0[Ac҂FSv}|>, [{~qoQYk'3sAC%ƍ{bIYvtqJΘѦ}j ݃ ʲ7Hz^PClvI'4\K~aJՂU&?Weم?ZءT_\︗RT|IAcSsv^!\/GXCNNΏ:?0PMd Y'  Af F  A5  Af FAY#a ¬0@aH 0k$ A5  Af FAY#a ¬0@aH 0kGoY/  :E'@BQmm)CЭhGg +,P@%$vQU)J58 v dgJV(KO%Jr/י\|'{Q9$qwG({66OheҭMpBܵ܋[[bNd­>7~y#Oe$pp的E_ 'xЧ}GLtJ$,SM}^\r{l WyzpҪOSKgM~qlٶ-zЊӝrA.;kҫkQ_iwOwk+r*D U&N=TH}j{ݵD\f qUCY m"9ml7~ȾƊ|`a/B&Kܜs˺(ơR7[~b  uqd]XB 0֖ߦ[:   \+=PV`h2}ʼቷZחq1@ǯش |VE m<̞^}>5`ܴ5?$[ kLje6]0G[[TaA/𱼊yeUj5bGJ.;'uZuI@lڞWsUSnAuNBiSGs4XԂsX($<.,U CYR36TZE ~=}Xr٥g=Кx%z&>爴&#<\XivtU#Dh M-,|'K֩7y|UnX?IiLAE7r7žaIGd#ҫOg264Tjx)l]f xtWZO\^~~SFNVg3g{;i0,lX`lcȊk X}旻j;N-s6iJ5Z4:jMtaZ 6u|6=g8`x]AtS@QK ;+kúZl4& BA`jقͳl[ĠMЂ֔2!C]]PQ1+Z7AB!\ {X|{g))lM7hd۪O[`J%u_-y( ATg#q j+8`; +$ qW({_ Y;~Ͳ]ꦴҔr?ݲAN7`o8>c :^J;̹;~/EO~hOJ<ji2T^r b{Kfxx#:0pr_"yɹf}ձ%/}\aRd?x:_UظnA|~X~:Hv㊪e294_D lPcDyP]-ըm[&72/ñQ%#qwL!]nEF)O}|)ylu,U5ZeiEl}u+\YL^R5Hu\`Q}_Z%W"9A;!#fK66~VHW}(yӵd]tJY-؇_v=AtAO~%|pB;JAeڌٙ튎uXGD7gd+7 :V@Q&~_S,0&>]G0р[׾yڏݷ8G'-^?Jg½XwT7K ;N6tGɲgi;6%w훏QaG:*[+DYl )= W>z=᣷ngD_J T'Sbu[/ `Esj5NRVW}*FH3Ek\ <{ع@Nٚxa'\'._3"ښh*- fJsJ6Q蠗0jKohu&z%le:Ȋ+pRox9y(~I.:5XsQ;=XOO=NČ[<&)AK~kS'9/ Q[˧'7 muq|FcL-[|%'i)8 s+O\+Qc霽WV a4tB'r\0c5_]vO]{g:s3mȽZx`Gƙ,CA9 ]ߐ2dn|Am@K{t[mb/w}վӆ yo@^~ K(uKB"|5o:[U^5 "8SF ãLeb":uLwˉx@SZ vmH{VRGpR\rɍD17Y6 #Yt1cm²kG\<úHBn{+̈́SMܵ-[_j1ce =DH}]ə[T1dғXa}k 1 9#lΩyEv5I8_4֮6 kI%Nю /TxpCYиkxNYy`qFE 5V_N ˷'Nf+XP}b9;9NǗ*Lw5y+j~RYs . κSEkDcw=sZlT+RN_l<ɶܸq'nV4t^ZKdr72sܵws+O[W^ tfvi<˨!,d.juR)Uƫ]y7_{%WANյJ ¨1U&hr \xC'9yD}!n4'r M?{wLl#1oQEe3vWyOP5w)al8$gcUc\U6}#ݔڤh7EXO=]APgnz"rЂ ]AߵػL0t{ﷆs+3qXY_eƉ wy2z{a4Ɉ7^z%c$ꖣ})F\/۹eP멦\U2uӕB6qbHIxBFrU oӲQ!41W(m]5-ߊ`*M1q|kvUf%r}j`F_WU4zT$*SL$i|Jn(;ǯ2Fba#hU`>+5r (cJ-70*9)\k;=UtKIm|8=C'jn%qulbl==A:өo4Yٿ]ܣ8yC;w^s)9Y)u\Rm(ݱ9I^[X jJ-,,'~6aXׯه.fYTS J~:ɗR?ӝVT%Fq" ց֔l/փ5Uh`j7* LU`tLc 6}Q@9{N5$z`hH^ߔ/vL%`*i9FSYpMa1S-ׂ-_zFukz А[Z^Suhm̦Dn/\s'S5md1F #|qg{8N>mhU܃vXجg/|ӟA,iy i@ocLx,.Krq]Mn;@>"Vi7gR`4   IDAT%<2%ĺ 3ӝ*"Sژ?Ufo K+ͲN{e<5W0|N$w x(~¤{Iחo-c5ocUv+;"V'Lzm4AG6 \SUmхί- oռ;+֗HkbvaLe)):?K̳dCsW1t4`NIrztO@K0%߭)Էf0t` ۽4qגּ$h \]F]WhXs7:uF0R7&0fa;۾Vi 6˖ޒ[ri۷FM-t@Ҫ;Z #>К ҿevn. ^/jޣ66cvnijp}yBi"Ri̐&JC||;e96|&g4FN}/`k#n90`nM7u%+a;Gĵ@V*A%BU6bڈ 80 qoouO&g_ȼ56>P秗og!5+n'O[dԓVƄ+@3%F#Fl%jBA/}Iܨ6 eK{޽^:l̟nDBm 7``S*C2在͊?}uhŲ]RW&&=vH {F Xk=)vcAAhC &_iw!0j{hUQ : Xh+ƌs)*Mbs؎ &!}(,GLnqqﯮLbsXca!VI+7Z]_$epyBa#>1cIza*!c~9b^RzKH績%سeg@ ]SXU׺9wׯ,X>OV:\) z/lEhHYF#`E|YSZZt_W>uLEY_èyN®}OpVCa-- 789;Lb(p_XOM朵O`rVܖO$ӻkc=Fe(-|'qMqvuMgq;FVM7.Ho> YVDm!7( fːZr@{D8'?3`kӪ0r?v9ٓѫ<@u=$ږ@ʜKw2Z ёm>xF[])o~-k c_ýE<ӧb/'hrCʨKJ1>w؁rܢ2)LyѱxwHī3Em hNIB <SS,4>\/]jΤ|O\nW~m' {7QbvfWRiۺsT8@v6qGε!p]%R /X: *h :[ kAQ~c6O<)˭G;j_[TЅ=0r顓#WS9n}L*0j ('q#-Ɠ`o?n0ۧKw{ [\ SS^ ]m :Nv*٦-%x] g.)0gy646zuza ͉8+MӱlBGs5)7Av28dzgJ|cY|5)P/ƴKrN+9/l)HIR*raӝտ/J\Lw |iފDVփpή8snl[sIZAǤ|ل:viU}nMP1S{L6sOXǾ }/qȡ;oNQbFQ$7ǕSU?#3JѠAͩ7K}nѦ}j ݃ ʲ7H;?/3>M?I /Qڒ gWoȐ>phF-ck٨82oSBWu^~L9;O/ ^Ɂ|FYPt.#yY]Ƴ)>;gaVV~쁱@[扬}^`24WAuejcgFps3᧮>,E}'/wc7jϥmݜvwp] U><8jnlߜ}RtcõT-XoYzZ]rbRy1^Ay ).Ͻ<WϹ/^#knnP.r\n}c=/Զ{hMo^~r蒭צ]KU=_c.ڤο}= y0u?íX;-Зz-d}>?9 t]/͘>{V4]|O۷[q*a3 : 噅_yAtI|}tΆ=Oi8So~76=LB6Llݷ&z=A ڗ !O߿wI>BǾq.>L'Dou)_ⵠw4-SaFw|/kOU ѳeqiť=EO0;3Y3 ,Bj z2BeR/B]-,,lnn ԧ0xzzz]c 5UXX444"5e1 s! 2BeR/!^0BHf##vCBBv/BGڶmŋ[SOfQQQ۶m>|_~xv244\h̙3lvW 2eJxxxnnѸq~:?ҥK---s9pML&sE#\~v5660aŽ;Ν;2lΝlpBXn:Tlٲb 7|m{{{9رc555PYYikkkccsummmWWk׮%''R7o]vv@ 8p@yy9ܸqG* ϯm.֖1k,P (--vZYYYCխyfZZ1cxGQVV0mڴb{{Wr@kkK.D"===U/Y$//ɓ|>Ĉs?UUU:::#G4iҡCZ[AA144 gΜQ(ƾFFFǏW`PDQTk/;,,,??D"58׮]YM"P1&5y3eaa :ͻv̙3`e˖[o9s&PSSUPթ[* Ba\qqqpTllldɒtpK&(Ūm/PEfǗ \r++33sҥd0#Fb[[ہmWjjjJ$,ZۣFD'NPƹsV(((Pfq\\ŋ[kT*U_tOnmm?jԨ"UãMilll5Y䓌˅BرcnUOyN :or˅Be^^^75Qsuu---EQ>>>Aq8___ի p5k֊+޽T033h=4Mr+Gj i[*;;[|SSSzW#DyO˪8^tiFFFow?: t 3***]TlĊ+^}>@Ej+88XT푵UYY ?c!:v*++[500iTHΞ= oߞ1cɓwܩ-!I.,illl{opccW---UYpº EQJi0HDDDCmڵmOAAA7v:斕ՍWm" """ڮ@tǏ'jγx###Kv09;;T(@BB˜1c_?!!GCυI&-YdCUѝ;whn4hzŰa222ammgU 0̒T/YXX0"(**ƪlll(R݂0֗***TAwuJ k@KKK\\ܞ={ %%%VVVe3\+biiIŪ*ccㆆUc9NVOڜhPP \.sC&:;;'&&݄:uJu}^YYŋ| & >/h9w_qZSufsssvqq422rrr)(( +***TiiiuuuSLQǷ@LL Ù6mѣ[⸺foo?ydss((ƍ...ǏR?666g>uttfΜikk>y䂂kKKK8ȑ#U-2eJYYj0$22R[[{Μ9T}d޶OԜghH$Ex(D"Q 41|͛urjB,u5= 333==Ǐ?u5kTTqoܸQ,]fGGGGsssW^z;vTVVnذErڵk:Ź~pL$$$DuJ#GƎ;sL\5&Ǐ ;wncc TWwhkk[XXt(--'N^#˽ry]]]JJ+ڎD"S?#((HUÔFu5==]T]T*H U#GL4 \ܹs&L={X,uVqq7Y ,x$Iò=KWWdvzԳS݇-HBCCU_,,,x<ѣG ԟ_QQQ̸vjee֭[;K=#_____;v^BGrŋ+ ްww_ޟwfqg{|8JЫ2WO'낅OYsz/s>ٷ',oдl-*Wן>6xǺ9t|SFs]oB0_ 2g&=YAM)V`gY")s6?od䖐Z9߮oh.;{OHK:ه?h07 eO! eF0bT'ݻ\79B}K]Z mjp}秧ٯ-8-g z)t8nmq-#t|Gz;ܺ$seG|pcԏ[&V/0"inH:g#'9( lm v:6$\;´NzêJRn;;jU|m׷|kSU-~swE<{w 0 \ӛ?Ǜ+1;4 `ʑTfwxr[o6̗V~ ) ]K'Ufʤ#MVOسOB[OڬFUUTνj. d薍 zL#}ۿw@~ʿ! E}YIm63<:_ȈUZnd&4eZAK+EbQi̍;bCc4MQJ%E@*,C#!&|w}m]ס?u5.h p1"7M\\9v)4#>z[%ږҀOj!Kp/_=2pϻ:QSGPͤ][SO-|׏Gʴc%=PoI&{"vGo݌,c Fɡ_2&b2-s~%]5e~]X2hҡ*$ưi^#jmȻ<%ý5dʝ,M޼*dRRy̫v7bk+O칶)%jQJP ;tk2X@ Iic'&m%yW~}ΚUoOH.e ǺUZ;g%4M+ސWޙfkaтUpp>~6g[*߷ᯂto7hEtu) @LIshp'>N ;qaptuF6,gWA4AX,5ՍClBS+#A`X$Kɯ73I\9f Os==}%m0B\ːVTI9#z_eiWR Y{E9|MuQ~oh9߮js^KI]syeqT~X7Zȿ|6S\n+o&חE4,#2V<&!!>|0;*K-o ntt _,JXo3 '^s$yg~X!7m=*>AޫL_We|&Tg:4n^[TKTwtۺ;g[/};\!ξqHv{\|R/x6BeR/!^0BH`.#z\F!Bs! ͳBشBN% FǗRijjR>7]|G${`vKҗe9/zNR\A)}_Vxޛ|!uc z\F}ݽss!λr F%TPsal]]%XW5vRUuw-K:Yc=TX\.Gm.3\ƌj#)]mܭ.Q[gI-+^0Ur̍)Sn`@?y><`kWSEWGϬ2&-4TSyL9,hʋ8}9l:B 8隳Dq#ҲKgK&tnm02>Fl.Orjt0o6AK/ީ,ξ'PIhzYjn9x*nq5i}I9tZVMIasy YL>5̕&\ ݾr9HB躎2–OPҬ Clcqc-uTCvEzhen3fY^+(=Z0mG?g{KiE6)-ul|B=s9YUjH1 9r ._U];uĤօbckj-c4NG QMTu9,anZlBZ^Oiq53AZpDB+, d^E9b fި*C\+%Uc/=nrFŹM],v:Т${e`|zYXAW]%] 2}c?qĨ7kW00=4=Sv;CgUw<>#>iΌ{Csrq6da9BX_*RhbRSP:nJU:7E7E|=ANe:󦌭L03+ B D)VΙ76L:1 IDATbgJ*) i䧵<]>o GV]_ Ј?PGط?n Y!2i.T&GG(O<Ӎj Yf]*HR3wåXHsԤ MM:l0, TmjȥN֠*m7_W2#R)fۂ -r(H-UT9oF`QzvFzqm3pk )jXn| hqQLvpKٔg?]n) s]C .xByt[]y2+#5Vpogѐ)1amn8u 5KPx6ҴS9"% 剉e>. he1l Fz&9zҤ.Z#)ZUaKgp&fiqy@ِsg Z+4V݌r !OZ S4su]Eѐ^fMOn~T;̽CKǍ,j  r8M\[pbSkԐQB®rU.;9:ڳV>`n_00x@Z"$hj̹rTv"r NA Gz.Ղڹˇ3=w444-hTiӐ%M2?o=W9eMuԌGԊlD@~E@,MN*,o7>LaC# cKy_~4M--A3A~_;r4i}wǶtqԅozakfʝGqZT}ZC/zۺ([}ݷJ$^2Y7>&g(@Uƅ]z/ҰMIvesuQʡcGd5{JuB#6WLyɞ2=ee. 5tt Q$  ?zeiraSy7JKm58w5ɷ MFL6Wmeh)0N$8ƞR`jj@l⏝/˅DB Cp[͕?!ܵ xhP4iO$$XNѡث&4ElhL@2RG dk:<cM&LVS&z `prBYVk<8VAu4Ķ9=/ĸ-N xCmpCMWd]JU9M\n-WҍYwhk6n/7fٻS 4G%ݘ|or䨉X&ʉgLtD"9bB= ·@Rpl Ԙ\ߙ:\YQ-3j{P6\?u>CY4Z9hX=T? 2e+(TW^s$e)!gSĄY Ieseʖӑcz'E+R ͓pl\lD *LKr2tj\ΣFvVS#'D$b2,u(q+ehίJ%i!gゖʤ; a{Ȥ2}#uJ^[:dmIdqrٞʺa)1q%Dtv(} RTD3L\ $-o*˽XMtzT7M%- ̬ &%9=E/[[FIyR +|< Cww]m{38pmҎ3GztqT~~{RRqY3Xm׾!BjߏL|m*^=Lv豑]s_V۲ޜ'/#E/$%%wzn`.#z$ʞ?qQJߗ<_EHtrޯ7]R>7clGB~|N#Ћ !^z R/!^0BH`.#z\F!Vx3l6I7&yC;y XlrrO M/Ss!3*0/K~vtxwԒ\&M6䋘jڒ[@cAL̷J$±1*J~Ō:ٿܭ|)L7KLV+Rpc (}7Ehz~gg>rѹ" gb$eOa;H}ӥ oK(GnV\ht_Ɩ&h6>zZP/{By$G+.;n఍/7ӊgcVП Jz[gn?@ ` u(neϴ9|!bXѺqz;6EщogC?Zf _;6&ԍgW^kZy@3NcF?_I7Z}N?T/ %7^N048}{2.?Zzȳ2* W=8ٽ>|m@7ҦL*-7X.ؔ 2PǯfP+2 NOi41v<~$CUg̃dQX~y&~}.oB*Uli˦ 3#EQi вx{;3 7ύۀhiUk$Toι${? *v,#6$5<-oFMB=G6+H1]=RQ'.H-CK{]j9T7b8+=%趶Q<05 UV4KN^yKgRCmHCo#n!$ztU QliTw'oaiP[YU40}_5r/wn,fN5ڋ7?WUmE'tkHe۟ lf&l16qI!ں"z4|.pp`Ҕ5hzQ!^B!^0BH`.#z\F!Bs! 2BeR/!^zE:|'.3ⱁnj-&Z|5фAWl( Ez}jgglI{#iĀcx3F (:Qv>3 3]tBw_RGpϕmq~5?sfz,̧8.'^Tksj Y$I$g=XfBxM]QPozq(5HT֟ŢxQV+>d{lb59lR4$UX9~5=bu-YEd5hp5u )+>#s54BE,Lptg rQ~8VFdU>i&]z/YD̡8L"PtuU<W!> 7U"R%xVjN+d:&$475hh)S ϒag_'GpYMyJ @UfSF^, XcUΉK;qfryLV]coamXM6?a(`v0'LEEe*?$(d>+^u[Z ӛE]?M bEgw렗|)Ǘ3km!|+1Pzثt]yx1B/N!))E!\.hRKȸ2hvM̏=q`Btl6ťuIjjjxxD;(?'iH\huho`{*LA @֬$mLmuh yT@HU5NNZCHS*ֺZ+veҕ{{w]\ BΟhNMMUNg4{X%VҪI(vDEx0S t}1)T5Ⴙ?>xkO[l?SO.r#jH =9 bu.B'WވֶYBp:d%ui=`O~ c; /y~m}ۇU@]w|' ѻwgƺ}7uZ0k='&? nL[fO84 v B~c4;2y)O&Mՙ_~^+ u %h_OVX_bװG2eKuc67z-½u 5' mL$׏gwlT|_ &ԡ}ؐ|!bXpo.^2AZtWˈq_/_`]-͍ѝ's'+/}y K3O1 UUy0=)(2)ABB+y4aU;c0'dlJPF9B|B偹Bs! 2BeR/!^0BH`.#z\F!|2!w5?hk_i9kb\olr\s[WRmQȰ;rB4hUX®"Wշגorۜm_*66h:fK`UɀFZߜ3`yo<6Җ@;eG׼t1_U v D]ΧHk=5~ >ҟD?772uԬ- >vϣŮQf튣5,nIG?7E'և$NO'׿kMD|014X6:Zfic<*ϻ Bps麜:-H\IEIC}B}]ԛ>,ӝ>|f)eoL(+h`g7+dk76HY?HtҕhBɐ)Yͮk~?<~bǿY >o%e}>'_ꨴMВܨ)Tɤiu=2IhѭĵrA7hWy+ZnztUsn/4`3{[+j xҳ6^,}8j~+7! +s4r vM tJﮠ2B57 7""܀t}bӻIuߏaiJF$bw#.?]qUbe\]zYėU{Ϸa  f+N_9񅪚(DwZh&#.ޚk)eFHBNruU4 DcrCߊ+tGN5>}9u~GhRùn< % I8a~ީ]|x1 `o,LH佢ioMS{oY :]_]f0d &*@"xzK R j< =O>~Št 68+,Pu :&<S#"Vh?ꨵ:]݅8:qWH_ZӢէ-M࣯@zZi}9X|]ͥߌJ[n}udYُ2n@4՜(L 靿n![KwYSL%]MˡLˋtr9[F\,gDo-(F4e^g9}?2щ.6O)Av5 XxgLԠh%r1}ͤ=kzk=8Sɂ{ ׷,'aH⟸~kٮ|F̲2׫=<&޴GLE^ҜCjaD'naZZZ;$O_[#7iqfN#-9,քc,!}_fDzdʩ53Ra!cH2HHn"I$I#f6Ō:ٿܭ#PSSS;.8 !06+osqSTהz\= `mwGGw>@ ">~W!^ =3tJsWSOuZ\Yqm~c̮/m~YBc9 ]~LHj{t^:jO5Uq!_IudMk652 w9zhETRZ\-B/, um2ƃNF{ӱg),~ Oc2>嘳 :^;}. 6U4GD-X@XkVf5ڭ/;ۻsprDADBB]K.զkvͶ4~ڬ]rȲ6R*!wDn3\ΜjE|9syf^_'!"ŴcѶǗV4^G{S6;y׷LJmaeK:/i^&"{r36h|Hp j(.Zwg1d17X-FRhhc/DDTUJtJKPȍ9[dbMWWҪ" jZ_l' ,ts1""F@DY6oŲ*&nk$1"FD1)$(YN*׼`U}5vq9"cJKscܜ^Ike"1()_uc#xHE\\p^cgD^?(1b6]k//W ,xy|%v7[c77]޳goOOX')*cĈH3rVna(XӢӷ|658(mEoPW> 7ꌙ^\~Ĵof-K/i|=-&mԬq}uCuz š1z2_|A. 2_|A. 2_|q}nvB/ĄE{3cT>w*4ɩt== d-:^rlGLD$8(e!(5#=&ƬWiT}Lܼ&"{DWD;RyH=BD$y~.uGގub5I. W<$j}AAE#x^_nj⑯e wI7}Ups[S'qA! >f-%}ꞥ=w1""%3Ũ mʯ-FC~2 NC7DdEb_Po!"Ҏ N[7."$T,YϬ5عo>kpG݀\5U|ƙz;,K8*>n ݼ如&KUN7bu&:6TI'DT5*}ƩIPLw95U IQ x9Ic <ܱ{NR&z=>挩0~ec5mQh5[+FDֆU{%/fBCMQ7εI=:2&\fO*YLi&F̚ticfs7Eo-˧pwˏoWlW5?cUߪ[VӦ t6޽&y'LpTv)f.yɪq̴}6Hg?ޟqE8ut7[" ~zsY >_D$)ƂMbrQs7ZR}u28XˈeYpSU(QN$=mr vE!"f݆쮟:fr"˜@71f +(_~SP {S6lqڝHaJ#Ap'8Oί-Ї$p?Z,&OsMl9yGDb0_%tlzOhk<1w؜T742k^nn8Ub1X{s}'g 6NF}`*,Zkܲ;WWyqZYZӳ',ٓ^W1髵Ā u>j)_ o[X6jG*c=5.6cN58QF<ͦ6|W@ |A. 2_|A. 2_|}$ߩy44P}^fƂ~<\UZ,u޺TIuKUQ o 傂>x}w~<@=Sttsm#G 9trff悂.,@/eOp( !!>Qz~֗u_|{'>kl?q"}[>ճ2܏=G{r-hԴܯ7`g?q.GiS{?bn dB~?جw7ho]~RCom02"{ 6KS]Z/oZdؾZ|3)m౬$NZqü{fṫN sˋ *+'""bs?_/jrshW 6Mu Qo;|_ NX朴I[Nڲ %yq/a瑗_ o~wڡSIRQ,IJO'۾~]T- n %'TÄO~O='%PCDiǢm/h?lwoNs>,VH-Ly|]宛!(e"-71cC&·ȰHuw6{3\Mysb$+|?vIDDjOUuDG䈽dƜ-2&y@+iU}5T/k򥻜I_ R[׌~"s1""F@DY6oŲ*&nk$1"FD1)$(YN*=:.J[k㲽_4{=ة|QE%px|cqU2诺yrӱsWWn] \6%!H7g4[gm;&|qiSʚ]yA*68|̃ڝY͚]' 0sYp`ĸ!&.µO„tg}K vD`vںw&ॢfzf =<"4࿞=5 2U z~sY~tTMgZQ3Rp?XۇصDwf3ډj q:\aF06~$ v ")U Jqj""cNMUBDRDD.*ޭcNe&Wnb351[}5 :ߠ0ks,@^VJ;VVBaDdm|QWb6.44,~sK$"z#DD$j.ߞ f+`a2HQƯεI=:2&\fO*YLi&F̚tiãi,{r㦽荙Go6~eL,D*V 6Y;;+#u좱wq'|[:/-b.qpVҪmH_͇OV'KHP?=n@ވ/~>N=X׾uБ tsYwڰ槷O<R|{ǽh}ev|ͱi\jN3VZe!:mPKhkw}RgIeb~L۷mCt'XԋSgN~E!"A hۘq|!W8ZZkMD"b,d!&5qq*U1wP1*Qy.Y%1Z '=UՁKD&`gZ"bm@oq.w)r{+ $ٗ?1;ud]$q@D.n76Vb1yj;n*g;*T$z\-c#xHEx^I<ॺ\ rkp{ɯjݛ;9K}赁}uڀ0zSa%MXC1+o~k&FẐtՒז==aɞygO_ն oQcLJx4mܚIJyOU˗U>P;wq@xwbƶ 80w h6xe^ r/e \ r/e \ r/e \ r/e \ r/e \ r/e \ r/e \ r/e?4>u}S\IENDB`litestar-2.16.0/docs/tutorials/dto-tutorial/images/nested_collection_exclude.png000066400000000000000000000527771500564371300302430ustar00rootroot00000000000000PNG  IHDRȑ pHYs+ IDATxg`\IBB!B]X b?|QQ&{ RH{A iJvwf67s{3!O!bE!E!b> ԛⶶ] .볳y<Ӯ B}U\\ԞvEKhjkkE]R)]R)]R)]R)]R)|n=<<<֬Ychhi{XX/TjAD"HzHL&`H$~:? └pHdaa1yF޻};nܺukhhG}ɫBiץ{A=kaavӧOI۷* `ܹmmm SNM2y1ݾ}K[lUAyzz޸qiWG#FX$---뛚JOMIIQe@ii) XtÇ;(;;;,,l޼yòP]|mccm۶]V ӧϚ5Ҳ;wliiÇt<\*t:'YjرcY,V\\֭[kkk=dȐիWTUUN꩒>>>[l_<...Æ nmmMOO( LfPP (((tRߎfhhT*MKKz knnO%33*ԹVWW<Ʊ|>ܹ'N(۞{ =lGGǾM^7wNCU~~~{OЩmڴ)))?䓠-[477}ݢECMᅬnݺ+Wt:… g̘f{jɓ### ǎKĵk`ܸq/^šg޷oML&s޼yE?~\ 3$p՝3gN~~˗&L srrڋ355NOOT0 ''}m}޼yq^P(6zTo [>QAAA3fضm;n/,,:uL&8oooKwiߗd6ltZ##رٳ Jwv , ~D"IHHpuu]lY]|믿o߾۽GyȑR{{Xv횆իW@P̝;677I[[{߾}{uX+HN<)ˋ;Ʈk}}}EEA3gA_z}OwwCjii566FGGgffvppǓH$jii p8'NPXv5\zzzcǎr 8,,cޞb)1899jhhpԘytԨQmmmiii7nh8m4OV!Sqbw dR&|W{}Ed\%N;XXX̚5kӦM]=== ~DT)vo,_uqNd% H ,XfffA*w+**R(湹BPgjjgnn^TTLl6[*I :٥yyy-Z诿Ru5jTTTTEEԩS[ZZJKK555\R^^r.^ܬ P==ŋ?~1@yE hjj9rĉh8nnnEEEӧrѣG/͟??)))**JWW7$$>rDDDaa!477?Rs'v׮]{M$\1Q@@JzxxDGG?aY]Ћ8d˖-^+**:}tCN\.ohhPn[w555Avݞ*>FFF/ׯuuuh؀(P(:>L(@ |2vmmm\nFFFxYtiPP ƈ#bccfРAʣ{@$ݹsQ;---s̱.((PPTT< r \ͅ kP(oS MfeeUXX8jԨeD__ã^GjNO'v>tGLdj[nFFFyyyO^P z,X7;n3f̶mzo7W\^fffeeEAAA/5Qsuu-//ohhEQAp8??? py̙3/_rme蛚fgg_BMMMc?4MwUĄJU{|$GN\e[ZZ*4BcooSM(R(4M+(:w騤dnnnC zЉC.nnnwe+dEEEQQQwiGjNO7nߗ.]144trr HNN3aĉ/^zuXeuu-Ff$''{{{?0lذǻsk``P__iiҤ/X,9r'OPTܸqCCCcNNNVVV RJ666fffԜ<%GD"*zzz} 4Mwйs! CBBz| Qθqƍ׾q͚5GLMM\na7n( ׮]fccc뮣W^z;ꫮO=W*|>_(+oRihhaaaKP(:4f̘3fdW*=zhPPМ9sZZZΟ?|.BCCܼg:( PWWojj:vX7oޔd2!==]ّQMMM$(Cuuazzz{zꚕP(:nT(^^^$I䄇+/BYY١C'NMMM/_VrϚ5K(ŕ>|szB̟?H$}Hoiii1n?Dwa!!!<[LLL/Wzxx{ݾZ]]m۶n?:ߎ;ڟ-#I> KLL|uS211Yh0#Fxyy}ObJ}0:{y ݸq#999$$$ JJJS_Qݾ}{O/"77ܚ7ďDNstuu8RRRL}0ȀP?}g.B*B*B*..|n .,,,D"&/zdhqtt,..ng.ttt2AR)]R)]R)}ukB*@EEiW!2 JӮB@B*B*B*B*B* 9\i!&W 7RngnUp-'3,XiO~˟߿߯;|4ȖQH_ޚklazov.kl{ɆqzA4T1|fE {v9͙7/NjXjT7:2tǧW4iyh?' g~z}~+tݍ,2T13FӳWnzR[մ]GZ'`dGKm /uV0 Z2) 6l(ظRpg U @W_R`ؽug߻Ii%Baz+玼%~UL +u"_iyDKaRu+lHgCFm_9#@>š.zDCiY^y9@hS|9Zn>͖3E^4Fn OX׋Lw~#=JKjkMt߳Fkң{}Է[&WT3qEm4MGx[kb-h*ұQ'ж״'ZgvӗU6T~=QhT]'ۦ}-\䋳Ul+wkLD% xj{͟|-K@h.]1/,=KMmkO[⽯Ͷ^uFWb"g|UK?a@RZԞz$:Zd]ua7ռTݥO e$>p_{~AarycEY}>'2PА՘Qih&l+*y٪2# >AjJ7*[@PP4 tsUIm0FPw{2]o?KƖ6ebHvn2u|bvUmyvRN-@bghϧ5jz >@tທnZ/G~]_H#.rԷЧ=^Eē>WݎJbS[j3R&#AFZs$.fwΞapW+wrg\z>3V!hy\6wO!DVaҙlR`TJ=)6'ԇZښL> Gti2!T':t{,vC~؎ups  ܩAz=B6[ڊ/oԒC=Yb2}e-I73ΜhF$-i\P_'/^ |B=`BmljV<@Qr71l׈zk^. po92KGӧ6,oY @S `29^ywuO軶)DUUʁbB"魓E޻VGL46Qz$;cHEҢ+{7\;>{ͪ7}t(C|5.Nߙazx>/53sT)^ݷvξ3>Ro_]tg=~Sq?p2$ Ow-?2r_Ym%_=͝׻>/F|2Iy3_ϙwcR.lѬ7#h*ܑwX$i isMQfܾbKk?,Pm4(r/zog Nopn&)Y֯KU^'el}U@Eꏔ.Mʼnl^dSYڂT6u<9<`!f ImPiBH!R!R!R!R!R!R!R!RL77ϧ]zFF&O!~N@][[`tcP( u-ZRg9ы?i);)8TY ˅>wUuXlIokME3׃J~gBNCƢ$,rFVv.Nݷc?|FBAP i)q&0zzdfa|f_ B%8;^à%wb/ߪ\6}iODx^j^9zSG-롤StYL1#Vc] k,H|)DDh<†OP;Fcx[hSꡞ~J<"rX/ %(lñ_J q0Ț"%q$UugL3 Ɉ8DVw,H=JX`j._cYv3Wg1Yu@1 ǒb1(X*is4uѩJ8_'ix9k,kZ^0#&mʢWLSS@7ܓJC9CL3# `+}46-BigF݄AsG=oPRr;}ɞȕ5kW?I+GO'+&溕 Q"uť7tWVCڸ#2Z2H??]q9}c?>̬+ Pi?]r=s䞾s/&.djIzK IDATAjԽg3!4ݐxka CyjIG(MRNo94M{^gv"+K9Z5YܹXu 5.9ӡrg m33 031nlj:0'Paك^=WNn~dv?6EQyjTA؁F zT՝ӃJrJ(62y]eMO-,Rq&ɯz~=K#嶥O!rcŢ|^%Pch_ۚ7ݹ!2aaf0U_& uzJp_6y *SR*|]9e@4@KnMևY98{k5, me@P{u[VE#YA^fRhhES۾m|لqmq +EG XDp,:7;nVZ[2"O 2H/*{\7:pL.H.9C4mLom.QjbeRV.ihn}ۇ_X7^{ECKO7@c_{PAd|<44/ ՒwbvY Z\r+/MAKe%  ɽ۹\yӱ-\Z0uO 9l5pzPnO5qn mN8V"_29I4EK^}4M܋HNrsJ2+jKүW3E?'K_UPv5Ԗ iĔIRJץ%mژX\$&j\v{Geb]?TlAt)0􆄺]럛]M2 qzCxj0tOha-JZ\H&SVSެ젯Fl5VVo$`61Voml \uuL&We U#`kiJ휤2%I]m!=Iۨ!VeG\LiJp@Z[ܢK2̯12hMtVgl𲷧T[#GtKڙȑ&΢uE\9}k'mUiai`,xs DgHnɸ7C3+JpQ•!ډs]ʢɂ9(hNU>Ay~_@JΤ 3g %V_ʗ*7N3v[,Zޚ~fx cbݜsniQ^vEfJBm{h5vUgjH220@MMi,vlY*7z=p*"O  !RǮBw?JQ}Y*7٪-B[j ueTYoNH+z!Rg8B=ӰB*O2 Ja"Ja"Ja"Ja"Ja%ފsL5}ot06q>A0A~^8BH0v |x7棦5Bd2vnj# 5*2[[)'xI fD:ev»ÅMjr͛?- t|7]ʅshJ]͠Kb bIbPQe?NNj` F}ztS̭[;ohgg5\jUr` #MʢkW@p=#05[6Pn;\@*(yKL]4^0O<9+K*ҜvrdGIt DA6~b nHty8gnˬ?~mݪF)[sSHȘ:~ڧZs"\lFj/ ?͒-F[%gk9loq3ݽ6,oq40|)tk`[5.P 8\BYk_X7%_F=']EVM~+-.g{q]TIQ\X4ЭRyN,ek y h KQj„d)ȓڊzhŀhLZ}&YM#ËΝMi|hӮBhx%_NY?Y3Ï%/dq./?Ii Zܒ8Tp>V爯Ե14uš[zlO`(5<:2dr6 %C생@ԝ ϸ'mŀ@ZI`G5B %eneA"{.VnJiKWlX\((袜>&!-#N|9:yio>@BCwgt@miyI "9oA{B.֧]KK09$%( tb*EBϮ;3E`4^5f.Buk!J !R!R!R!R!R!R!R!R!RL!ۏ8WabX,B dE!~*7|t(wMݯ6k+4GVmZPTT&RGCv lt4sȘ[I ,IgsFq2.[9 }?߄DV.g133ˎ]RK2Ŭd͙qˆ^F geeG'oњq|Ǔ.Keș:wh4$)ab ؘ)}3]l]5Z.^<7Yu7-z B؍b9...[222"#Zj6DMC֥G };jiȖO.H5(GH%%蚺6ǹ_{݌ͼRGH2jֺ/B=5:ruݳѕ/,'!I톅2y322vZj4M+nSA)irJNwP+AXo{y3oC`Y{-iSlg0hư/drv5.z6vK-,,,##̅R+'+4Tv @r}D'+[MC3d &PiJKc.mOO t`ihJs.^Xo}#\|hiI .S*uUo}X!  @ee)q4yn-8w ь].6j,l\<W}hEJnmUPb4.v[Q#^R\]!T E!E!E!E!E!E!E!E!E!E!E! 4\la nleSoE9NL}nՙ87l܌mKt͎FحN1n]w4֌`niО"ƌ;Ó->?>@UQԟ}c?Q'NjE= X=xa1@k ]r Kha>c| iyw'*rߖ_K[jjVc7!Tv~o'm9<͇@hO1݇;#c\~B??l*`hM;;e,3t02ZT/+hqM0IP1Hi" f۩273NJc!gر^v_ƍiDy @,˱ɿz 3lf uC|g P_RqoWl.ۦߏO&Uh`6 m>Bj>AS~^)}y(MZL{|T|c#wNP ϋ:ڪ;^u-A7_}^σSەUdZr {.KX:XN@Q_LxfZj⹸zb\CO̜ӭS:T~ָx1İ܍VyUUbѫ-[v Q>1#8.yz;:W[5Lb| ,xcW+~{+R 2&|~/ K{v݁FLS;/ΚO˥m_\=z:CC[ݹ.gAfða1dҐO-S>Im\=;u?7^fl;G;kuCygA YRw4^TwE ~z/O4&"@#e>Z3*d(wXKmkGkK|X@P*|X3h.Xd!u3M+N% "W~ Ueefm)٥gm:OH<,sѴi7N< ~gv뺛ewqF$M(-cY"2ۗ&#>;$dߦOYUJ[%-m8[$Bԙ_jMac&4,hѷ輓۾褊½s?W?X|*hBɐ*zXY̪mQU4R#__8(gbV#uw~z㬅W'Ge+Xi 5_6fSKH݉ȋ+_Ct3*4rfɊH7>(kѸ4 ^>[6{̏3BL%?| jk]i_K"tk$4 %6=.(NU&ֵ@7kj W@r:l~v IQya+w_X%m!yaUr˯Ʋ]UQZNˋŚ~XK'e@+b\Ov">A wNg0@2Ԕ=Ƽ1k`P&͕Z\%9 ڮ_!Bi̷Ͱ, $)`4 D2S[k7no1AE}Ŵ゠S;_2U7+8t=u ̿WH^ގ-4M)@yA)9#dZjtcmnh5Ps뜚6lk/^HKD_WA~6K>4z zZjtvBK#&&IӓD%kF+ m5n"ҕ'sBVuBkԀ˚*ځz|:BSg0H` Pqotۂ_WH$ϯh8A[nֺgW(U~q|1r[oӕgr 괴f em=,vt9__7๘ͨVTٗHV絘w6}Iв]W>LU}Hwv$Q@^M~i.IXuՀ_95n~;#t}x?6֞}~f磗ol8r|]We͕=aA7`GWJZX21/BZ][g['vB3 Wn;Զ#EYMoCG8_;vBh`x!z!4p`"Ja"Ja"Ja"Ja"Ja"Ja"Ja"JyM~IDATyI'g͕\lB>?;˞M.W@=oTkT]>|r2!b~7|t(wFK &3~\GʑRX6\ÒrbjI iZJG"*QF[YO߄wU m^o;-dY~f|rDB/?Iށ#}^B=c7##ӫ.;v] H^sqY#7n~.:vcö^+1Ŭd͙qˆ^Frh 'ӯ[(ekN>8|jy\f#ӗ_S3ibZyH#]nddqqqiߒh7cF0JhyvS"g6DMC֥^om4pЏ&L6V5" ҨT!W efdd(}84uw!3J@d(Yim4j6hDž5( z[v] %"-oҚ|q"HS}͞[x+XV/0,!УxZjv]c9maacQVuE4Eg[/4yveɾ9)YV}cDL(D=vXei_E]jȺ!4E(*K)/AZy Bh%BH!R!R!R!R!R!R!R!R!R!R!R ,_[iޯ1{h|zTeXrh5aa2^z.Mo!^#F'$]7tɜV07f8=9B7݌Fhp[*CsQkD˄}ҦB7݇]{ d6oU7 OJZc'5Av+)i M݈+-/5؍m:Bӗ]yB8C3[QBP.aa1+wOÆzxQe %}w cQqN-5 RC!O!Ja"Ja"Ja"Ja"Ja"Ja"Ja"Ja"Ja"Ja"Ja%ފsL5Ԙ87l܌mKt\nLlA{')3Ouf/  t1{~߼e}sqf7/aa龲 l k7~'OygV=K|J#]F1y^Ew+s+ۣO2ia݌H,NxwIWCWM2Ҿyse"|ܘ@PU&Z9oFb b9a` b GYU jY4麶n\/GH{,nǠA,iV-MYh]zyAݷE*孒6Ld`.y"i'G{DWݰK$4l'I ֈMۼajs*֭j5'>5܁d fLH]畕rFSDyG%q}o(/¼[OyC[SziԌFKˮT ޽t|rOdFleч_ݢB-vZlc?݇>JK%_hI$̙̌mW]ߜ+Μ|xx=@u_-q[M>DD#j&xx95ĉ[9kb#C}5H)'d6[fif#pfdT&Zc؆Wee'R}vIQZχrb)k;M=q['D9Wx랒DLYx`y~YK, ]7mWdzWu9Y:t> zVU.IIᡚ;'7=K!"?&3Cə]_L ""OL/{{ù"n V O3"([yڿ\^ev ~98u8oE’0ղ̩-%ND)1%Ynv۟۽0686wUZ]7z3~tR1(?XnFNLD?K-'֦gjپkGm|n,J7둟j6=߿xɈc\yP dp.P.P.P.P.P.P.P.P.P.Pv+UcS y#87Ǽ$z'G;v%kօC}{Ǵz'*3ueoE@=ea3p ӵ  2h]X>B"K#j޽ 67}gs#Dn=_m~|[:Z2Hbť%uI1Rv:7ugkv(oPH̎Vm4%6/cQ DDKY {Sdq~ZdDolJtSv3i ^n~7auZpa2C2GTH9$]~ɉ V7 ;~ӈqDՑl!Lk[;ے?/YC{$.34V|RE[7hDx؏_N䬙u!|w8> C5}Ƽt֒f71cx|DDJYMҩ_{Rh#",KN%"3ai,]\u6hXZFujZ;Uv¿6Yp"rZT>9.Eįl\읺~Z",rRI1r2ɔB'N7tг5왝W{LFlLhU0@1Kנ6 l7#^oaK~~~FrVk]d$*fv秧3.%1K&D1uHD4}_Aen|vaߎb^o#W&;o߄w 쫉[mӟ6G c})yY&mEu_-q[M>DD#j&xx95ĉ[9kb#C}5H)'d6[fif#pfdT&Zc؆WeeeOgʡOy&쒢Sv*zYr1"Nĉs=%b=J{Zbi4Ti"+pvO׻n""[WŲ毨lBtT> zVU.IIᡚ;'7=K!"?&3Cֹ+zDž/g^Jw}1%ȋt>1| fn1X>'/ >AψBZf2}G,<g7Wvo7C*sYWV$,H s_-ǜRD1S\f ncsW%휭eu~á7M)/)fD$|.q_rbmښ}֯vV΢8H ~yvH%'3.Tki'<          G4R:I֚էKpG0)v 0S.hpjB"K#j޽ 67}g?w/8WHy1"uV.5QZߠ ڒiJ^m._x"Xɮ~#.2/‰$ؔ+\\zdMȘO&vJڂWk]ʳ _8S"I(vu)''*ɭybtJ!F«ve8d\ 5j**oY4rƼt֒f71cx|DDJYMҩ_{Rh#",KN%"3ai,6^1"T"|V,PlW)?>(js:% '"gͮEsQzRNJީG?%b-/e39:(wIv‹asT [xZjZ( 1㴤Xrz{[8NT~⁳]OKjd@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@(d@f5 -6IENDB`litestar-2.16.0/docs/tutorials/dto-tutorial/images/nested_exclude.png000066400000000000000000000404401500564371300260100ustar00rootroot00000000000000PNG  IHDR( pHYs+ IDATxe`YN"ww#!@`ܽ8TP) --ER$@$x%w9}^!%ylofvB ԭB=볳y<~AwZqq1 k0p=c0pBHC0pBHC0pBHC0pBHC0pBHCp.z*y{{YĤ~_jP7p %D"<&`0d2YㅅX[[@qqqJJJddD"4iO?GEoߺuomqӦM>fx{{'''K(***++toQvvvDDܹsΝۋe>|͛7?!fff7o.//_vz AӦM9sMccիWn o\. pNjժ1cưXׯoڴsу Zz}UUծ]?]%q}߾}]>tPPҒCQ0̐wwwQPPp]]p[[[\v%!`ccbhh(bcc;{sww̤i ;ꚚG8ϙ3ѣmisުʃvqqxs鐶m/9;;J)O`XX.˝6mҥKutt۶ؼKtrrZb?nֆ :? ٸqH$zwEř?6O>innxbS/\pl68::N4):::??d̘1A\rƎr9T>k֬ݻw4d2ΝKQԑ#GѣIٳg_pqR4''8 P _0 WW'(ۿieo}ܹ\h5zXo ooM6=Q!!!ӧO߼yio/,,2eBׯ_}vnBXn]Ӛ7n˖-N\u֠ uY`X,裏d2YBBDze:^yݻw7bĈ:99@uuݕ+Wttt<<<.]*jΜ9BpݕUT/Ɏ;T*G}}}EEA3f000A_tmO//!C566dffP( 566x2laaaG$=z ͥRiZZڕ+WJ%3F(r\JU\\Ѿ7b\]]utt\nKKKjjjll<`aa1rHִk׮_:u'NO[Ё$Zf{g;wƢsu&v\PqqqqgΜaÆο>>>A\zUmbb\. m7ccc/_:k_~}A$ygrgCCquu5233S磥%A݊T*UnnX,V-XXXYYYERAAApp0͖@[BBZZZ-믿oFy劊 '')S477:::^x666ڞ;wN$뫣pGÇ766V_VVV555#F0a޽{.gQQGclll``p Rijj`bbr!KOJJ|AXXEQm⨨BD՜?,x]v- a7Drʇ=yAAALkeYYYusBσ.Yf-[_~ĉ122:JAna``!pdBB:_.^8++Ȩ.::m"EQbXQ `&@f |:p\nFFF`YtiHHȞ={ ~z@"ܾ}ȑ#E"ÇWyvvvg`aaa\.We.kooٶT*[S9D*))122n477ӥ,]4&&瑑^yh͛7{\SSӼ/A{=],Xꫯ{=z=7譛N\.033ں_zyxx744t~\ 9NPPP@@U]8ܸqcƌ˗/OIIu:-,,ۮpaa!MmFM[[[}~777;SXXhnnpn1tye9]zUҥKz4ufeezSIIuw?5 zN,_|޼yoS^xxJj߇j տ C(vTWW}kddDC_=ݢG}[uߪikk4A]M(RT4MG$._~Ի@M{%%%۶m4hP@@[TTÿԧm`}vw >DP]|4MwPң|ڵkvZtiy0cbb[i ɡ=+š„ /^zi !!!qqq8͛4M 8fwINNk{0tG7kll\__iܙLfYYMꗬ FII |SSSKvvvE?#0 AouFI.׵T}A@YYM[쪿n?KBܼAgԘ655$pڎztRH\.kCʕ+b]|>UWWהpܣGUTT={W^7nܰa>7gώ0`` ZjmSo͚5cbb_TTP(222=<<q &MRE'%%IRqÙ2eͨQ,--xxxwx+8qDKKX(իcǎuttnfpSSS;;ۆѺӧO8qbQQQ~~~ϗC*v؍1BI&UTT%]3k,WWW[[{rӥGC؂D"dа$zxΩ>gΜ]vᴰ'X, n1,,Lzcǎ;vl5k֨XXXpܞo_^,]f}םG{W^z-[TWW嗝=K.|>_,o3gryxxKR?zӧ+K.=tPHHٳϜ9󠣣ceeEikk755>|mRč7 BhhhHOOWw;tqqҒH$P]]k֨xxxdeeTU*/I999PVV &@SSӅ ԇ:ujܸq3gׯ_/--}txe}}}%ICKOOdvǧhD"T粲 xxfP?bcc{X>666͛7w_l6c$w}7"""11fnnhѢ={MÇX1xh~L nڵkaaaAAAVVVPRR~xMߕ֭[/^ߵxyzz䴥ӳ yZMxƎ${<;B-SSS'N)))OwH>BO#\!4!4!4=\z>B \􄲶H$YJ__ť=0pѓK__gg ) `"`"mJ#S'@n* Po!B : s{!!!!!!!!!!%=0p d# Bϻ \K¡+wFgW]6ρ$mgR j ou0Sv_we=vQ={vu{KCooiwΏ ̧Bϲ~.'߷?ڸIMAJ3lSK|4kUJsdVGWb7?ܻ#&Z8Xb8۴]iJb>:x'H,NwA#竕sF\Q4Uc-ֲ#m(c{*w; XF<0fObT{0 |f,3̈́QzN&'7@yxyq!PZEVw{7;kMWktiQo6,ۦ \ywtS\d^` ]zpRkn0*PPz뭫#8Ѩ`WTJݔzVˌETYEB{#]{;#qqfnWuC%WN^Z@^[+pOYwڔ{բ|vi-~c7.ӷq7wO\>Ł٩L KW}E)n%n'>Y/vw_yivTb"ԛz(U >f|oR+ I tȺ:nTMU !4нTފqD^MZw}~\%E\Wcbjʨqq`ZNŠJmg'rpann_'hIMiH,*q@(iEU%rX&o0;wڹs/`~NE+m>s>]݄d6Qw줜ZcW7Rgbmħ5jzX9@ts'mYo8\@Pܖ]_H#.|߭crU-^؊DU8#.u5IYq̄[Ġ=cٜ<֒\\} Kg u0RʁQG+_G@ un 5 Gtj2!&:ty,Cysu(Ȯ"%O;z=hں:V[Z/l|Xc/ݴxqI7*̘`Z$G17I2+cz7I:2B/uB;hBۼni>*LW9d:wxykOkˤ_.o\ @S*`29:bȊ^Tڪ*0!mTsSU/ܽ^L776QƆ$۵cHEҒ;]95kͪ&罿?-=`woN7OqK/>#%pނQ"%UgK|0G{v,U{;t/6k_?ѼI  ,6T%13fZscza7Oj ]kCO $hdX$*jͽۘ ">5׋'ho`%$&޼(]8R_WPW=ícE8[qG۱tc豇Kf~}25+vPӈjK ㏾7W?<>?ڒ'-K>mk[vFe%'={ HHEȳŋG%'74$%T5ȍ*Df=䢚뻿8W">w?q)5gkA{ ?nķN¯~ylmIJViREg5^-eۿ#%Sii .Yd!KR[p؆'g[-}-R!ͽw6voErh/Bi.Bi.Bi.Bi.Bi.Bi.Bi.BiӧBԼBυ. h  KFR54Ե4i)/Kt?#Gϵ. Ph!5(24Y<\j_]4@)/K<]EO, \ ^^^].BiC."| ]FRN%|QbOsan[WYysftRMmp,K=uoAi\ZՓrWw3ÄG([k/czA#l*ZWImaF nG&+cn"`ъ괋9 4\,,J\_x+Z1q*VxX?ճ)"\ݫ%n#XUvDJ U!cfN=ש&JDG&m1b$~]` Zh\G 1MTKl=g[U"fNZZ^.7LGGgt[U8Uc?VHCg"\04|ǐ`yWdL=A /]au.&RjkBkʜ\>~>l*oQpܦ⑷p1#܌xLYE¹qez Wo2<6NlLpb3IàeGެ/>=XE|g-HdnC}^̩@XV ]Ph),&ݘqBn ~1B-( /+zc' =JϘ~ֺl)7xTFDzxkD𶎩($8֡ É=E,)sK'd̽׭RFsG/ HY'~ӧXY dDju iQYuoA]ںǥm_S[wVk{ #Ǜڤ WXvC<ل*FlV#cag_X J!+@.HcwCu 7N7asNRx ZZԝg&xN^EjbBbjQhQڱ40 _="35"HK;ڂGh05r/'>~ #S{׳FM_cKh̙c}I" YGD0&̚6ȼ|zY\02,,Wt[i`f$-pO:J^?{ ,acD|}aкNs&nukemKU ,pW=?* ff{\b{Y'ʪeM8hfJSn/VtC}תJD) IDAT. vk%Jnt;^Ų;rh0!hG@ǫZv}nVr*fko'-KssKs 47klj<:sy]A /^N:dv?2IE󴴅TAF nzTaBJrJ[)[_\o'j܈ v8=z~e38|{f lǢ|^%Pc n_ ѱ4?`B$'Ic;kY<ʔ w+Nr 49jV9J ڬ!R}{}^]UHOxA9<D@ jʽqY6z­J a`mǟѠ#@.i!;.ӑک?=C ⫑ŋ焋wN׍,i dq8'RQ ^".uqv޶}G7.I<ߐƖ|(944/ ՜w\Y.CAfUꃞg@ o^,ʫ$EIP.tT噹pg*]Sf6#uUj<^At{Q LjEҺ%7eR5u|os]}S! #;wa4A{,N]⓰@s;N*;m"Ѓٯak jHmS#Y`e%^/%ucgpl:R(In/J_&Cin8TuB4PA]Z[~zF9Y F;\PQ/n+*Sj'xP5Gϴ2-,"IPHJ軿ͪL;ܮVN M   ͆Tmlī+rMSR @0<<;oGVYq tmj94IxYY|v,&H,p 2)uOSCâ`I̞J5)jd\>qRTI׳kiIf ᘰ+&3)fݱޭ'r7ʺʯ<ؾZiZÈ>+Q7~A3D\jΡHC-RRB3L$hȿRK틗A=,JR_y;ZNp?YqjJ#T=xC_TXxϽ-^0W'ר{nr0Lf~kSyQ8iTaaWjj*}ߧqӦJR+K~E35DH3O\KFY+kR/)HdrGiiiw]eOM-KiBjjjWBET9͗O{s"ԧiPHu)/Kt?ST IG! a>uB)=\BBBBBk+"Ͷ]ttʋN=-r8G(i@g!M}2Vy26BOx.#$` ؛_|WnnosKkT)a?3WMuI*IZIl>d[JeS wz<IEܸTc#BN+ĭY"2tvI 薨 ܾ}KnJV*W>Y(gN7lJ0Q{oUN4HYsvsmH!qsc)_V2ocꤾZROqF|;iʹ53?ϭ:pQL/uKJ&/o{*qMq+l/ƅTK"k L|BWU@>@p\|kaȤ(._GKzQR'eb`qBa|S!`1J͙,eR^h֓*YJзju^7GZlg0*1Ӻ0 ksƪݦ4t OHB+ ģU*e_!$y%v(J- V6ɛ5GY=zr)Z'IA\Yy(F0 AJ'"AТsq~P~OJ"Eԫ7G+JEiw7ݕ7cYK@j[=/|GY'ѦM)5*ybtÀ<JJC?J8} 7oo"E$麢c5\%mvU}yܗ>DSbah0LgZ7t儃\fQj\$x}թ@P2D?k>z\FIIF >-0}~lBf1Zߨ"`!4{!!!!!!!!!!!!!!!!!!$!nf,P/_ve 2 `l)Z >C :<hEc|n#ғZC'T{%6& ֓ 'B~aEP]]b:o+)_(XۧCMˋ^˧-mΉϱkU8ҋu ]'}#ͣO #:f@TR&>, XΡ:8dS+@2z?3\$P FS_oWc"]Bi>K!4!4!4!4!4!4!4!4!49xз] 9?abW}U&B=ɰBk^{U6aL&ݶ{6uple ZA]SQ=HUM:3:Đ!co&ws@&l=xAB\g4]xsU@OaH9 щV :S-!H]nFFov켦Y񌍖%kN~up_7lФczӏx]r9 %S-E$'bl7ylӟnˀmERp1Է‹Z6`MWi6wEUqaBuїl{ۖ˝vbM3#{r_!|U a훵4@SRtOU*ˎh $ .e׾7j3/^?NMNP|lt嶳uA^p#""@o;jM3Up˩_4*ZۏK%z n,3F=W={ʷmW.v00} ?]K 4.m5JkAՖ -4YSfl .D'+[,§j3d@hMu奱EMOL `E9gs[Xo#K޳19'TO+>tj]i&:Q?TȤJBPYY \p FdJ]LY ֏MHZ-7=x$-oUqf_os ^/B=\!4"`"`"`"`"`"`"`"`"`"`"< 4Yjn reݓŞLLf[.::E;UgZ۬39O߼D5o`3r8G}nSFH([ 4dQy]j{/>aO+q??P՟z4E?vjE{ ^ R6Ùdon) jgUbEOsQ 顰eSxCo a xae5sOo'+FKR-m' v 8=MBw-m? z[V00Y0[0=|Qf#JFvxҮ #g0s3wjBhmgމ Bi.Bi.Bi.Bi.Bi.Bi.Bi.Bi.Bi0y`Γ񬰇X1 qsX4Vak5fTZd7/ldƏO'SzNԿwfg0?fphQsN}ˀPkݖ|hǧָ.Wq}^l͡/d(A~E7ҰlR棉\CGm?׾^af{G;me!*ER[tn 0p1bE\wd'X~?Ln >5@|ۢ^Zj`(U[4ޤ × fAU"ѪȡuO)ÃR`aV-ʴ-um9%$KL^18Vao"o|fC{`m.iD梩SXWLaog kJH26}-!{Un~!m,5}MJ 'lRҒhQ4K[䴲E܊tDfL _ȿ9yarɜ^zkFqTQsN{4_{S>?4dUݬF$dyxxݼyBOS]BȚ0 F!ZH#D+ih#0ƝΎfL6  kkk;U 'vvvoߦ(꧟~z25x8fٷo}2 8ӧO<#?OU`b4BFV0 F!ZH#D+ih#`/ (4{+YYS[°fRyg`Ɯ7 o{Fm];0qQP* o8_;֑chS;; C7.%\YN;wlmV;wTAZZ Ϩ78kΓgTJA?O-L>r=sU)p=VYTY$cΝ;wԱ(Ocl?Y:%X{Q~59?V ?wX@?nATǣ C׬ 79 /2+c$ b^xAtF-s/m3I^8#eJ ^w gk7OnA|wY-s 40}QI G/WyŊ%H3erd۲KΝ:Ɠ"4å܀wNe: 7fg.;U dUw7ʎ:uԽIFvjp@ pt< #cB[Zu\Ui3wM.OJJv驪sNm wsOYzWvs]d.94%TAZ  w>`]PWx4hJ֙ tUPz$= Z@ *yb.Z8Úe6osU&:o-d:n1rCjL獹Y䖞:U5xqL] ;&h+EPWshԣLQh'8g^lw RSٙ3;*9y|qꄤ䤸ȄnGxcccy晡yX0| "U­ɊNvGӡ9'%Hx$^n(G]8Qq |[w x?oz[6_:2n;gѻ9GH^xaX~~~Btw3P;/ŝC f~R}/"X"CB^/ᄇ·I%? YW5:w*Oՙ|_'8"Ł̈́;}c|)$ﶭ;ξEU;yrN(^Kza 9zL;? 1I,xїiV9 nwDw``09. Ǧ͛77xc0<o_Z%u]$qN}HͷG4 ,N0?ro `>%L BNfH€5b·ufNKE];ؙf\0tCdWGGGW湬WT/{|k-?`?ZmG&i&o :w~21?WWWg{G>GiXcF;ǙK,0ί+;Vz(;m[y_fq#>  >C(cCxJީcWqNa/-k/`6{~ f=,i6ǚyxֆg.g*;o!6&} f30-Ä3uHC|Ø>l=[JA1| ,sp^TTFyYgk 0`̭;Q6.~U*T< B!%3t䢥]|A[3]:_B@ H7zpqq|Rݼ3TAK]jIuq! 2߅ d]NRL΋|*Bvl Ugu7 AuQ.u `%K oHp\"I¨3oРTqGHt^`׈ S֘#/}4Z<&JJ>ؾe˖ȗ^Mb-'t܀EBYqMn+w.o]V[%O,$I:zwtC8..s ߼ƓBʶBU wMP]c@_u/0/ݼ91`֥$y5 _0/EL f`L+qCSx=`󻋜 xyw:y4{'Hҏl ΜB,4 }׬ ]'wF 9cKI 0/ݺ9vnkzڐ<[x9vg'UfD 1Gox?>+XmEEY Ha>F",ؔ] KKI.ʱ0 TL쳎36{ ΦLFTu2O.w 4| A,X;?&5u#/]IG;Wp1=# `؝aslW|Ggw}wTVAy]IN~wN\c8:J|:xܹ}/vH9/|bx N: M+ߕ[e,)[t^ s;X*RQҁ3OnĨ3k Ε\G(2zcfM{hnZϭȏ"WܛT ɏLHYg/OD}+T16 qAע:q(}o%8VYpcA6Ww}ӧ_"J]x 9­ݫwH)تIɱR,cAfEYXh!#GMD=!~ !Z^zj6c4BFV0 F!ZH#D+ih#`q"}'SB*Ə)!Ɖh6mړ)!(V۷oͶT[Qoo/Ǜ* EEo߶!w4BFV0 F!ZH#D+ih#`4BFV0 F!ZH#D+ih#`4z}iX$~n$9PM߮1b!qBwo>00d^CwJMb]o OBw1>X0ߐob$LJL[ߑL!yxo"[~3QpZke A{!&Y!`/M]-|{/]VIz=ypt:OQc鹬(U4N ^!nt'J4GfuvfVY 1UP:_y=" D"H!:L =NGz~13ucD=/JKJT\s0vz)W:U\4"vak#RDN`գ*+4&L\܇2VU,gH IQ F oCU#]KSg}a7M#NkDYnRxyӼ7nm-do<0s#ܙ@gatZZĝknSd$'(!3-a+hs @eޡKkJ*>욵8\Ba=˷F |,}׮=Ǎ+n ''Pr/[ة]i+A"?\/5]ɯxOypR!Wjeiiw =q{ꐬӴ4rs3O۵'iJC"v)]zH/Ʃ?vj0c:7OӟV!lu]׳aAk YAŵ{Ō"M)ߟW)\|dstY_u[$wU0|#]h:!Kە~,C1>+Vv{m<5{-Q5O"NvoT( qʾDnކ@+32D}ѐfW@ +a(b`g_H 2>KٚV]էzcSwL<Tֲi22X<6e293ԶzZ5= IsYf@3ljcӱOjs=({իw"4`lltpCm=QƦRr4V1)(^T Tjb3(88usEB`lRi'U?8g ӭ(T(zǫno7J2[BuNYZk8l4 `|ȦaY #@o{s7qlwR7nugZIaoiiixp ɊZ%< ]*KKyyj-YyՂ%"IGI4zrGy`tI.5h&P,]Ƣ {{D׉efr9 ۥm-+Thj7_'>+t~ɽJ)^ p`3ځ1!m9&YbUjR0uwft1V4$ I_JrAڧʁb)bkd`:Y Cfm'̋ڙ5zJh0_24v{Daو#c:}ǿY7eGD`+W~yAYK=uɁy(lǾ̃7\V?Yu\ж LԶii Q$̵Zv_0s2&xPWK3*5qT؟~{+ K5&^H|˜Caz䪿Gatfbϣ{j-O!pN7`虅~ؖ[pp`Z c])0+7"gy֏F*芦e0Z R=2|m e48|#- Ğ|+16)[a=-`3c.C^!Q˖Lɲ%jm~OzxBq .7huZ=\]Zn3gVgWߟhK;j1/D0 04P8 kiCemw^L Cqୖ ; @70~KƿjJS \gBr{۬yRq`0EÖzNa qDY ~?Ҫl_I[( -djou[*co ?$~Ze2QE }k4`Ǽ06~ur͜k͇ _:[f&+ʸ)^ \Re-dR#ڋ("1{deq4E^]\=9u'uUǣtǫwml}sV#ݾE4նvR,(ں͠z4ru ߝ ܙQʋw>-ןl N;ߒ`6W$gՐ-iikSf 9)ʓK6ղJx`duqjhV4>C͵i%]QX`͛7=V|c9oAx1 QB ep%EOގbe(XX0{9|QX[-/ΝB8%ֹ@H#D+8FV0 F!ZH#D+ih#k,I/qD%R^l'1a1g|hONGI#].ZIFyCb֬Y#$T8Kb,*Q ya.0<+)XQP#O [f%kꆚbY‘}*&I3 KF!U\P\^,+V44gHі<;n_de,UV^0|d׿ B$+TgE{/ Ej8Cr'{P07+*u#VC*v,Rɛ RpI˼%U{K۵!i°^W?|xԶ%ixq ȉt Dq҂W.X,uGfb77o8(0)jkVY,0kmz.0G'㚵mmf. d.չٍ +7$r__Ҡ,W E~^͊d92:Ed -@sҜ&fzk%)"?/6EF;W*"(3:}booY 5]b-7H|q?e2l7{-5r_d`qLmm6 feNm@`ho1xvPm^;AmZY\W.{ʕL. ڶi'i0 V/W(}Ѳmk+'?qek\?W.7h&AA~&zȝ LWW.\V$ __,k!:p߂&AvNt^m0`!kn gYU<|re4:LUd͍;¼:\MP&4 ܸW}ueRKâҤR*ItCp1ߎh'% v|^V=3X=4=r:y97'2$z19dMjR6`W63(#K/V>åY?'+ۖ;U3 _W~`V*'ɴ'lG{eFlʨNL7*mܱdlV*%.,C@*JQ1nȆ̿ nLݒW!fOhn4?`ٖT_SkZyKjյ#YIY*'I~ϫ"*/_V>,P%ye򏌷]7nMtOSZcA?6fNB駟:::ܹckk`0޽{~ҥK?*Ҳl |/¿BV0 F!ZH#D+ih#`4BFV0 F!ZH#D+ihUG!z7BFV0 F!ZH#D+ih#Lӧ͘1 zDzw5F4iOoI~֦쎷ͳ½K __Vx߇`!,}WN6><;KlՓ^n?]`囘εfuyIFn=[QC h0\E $7&ܺ]AONa}jۯ3A{q7vozln])S5j_L[:kpi  Z/0]Y3]Zk-Ex}L}_jbL]wMgb{2Ǝe[e; >cZZ?*?D制:0R>S17[*ktyg_13 (EgSe͛ߚuvvx5M<_l#0UIsC/~z !Z;hjKNu g{ih#gЗGN)CHܡ?Z3U 4Up`4B23 ]cP7TOڔW?XJ.Si;eYzfy}\~9Wc@MFDS 3ln1[ggE3oV_}뙐5\MwѣV KG~i 5U@~'G{9^Y6J?KM ~7 ꇭcGWMWC2ex-[2SUIί\5Ym߶~z "MY ͪ+Kb na<]g?n:e!aM͚/|/'Zzڝ7/ZZ?jp~ KFVK-nhNu M=x#D+ih#`4BFV0 F!ZH#D+ih#`4BFV0 F!ZH#D+ih#`4BFV߾ϚA>FV0 F!ZH#D+ih#`4BFV0 F!ZH#D+ih#`4BFV0 F!ZH#D+ih#`4BFV0 F!ZH#D+ih#`4BFV0 F!ZH#D+ih#`4BFV0 F!ZH#D+ih#`4BFV0 F!ZH#D+ih#`4BFV0 F!ZH#D+ih#`4B2N{{{L!'wy2u bH߾}ԁ ƃ}ӧϜ9b>B6Nݻ`0|ZUb{il)Lk&@8~lϱɾ~8WqONb9֌N?~y;v#ffo_9wWt~ݴUk԰n+',r;ݝMByY&V}ڧ(0Пq瑖7V07("pfJrW6/MP[xXjdt77粢[TG0\ƇCڷ4>جm]L[.vۄn: , TR&jWX;2l7 [ژuNҏ=/0IH2+S]4-3Q-mcQFRjq ̴%-&uz6>,+kքO yC9ek_062Z(*]c1/*j z4eMztMK&}ZS{D2PcT.k|X G}Uqy~Me4Fh*k YS(bpvk* Wm,Jp)뿨j5L8Ul-;x,2wДZjVh 0݊JУ>^ ޲L ([ 5{.?p_e\ݛIB[Z᰹{F  'uZk٧^ժw)ge"҄813W[ME Wm5M}equZDD- sTd0K5a4?v l!6è6r^$ZF A9qq,7M{tb _5Oǀ5'du{ˆG;͖?v?wkg&7QälY)ӣo5Iߤoފ kWvɽ0ɳJ#^.PjyK)39^~mӒJyY%JW wD{qE$F{,vHݫ'}TSi7&F5fPNRCAkX,7ėjoJS \gBђEc7_( tdeBD,d3%n `>kpXsBBF,2x Ä6eArR!#>#-3jhRd4dy}iצZW,.\R-\ӏ{W\iAtMU+Z)Vsյkت*)0G!]ig z4a5E~0amf Ҩ<գ/RJ7%KXS}\^oݛrJ_n_ftMUuPqrt.Ti9tUk;wMNA s6lTQxaDyTӉRߵ1w&])5^@(!a`(e%ePV\@sVIbQkUU;KB(ˮ=3*D%$KL*;Tagg{LPBٲb|j]{[ܱ;^Lwx"Ea Rn 2:!m9C_nuSd"&|uP/J]N؜h~zޑEOpz/4BFV0 F!ZH#D+/ݲDZVbBK#D+؝"t^!ZH#D+ih#`4BFVV3fl1mÞ;O?INIDATw}lӧO'bڴiV81cã_ KJY,d+ӧOgF~gp2~lmm''ӧ6B9z0=vH-*L,BFV0 F!ZH#D+~&xW_Y6( EVsa^9wqLYef_Te%?XengߔIwEU^TrޑXmitor}9u]V*opSlp]xb}@ zԗ:y2 !s',/,*n|cy|Ur. tcP=N#4|񓻖;r3*|{ϱ߿4??1.r_M_gxX8DE愒'~βeA'jlʹo^`>Ͼޛb<Wf\ll9R0Ϻ۷_2ug|ž:y=~QY-q˦g}]lfz<خ4d2᎗޿{%]"#fNf5{zۥ|g7oӔg7xݻBGkvvU/8Zy 'F.|ٗ-#W:#r7gυt$Gj@E8#WO;z~K!kN(g- s=D3#w{キ-_斒_|afR&r9}[`hvW';N9Ws*ҥ]?]rfuN֛>?,~-ـW? !a^_SYUL6@YWe۱4Y;MөL.9\2feayU%VFc8Du-b,GhCvQZ/ 1MQS4\۽lZ?ᮩBoMYS|c f-G,oB8&תPt&ae!R<xXF0o1z+ LL܂etVլ|+{P#NcjF8,믲p^~./L [oy@-&?er;YG}KN?vlƢŌ0 À<0g CsF*@~mr*V3(DĤX`җ[Qa W D<7ulҲ |M͇ ]LpLe`'"Ń3Tb`:R߈}.}Yl_)F-KpxRO752sEI^Vw>B4n0L{mY>go|*֓W.{-KXN~ %,9BkYa0M~>/O0wω;tsr#GM~{Ҹ1c1E_a5р1/#/&nh(Wו[7^W1ݚh8>'yNh,.40'$Fvk,"_RS\a@c\g(e2erj\/3eq1+#|\;^?LCbU8Nj/cL S4YТ+, KH\Z-HZZzuLp.i Q"ºWD"}<84 f.73pSGhƊJeQ6 y|mpH 4w#vϖ9۞KHmIۍ dXp ENH)DBa,4!Cn8sO^ү۽XFmaŝڵk;IۻMqNFF aӵk׺k={1B8+Z҄p ENH)iB8"MP 4!B&S(҄p GKII{.Dww3IϰBXD;ބp ENH)KLz<\j}^uʵ^^aRdp: Qyc?x-٩UL5z?/G_bkǻb5k-N^+!#;v4VW5 z͟fcfBF u^X+~ZD <ݛZtۢɄpg"Y AȆf3"=6xAL<q*Wͭ-yzi2zƒ#J k'VQřo@l_7ԄJ܊>XdtI,BF;-DTމo*n!oB8"MP 4!VA,Yty忌PԊ;ڴbGT8@=Yr]1m{ dh?AO, @H%<@H;jޫ r 0r"ݗv,0fxoݱO.y2M ⣵*`fԾW<+ܦzNF(n H s[_`ݼse'ƻmDCe΃gpӳK:NwZp|Ik4Ru68Μ_󣃵?`7O))R;L$P;~À~ĩ~V%IKK K ) S?*'Sԓ꼴`Z6Ba*ݷ(R*Z#V=iM oyB}XƯys-@RSOhxж\ GҼw6Ypk;9H`o`^"{k$ӿS(GlVզ67Ljfq?ߢR(C/:ZֳƟEk *:AP5yW;sm0cRؠG"s *CVc 6* `,5Z<#R`c l6 :V] a&&lK+kЬܥ:}{+r ax5+jWSvPJQ=s}@ a\j鬮!ϷiN,F_,.Hy;73bfklv/<υms\fhYktʹrGP9yag,2h,x =Hp[AF7 @,VfXn(,^) tx~nUjCBwq<P+sa*GOa[1=% 17|gRĉ4>饅NF,gnIskz nXZmY Iz]Ԛic\"<6:Gs*zx xޣڮß~k\g$kw83E3tF6\+;Zљ^Ao<)a))]ha-}A/x?Ál 9pMĎ p{ȨVۦ8VV1cdHUqhyP/MP/MP 4!B&S(҄p ENH)lM Xz*PA)TVkH5nwp NZ4>#w/{'we[R|ڻ.ݰ}w?Ȧ&',%ڿ-8la)weW.AO,PyDϡxAny%PY_#c@U?IGwfkssuѣ;'\<6t\[Mc`<;TiiZeAԥO垾PDzU}/şt/d ?n:nBtxRlkمo,M97tT]pjpNTXZM!:VVsд/D>kk'(}WE)`bUe=px?)$1el*9q` dS/u+͂ꞳSQсK;K]뙗4)w.[UuoRQуciΨ3}+K{#EEҩȨ@4!Bx)iB8"MP 4!B&S(҄p yG$^(%jӊ)Ri>70VٗfKyK3V Zq~o GЦߧz6F=0»^_fr߆C;[r/^^_K/aBؽe 5κϛF Xu9*cY*)~J%LFaktSyӢ8XP=s -ܡ^4VlP*+C֭ćKTA0S; *`rrq󶅌,E7qnXwНת&?&U@gh rWX9yr\PMPB. QK4x"w(:AtN3:Kk.ohwHVX^&=a%?X8}b/>5ѐcWZr^,-޾ouLFkU=x86V`1<&h[F(H]~č!C zNH)iB8"MpBhHOٳ&Dww3IϰBXD;ބp ENH)lK{-Lÿjc %G>+$Aӯ/ B[kv;@lEZ$Bi5A: c0q᛫(dbiǛ'ns~&%d`Hv~xx1?r(yzЋ # ~,ExϪ^O 6y^͠;m CtSt2iSySë5W%A͠+ Oo; ,#i6!NR](/ov@}QEJsK2.#Knhd0^78cxm_=6BF"Nb; ǿ~zt鉓tR} ` 38KMoB834 qp/! x)iB8"MP 4!B&S(҄p ENH)iB8"MP 4!B&S(҄p ENH)iB8"MP 4!B&S(҄p ENH)iB8"MP 4!B&S(҄p ENH)iB8"MP 4!B&SEPnIENDB`litestar-2.16.0/docs/tutorials/dto-tutorial/images/put_handlers.png000066400000000000000000000660171500564371300255150ustar00rootroot00000000000000PNG  IHDRf`< pHYs+ IDATx{\W?L%KrV\r)XPaj J.'+B_>nVKETR\D-75bL B@%s|$!Fh+z`d (@Q#!DFB0O2e*jt:}k( SNtݛ:=,,, dQ22;::ZD!ZBh"c"z"`. (@Q#!DFB ! 02B`d ( |sR7X`HIZALn tޔz: Y%ۭimC!;7Nݴ5ysUe-@/TXF i[y{Nsx[7yY[NtR XL)tޥ!#- v`yxJ*H9$.o~QQبhۀ k=8&ND,~~q1^ya?}+q;] A?/b=ta2)zؓQQ֨<ԣ2/|OUj7}߿g4_߿C__% cԩ#8ZǮԜKN'mp&z)cOMڭL>K!E{&>Vކ}^ }ozO^H|ZNѥKE9i=, ټq9.]::y `I;?I9'n9ĥψǞĕvs7et_y;-e~}MEg.zZRl);z# d>ƹCGZHRcR ?-CaO]*?vkZNr\b{< 3m$K9_Tt>#1o8w\e6=։ȑ*@]vek}vMlܽ mw==wcuB?| ؼgs#;wg_a㢽mJ|ۣ?8k} |gY+Bف賍W? Y*:oZwpnTQ3N"+ʮYrvd]m&TT4ٺ%(<[ !wOVTgUp3׮k|^kޓ3uw'V6 :V2HJ`1~w/_~Ԙ6m/_޹sgl);y$.dm=4X~ =!u_

.r^I'ƹGZZʎ:Z2 <6,R}vDe I6} ok;F\*U@Jf- YkW&TNZh"9cQ**+OV~vv?f!=zQ$b`:J`0*<[ !a~tJMUqԵ>Ý k$@˅#Gckkذm\P^->9M4oK}-o'5$bҥx?$T?VmHɹc.N{}"ˎ}xvo֏'TUDTz"2Ƿ3~t>ehB4=2$K95zTYyD=D_,<}TO?^,:[dg9_M\tT_c +?>ڴ(1m==aP lNɹt)'e-|}@C w!q73m [[ۇ;yikxص͕<hʳG|"B|=5F3% B\yLۙ8@%q;a'#۸-qA$f\X$UA= ZmjjJӧL2B=Ά+WGGbj!Vkaa1ٵ@h_GG>8BH?1AQ#!DFB ! 02B`d (@Q#!DFB ! 02B`d (@QLg.E9DVc3{cwޕՈ2"i8Vk\YD e<&V[\H.QOv"1gr`FM܂+s\ǺW*Np #=R48;q$w1,ͶeY~4}3wZs8VGS `W{F'FF"0ȒՂԱGC9kU͒ aMC3ҙ~IL)~Ȗ١4ɓn,Kg{01ZjQN,]|<եNs9IȋtN7j*v Z] KS;?%w@RDrQ"Jhc=CU,㣊WǗcΥt&Lik*Ӭf}L>6:-64F}:so:`0F NBiW Xt Hx^< meBsf.q%gN(h3<f[t"IQ&ݍ^qJEi~g 5')oE[#}lLXʴCem t0̉b9fCBA;_ٸT#| /ר]b7=FuB),uᚃN^P, "\v[mEZcpA>[#yULWw}W%( M 8Er7S]_T9 3|!~94F?=iNJMq ͗3O~S3 6 9e -{/mm5s9$]hm $*IZvUTI`- rbk yLYqZ0bd.^9 تmN}jYwYSVuj,(G0RHLj?>x`}Q~8 \_Qf h66irT 4 ?3GըaWT4p,y=+?M>(ق!KyWRs 1pC剛RfIEhLQt,|f`tpL;_O-xsj7ɧWtU2}_,g^EQ|T)hn$iXx[M4ޟr|f[Ԉ\M;$ <ΏnFZPKZLL7qaT-VGxײRkCSeC!UZnp|J"C) ]\-N*0F\:m:xf`YA\L˟4ROsiU=0-uw;-=\Y$UsS}S1-u\noNETYE )@4  &kN*㳟 _Vܼ\ . oI;5jK <]Ugr&7GP* "NY@$Κ);i&N&C.H'|EPTJkN7GIoeDbt ə68]2f'Z+2]:4˔(tsJ;lvĆa>ӓ]i0gZƦ ,Kc#.͡=r*cl ?:;_/]1/!Y$r$t Ⱥ*ɂb#8ys&hYs00gB]Mu``}xA<ZJ!-:yR V{:0umm2g>;H݆&S EKB[v @VS9Y8Zшtju49M-3, @ @4W|:{r^ Y::T&mC-yt8^Yna{}a:;͙B3g1U}:u,kvؐ$dش;|.3w@Ҍٿ4C'b=VZoXMV5޿s81E Þ͐Iz jkel;.GYCDYLY]Hd'J!ssֿzV%F3wZ_/7/Es}ЛxGK;,|""mF(47@ l\]_(}{FƁ?-{zZElM\LM3皃~YZU!x )D:vSj&Nu:4׷Ի: ܢ񦫹\ca8#R/ Ls^OߤΚKf[~G\,'bF7 ]gNAwulg8Y5Gl:9鲯/3vߺ3֟`DpvU64hȂ\PvqZanY ,Ƅp u{#Y]l/8g80<4y2ؠ(Yy"`{lUT>Lu`;LyDEg6|ǜSHKc7ĠnJ`#M_kU/+*En!;4t"70A!-,rG6_nɂW]O99_\龛E-;/;(vAc|bt5'BBCgsM~5$R{"GʖA4u}R +“,AHdlSn <_ν<;d;N]_)Jn8eFD9!bQz#dH*bT x8#6ۑ43bS /vK@V^Y]M/N6,WΥt3Mχ5p}M zm,V%ס^!5eٟ/H^=igTHT{gz&=Lq 10R.>1y8@hR~ ߥ)b#bjad (! 02B`d (@Q`o@!E6ɄQB*Be (@Q#!DFB ! 02B`d (@Q#!DFB/ΎbJjllljj'{tVj{(ʊNt+++777cc㱨MI%IIR 1#]|E`,^,tQD"tTI` p%hID`r03 `a4pB++ӭ.\8NQT/R&/BTބ?_3AȰM.IkZ~LL ;N2uBw|bxϊE,|)IEpʤW综%~8ndGv]j +<~濚 =ܳΜ\!UBp]%˼^(p9,D/|) QO ؟_vk xhk\T A,U[>[˲}.yvBrmPQUI={XW 7^{9!E~I= Z 94N^,ϩVT RlGVC'.e..ˢ]G\We K|p߂e"^RZCcΎxaAcx/γI9tueo|5[vK]<#QQ.7q|q  IDAT]9 _HF\<_MR%XC!UO-Bnh"o]n|yV)?7j`8 "2(Ј@^-=ޡʌ}y b'A>3d鹍$uYU\''p}u$$ YJ@>սd xצˮ#ޗ\CmnE#fՑjjEu$ bۑ;?؟I H()XޘZaكFðw|$9T%#JH:Jc(И B5 ;2@U~N/Hp\q^1 u2W?>A2d~Ns@U,r&UVSUV\0# T"$%[R&5Y@ /.FUUIԻÐ=p(p D$)/$߈k ErRW˵w"Q /NڗWG<9ClT.I "9 O dvzRK]Z~7A e =ҫ(4Oߺ^`)e`0h ۞͐Iz&jkel;. }׎.ܛltEǬ5 K-Ԫ[A5,\'{;]jE"͚Po;>jĚx?WWdG6'(ۑ JhQC#U W'.-+ii?Y['c{qpai_?AL. u$I?Dтȸ^8:mo8R̎t~Dq8^ȕŚ@Z>9Y'fGc몸bvxh8U?̯++\nUjUL IU$ :~c6Lu'ɑɨb6x2OY/:IR**J۠g%$)J$gbj=g+dAjjrIjB# Ix4Moc" $dyRBkTOLy@K4HLy}7)+VBX@EbJ "HKڷ#QTY *~H + S.y>1?LLSUjjP K[BR,KN QI>WOB]c^VUyvC$ @ʋݑTRz\P',PW!LEԾŏV_v ({r[[[/_E 8@}<<<9Qmmm-++Ғݻt]:NѺܹ/\v޽{{B=N7P[nYXX ~IIB# FB;B ! 02B`d (@Q#!DFB ! 02BЦN:u@=1hwܙ: xb#!DFB ! 02B`d (FFF&&&<!2dduwwjZ=SRR&&= SS?,=~۶l tZ!~ㆌ ~ ?1AQ0'&NlM/c4O%jծuak!IesBֺkDmc:.3o}11BϤުWOw^Ahk6ںywJ3LM@!M/F7:~A#l*~,cf T+iju~ْm ]w/WHε wL~p0\7?Slr]=72c?F564ObD#t]v^x{zswԪl/L3ݚ:;;@s9nt1 EyYOIӈ_S/su?O;vkoܼC- B` F._ dTI(F,s׻@6̖'^`~#ߵ`/صpF7[W]/-tfswVAnә;s~'MB+֕*t\.E}Wq)K7oN9 Vp o]Ќi{dwW063~w]yZ;:P߼lfP :0Y&h.[!$A*ll>Z`pceho1Ƅ1 ZЭ ::nmQ ԫ68Z\hZ`N@Ӟ1{aȤ΄}yqhi:U݉7aF0yr\۬2[xmnzOC U(<t߿xۜWi~×6U.iݢzӵk@~rJCܽYsbEuuX c2>1!-YWfd]^:jjEײ5}0ߙ~3<_߼j>{UfnV.ttg4#+y[QnrX=7jt0ΘpL(p7U6'd)gmq_~ݎ!e˪>}w^ =^X;ML-^dC/btC L}TP/suegݪ2eZUJZU$t.ҊZ ]ijVۗCWwE?q~=s iݨPSx<lmm&6򆵃sia fj`!ϒdZ 4۟hThsMλy{\))2u]Y߆8KCO5e (@Q𔜘7<~ 4QB ! 02BLOg||-[_OOMe2#xEDΌѲ w/f"⌜m^L7ѩV]Remgmi_ KcSYFk2"Ҳ*ւkU]> nv>?N{e@;|)!|;R "hrMFdt4G*ӄ:0*r%E{,O@XN\=v_Kë+BVv3ÎS,>$_7'USnb w<~@U~_"ϼixM^4 lq[_7ͩcR};ljܘ KB]Gy*?J9}- 57W#(-rs蝯/:1Q]Ok﯒䵤jUgmxD-Ξ^Q`e0̭XjUљ&tcs,LN|}N%K[?{]6?MɊE(\wӲ#B7`iIZ/,+iFѡ$uwNH$scɌ TvaN0: ) 2WIjkihPeM-,t mKO+~]J}N{]g=G5$j̲_ rMۍ_}[u7>'83yǂ7rּ&DQ({GܭGNqLms{K5 mJ3K6tުi[~V=bzT!+eYswbⱕ&4Skv|ɼ7|;R_~?גw͆7a[Q{CntyginTJ讋1K.k0 8&$2T7 ^?{edgIKVj.jJtU&~NIR^_R_VA(aV Wq7}KPPɋ LeHPa;5 ۪N|>$>8=H=%wc>[6=n_}m?8vc]u{<O*M8e (ߘ (@Q#!DFB ! -- bTMn~__ECEݣ-g޼U?_yyu-$m3\1RS7f|;~egYu%E0]:6(ecpV!VT+{^W^3B2\+ꇿcf|Uanϛ|%w|sWrC=7y]}=tamɁيsFf4ytT{z'7K]䭓o; ioLj(ЖеnϞ;F+eiAvu,hfl(?ɛv_/X>%Z𷍝bn1D.v Lr[7o$A[tǿbv1t?GڼȷY,1?Z\έC%iɯѿ֨~9B_Fg&?9f'מYsL槷.3f-Ay=.1aϘVV_]g}o53!&lsߧ4~ϕ/:bފ lf׻ϡW@ke\Muoja}Y 3;$Efq~S#Ki~3`j ٰP[d\e4/`Yxץ Z: a7m5X4 :؞Ϙ|6hWm /btIť^Qha.Ŗ]ZB `ɉ b,Q<3}r%X~[yOD߲NQLgQ[ƸUPN"hjvEn y璢^yxM  "'; Q]z.gMwg߶㹉]vb?:%K_:-9hso̙g|.[YNgSE_9@!nfz:r2^2u'~x*NR_ӳAF=7.!a' %0IS}V~S+[ΗԚ*9qdۛ/>>gdM~ӎ VVR*MDm{\%+wkՍZln){,:L*JA\c3Ͳ]?3+iVϿ<cm[ʴ`0Ɔ0;Nvde0 f͠jv@l+Kh[ku_׮\~T kb-ͦ:J ;T竍wN<71$oU (9Ko5>q+_ ͞a9ssf4_(X']iV̠5KqO50c wXVeY~oJ2BoLB`d (xeXYN01%%eR*i 8zh?m6Y5A'&! 02B`d (k>C\ieb{0-vҔ5l(kE'#'CdjAJmjW N\S{F$USÌо8,<&9~hj9êw'h盬B(ghEӣ}j{Obtnl4;MV-m{o֜ᑑm?qsojw%h5׈2PEs{=vO<υ#7areu,q/DU>Džju؛ѶPTrdMd EkC i׭ - ]sWS]ҳr1q'wӸ~ JFNV|zưW~} (g{012F`裪E:C^Ĩ W.ȪeבLYqZ0bd.(mVeڑ\}DK_жkt``9+OK.6LPHKskm|s)||Gr||v`'͒gnä3'Wx[pyQ\\V;iŪE CjT#6> %B$@I+!({?{=${z>?:w)cƏT٤h-۽W9yj|!Ӟڰv֣- %߯*󥳸'8ǧ@09%wڵIUrxuvUpdl7eZ׮ݑ{9099Zpc.ݱn'#0iLkn=:^HqC1KTr.wpǺ͇kH۱nݺbfBTཿ #w+_h7 [g9{=iN m85^[hnK^ʈcli83,VѺP%r>[|-th?^Fkoj_jVm,LܔM<_|rɒĹ#3֭|]IS?:Q2氪IUJ T eV0ψI|^[DUSP3[&Qp=|}EMMe>b]ke~35.k/TgD wHD @UQ26&!Ѳ6u+-yEM2 W+1j)nsqcXSeC]K!v<@w`_AssSG.ᎿgW¦}kNT?P2xRMvqF__ϖo}mu-S}뉖Ǽ-[:n~"ԺeP^tZFJDR+uַuS"C5t/̴<|<.@f{IL]/^aa4aLW[^^ΦN@}Y>a cvyVq`MwoT~xw9 T.z>9ig>8{$'Lvlect-1^\8KXdizw"whVsc.NU4n-z7#?F_lse=97Nʌ0ըԨx˛?>wp6+f)B?gann}~X%Vݷ\?exLH-6Brʩ>ԕ); mO>ڳ< Ym'OM~39T Ssx|"HQgTaJE*rF[#zN(g:slRw5•~1]ȅ(c|€axznMZDϊ\Ʉc| OGQ["1 TxL#> Ew|rF2P0 [Vǝgw 8{7j_\4^3`0؜5tǽIe$ X, 80c-T\1Iii1FEp!/D%#*/qQ}hͪN6my_h~yY`+ZruD˩ONt(˰?sK vu͞3ki/l)x\u fĉ`jf6`sT $Zͪ!&Ƚ]~ف\Qr+q.c8vS/XF)`,=\N¬c?ON˘-ZZkʎV8~zQrZc)o80"˾2#7,)iqF5sS=|ᢌFɭ9+>j9 ṝu}6{ŠI+&sS RU[ڢǨTҶk*+bfoIu|QҲ5JKSyyI~wyFJ3)C>p*F ]rO]"7aˣ>=F;}O&w>˒=O.x*o;RpAy ҷוơ*MC|tLwfpvmʔ0V6()(/ũf.y[fDNbp4q S2 KX `Lu;>=),չOo_\0uﲫX] aړ.{{:<78"0%G4cOU~4O_Va'8P0HfrpZ;?v KD?82c݉Q{G]\MљKVx/"?Իio e2o0PY}W+sЮ.i- gkE-%[vPPPmIJk53ǫsDcCNSwc& UهJ4ZPq` h {iiiGu?ѣG;7>Vkg:wbxl^{-SBGdvX6#WH,WT47&`,F}Ur t,^y5ֿLAN %޺Z+ሄ"WWA'ݴ7h_WH~zM[5Kc$H9A㣤nz %P"LLy5UI#Y[m+Cj몴m Ka>^jkhEԏI[&*?ѸPݟl6NP5N k{7Tƀd55!M{uyOɹ7zMA|'΋ sN_LB4Nڦ5yj#Njs+f٨k8:3P31ڼٚG,K.бܕ+Wn˰.8˗ƉѸ{'Fx`uƌl_.V܋˿E`\xc%Ktr*EɹU˩JkF'*im9<>O;M.UT#*6ҏps=hn^+KwapB羴860+ `7LRܛa`, ra"* yOk p8hVM raP|؀޺K# .0F>!%ٻw}  " AN2vÙB"^.qK}rƤW?cՖ"Yh?򝌏Df,ڬ6̝/.*lbw 1seYՌꐶ$|A+XZ+5:cco1aLN`S{fm^01aqF1%y_vmeBa71}ɡ+"Hȅ!Kj&:JWrob^wD6?e\KkmYA"%aqFEWQ`0T'.HZ0CEKE"钌|_.)%&,SHASwE]ۂUyy 1];? qď`5Ю)IQ.-Z*j c/y'. 8R}`1 >e Z/5h"?2]}'$N`ir qx|Bɭ67!HN-r't+U5|˹o2MR !pud[/Եڝ_˿KRRK&atv5kQ;A5 rMQ^W+gUKҡ!Ù#'29!;S46b$2\.V}j;vVK)1\V[6[r2hKdKV/]J6'Y !0ZԬ e8^rPׂt`Ba"E! B =ܢ,zpoּ=](|&uƀ~[IۦK܂`F>yy$ 'xz/qs=yBwPW<\_m-Iwщɱ79/OSV%J!vsѸ﵂:`jk[|u_LQZ>A/nmU=Ꮦޢl{oYQ=ۺÌ%[6 >eks79]GN_!{O6?n'\89Q0_`EzP9{]~H-$!!!Ci46%~Օ5>#vUǷSMݝ/W:m\ )$M*Z]zFL߽ʸz6 ;w*-nxᅆSo. IDATB凋ߵ3kxN7*w|/l- ϗokvL9I3\0z :,S:&+cJ7V[ʟ.l9#/5;ѤZ2, P.VP^ v=w<ۢ/F.j5bvt06>Ǔ<)Lٜ,.sen_; Jrzqj#gLx'',P |.j2'I%m2Yo7L.b>WlZݟlO͎ut~E@[p_1/a:Q~E]r%{.(?r_l/t@%Rn?2dCg?c?q2y 2h–/mW!qI0,_}VJy1lP+2!,PdBX @Aa"E! B ,mdpL `Kε>yqeK:3#UϽvI J_eOg?v֧\w u? $%9 ^_JutwZ28\^^*\<&b̈at'ֿ- 269+b1R2Rlϖx*%^.w K\A=]hJ#ڥgxBw Gqʱ|u̦U&xǮJ 7kgf,' \=Wn|wm_KL= d?sp \Xjr*cQa@{JHφd1ഝ:rzX4f3fk^F9kޜ8rgvRW7p<쳯k!Tnڿy\~ᘀ\!Ei];Q|J -ThSt1a<^Ꮭ%^pM ȸRivx>S&&\8;PTz)nBj:8-̰16+׋h\v3]ݍ󿏾W[a??8yj͒%/mqœڳMئ | ޓZUc|0t`1s9?١ ϓ+c*zZ|YZdwƢ:o`.,_>_.{)E ~>YnGz/?/UFr x|ngנISwIq31^(=^Z7a^. ]-~'c0|uʹײvLٸx:1]Y;A/nʷ{7ݟǣRg]W} KIrdQz,[OiR,<4_M*f3z_3'-{8 hR,\Cؗ1l\k3+S#/nMMbV!ƄE! B (2!,PdBX 0TWxm?ּ=](|&uƀ^W֫raV7. ~2;ſ2с ! B \ߗ\{ZԇEjn!9]^3H#GisZuR pB+D.?0Y?׸UB)v|cLgZϩTg=w3oV<,7\'dryd[ϟ瘵ڲkpM;mOIÀQABH\o 1 aG__ֻlonԗ 8yĸ<2<"bପUI~^vj$d0<2:;g&j cן+P/vT]qBi޸$u7_~cm޿$b:J Μ dp ȸ5t !aʠS$#SB+с ! B (2!,PdBX @Aa"E! B (2!,PdBX @Aa"E! B (2!,PdBX @Aa"E!?&іIENDB`litestar-2.16.0/docs/tutorials/dto-tutorial/images/read_only_fields_error.png000066400000000000000000001116501500564371300275320ustar00rootroot00000000000000PNG  IHDR; pHYs+ IDATx}\Sg0K'sbD"Y0؂E7,mC;0+ܙBgܧƏ?~EW%"VTRexp I;i K@@D^?Ýs_}susB!`@!гBYaZB!+L B!diB!0-@!!b\fg͚5;88<(B1H ΝGf>+.@!d#-`}}}3 B!"aNBN f>B=u B BYaZB!+L B!diB!0-@!!´!BV B ӂ_bYټ ΖjYjFL-!ӂg9N>1;6,u^qs`+.64]1*$""|fӧ3:Al͒/]T}Zae;g/]t8+gV'&xax bZT}6/c``KdHOtҥYΤ՗/?m}`sr6#/eG ,&xmM\M;-//=B5bkQ#I_\p̰ýI瘽1IxŨ 9; w\HzEK B3[y}>:lP@PއqLM_i6 ^#Hz O=E $tG۷os۷Wjj$̝;w ܙwҥӒ >Y.=U9/nkf-#itfϟx, =z "]^ltks!!*8[}Rق!g䥷`uF՗.?-H-W&3\#vY/s6X/]#2VWWW/ΊDœ5nɫ.?4x7+ԗ|q@4g7 ykcJ4us̈%Kb"IQ}4pn=P}Bg9O$F6dt\pV;91A5P_l/:>@DlQ%&ff'&7{nغŁ{ӛ@o^=[#l ,O:En=wIJ{;?X< &b1O=ydbhw77C`k| ߵ:H+ 'yep11Fbes6VfyCL5>0! %_}! L^̟WJ ֿhJLIJ]Eٛ1ߕ<~3|۷?7|s03?ogݾ}{z0IQ]JuU^Sr&ܼx$_f `h>y 䯫x 0P,`;r&#ϑkbD '7ߤޏ T.pv f`MWLI= xܚE_^z%xc)ǚG = j>yu -0PsX=L&l0 (dr$) 3nчDt'jJվS%k,/SpxClw&/R7/i!H xvF^r =ه`\l $Ci4%q:@8oWnYyO$Xa,ij:Ui^ck3A bl4qXu>.\pݸ'Ƈ }6J% ݙS!0NÕ*]xRG/'-6-xi1S}ȩ.Hsk;-l5K@•s' 'On)_r*>U͙k |y*4W/9 t%* <5+֨:rmG\bI@sa|fe^ʙ}5STT׽{s&A\ BΞҿ~hPҬFFEۇ{c4GR_49rrYV٠xS]М$[Z>Sg #{wgnucNsdjWY{ 4\Z{L3OO̎WyɃ-u;n]4;\c'u͙Kl`GAsf 3wh7>D_)hJBZ&!#B g fΓ-@! B BYaZB!+L B!diB!0-@!!´!BV6҂|!zl3 B!.ihttttpp5kB1zQ__lGGG Q?}Q Bc@__%9@!/~!BV B BYaZB!+L B!diB!0-@!!´!BV B BYaZB!+L B!diB!0-@!!´!BV-7D.^3B0U;.}l迫mU &툢KS-0U;4iK>Yd ڼOuXn]D7iL mWI .ߜuBI,K9[=`<O#tٺ[z0-mL[gn:;sRDۿ;-@6eXoֶU奧Jk x#)6v8oz:0D1vnUe:={!CС bb„.N [uNVMQ EN 4n8oMEwTzX͟n Z >n}̃/C[Nⶅ.oiȴٜ\E/'',P4A/[Y7)~lv2kp^Әt ΒO!'x-{֦{vϫPg{ NtjAE>/N5씘k҃ 0񬔚uY4aZ1O{~pjp,&ݙ[?Gh\Ƶڃ5?o cTȽNsMkuN?tݍ\@TH1A}`9t&#j:~C# &.!`X!/`Zi,}F҂A&\ݲ-je bB=,ԕ[(h1 u`[@E!z2_VG C5p] z5-+|U߃ 2ٗҲih+iP]]RИX~ z={TE'9MwŅ^W1:Y~I7}jzokuua*+jG,hmCI%T *.&H^Uy+ʣn_f{wa1=!ɤC„s}{mTl fi s.;|%l[ڒ`<ǤYwŭk>!=o2 ;h]N8HP>hοa̎V+iǤmX% 0jgxĭSL n\:wtR^I+=V \} ?hUAֿ1Ank` Bت4/-T' X?=0lUD-NVu`z˜M"h#j*v} \`Le,+۴qm#:-!:/:xD%zs[7{sc#-w9vpFE{ȕ2,P G): Y9LdD QίTk0?.Ipj?z@0z{H .|Y&`^=BhuqpNe nc461F/Ld:{8Kec<ʇ]3jnՙخ2,o.:xLC nP=X;a[ ,-q/yzN |ߺw3]".>zHZ.o(8p!X':|n 84y%$DŽ]>e;Ok,9lmr ċEih^xקMh"&!"iiUȕ]M5q\kz\1Ǿl52X\6m29M^Se e A.tzlT, XtW pITWu׌N.\`| ueJ `l-?|RW4MPq(d`j={Bnbٳi8pǨẙhM^ڬ.=â?~8oGQ@ʪ{!MFM:5xt+Q@gL~.#;籀n/?Q4gd[Fή9mNv.bAHߺsi.szdfHVZV_xJKDH|8Pe"[GypmavZ.)lã=3*_#@9Ncikah/[-^NG8k=g# 4TYVV3]nFh2Dx7:Kzmܑ+).JeG/\14_=Ձm2>0,&wh}p}ԛľƢ4&nhR#,P} xSܺ}1g'CX4%m ZXuzl`'zz{4prb c Zq@O:k'2V<~4VtefpO<-`"yNFۉ Z0 չwgت(jUvSܺFFځ`?96듿؅Ƅ_umg͠΋6ZD~_r57juZ=|w>>k^ 2Ռ FO07OFOjmNl^۲@XafW#}]y4mz+Ǹ| {5Z#X)Xl6NM,W1l'.äw05_n7v幺E.45vW[<..ˉ=UpɁ1oj`{D%+jϭƧ fԮh `0؂ 4 W"eB[;M4 4Ew[ ^&^Ћ< `' ˧,'QӚV2h` V ʖ1-[,ՖL i O6X]f&I[Zcx,/(.)AĶݖve G.dQaKL + GػIbD#Ώ @L4=pIнw^V=w[6>g1{Uo}s,Y9Bñ ] L䢝2z2Ougyn4=bF@YeT]ob"|7B!d7B!diB!0-@!!´!BV B IPfgJ@i!¯3B! B BYaZB!+L B!diB!0-@!!´!BV B BYaZB!+L B!diB!SxI`0:;;; @C38~fg33{{@??y988888̛7/00 X"Uj%ē TSc](N_:1D728PѨRɒ=F8O'~cڿP䱳X/]_-x=gvT/ut{aeSʂM.kWY_A8l洆 6 `޼yϛ7/ `B~S.KW.y9zэLLъ׻lZ"v,?c xft ׎liy65o8xot_^]in̷WrSv ?1x^~ռ73 w(a᛼Sw|(/Yo_+1|Ҝ||DH \]]:y\]]G.uK)2Emcc\"friD^R+ #JR%rEmcc,3Қ򂓥*Uc\t iL,:-)J+67 WQRj+$)K2 yb-Y̖)j0#&dpT^hI֣=s8!_&/,5R0=r SوhBLPH"G5J*Wյ\"8S v/l̋ Uh3Y^+v-DQU8P0~dww_۲0OqLahO22%Ɗ IThlg-"/o/QMXJ3g(" gXF,;U*Jjxu)STZyv\в4k+RìB'K+UFE^RT.[þccv?`{< YmTj+Ir2td{ZS4Ty֫^Q|Vq^cafj^Emccc4Ѻ=F5dxKybf")VM9Dpr2=)CVukeYR n^y{id妹פ WfJ+Ff8MaKWfvgU$~2)/Gҡ3BZ-Y2=-':?)v!3!#sd[KAIr~JNp:d//LWEyYq(Yea}TNV4 @FfAd?hwq}nmQfoOm|#HPF9/[fRQ!U q^@]U (̫XdHWEWX^akA_mMU縑"q\"j%b+ TJVԶAKEy'qh &ylE'Ej?Vq-W]VUP6b{$BMQ10:ڽFQ`hDZ3$>\g_6QtAõօ–I4f^j%zGjpSYbqFE'fEL}d Jdj (,;#$:dN"k4-_ElȰ\EDHH;` !^ =BZVՃ<Ȥ>CY(5PrI>ښFPRY {0ڎauk;t޾|3=9+.L:Gs:;CJ޹yvg.49yMzJ㾡K[sp03`|1%;)}arfIp9Lj`ڴ7>f3Xd}ۘa}4W=a:ߍ8l;:7G[am?զr0_ ,Ke, +ұo^ |CuQcj- 00LKIm@`T赃 |s8ie)+LڎHۡy^36 ,pEgD2L&AgyXߘGf(0}\].&FHJ23OPZ'(μxaKqu0ځj-Ggv fޒ)n|~*jzNի~7Ol`8 a .gt^zAZ2m]M"-0 zZ= `F-N*dc7lm#r8}`"-3k"X{za[lF ⍗j HɷɣV49#o:ºȉ0`ᙐ(ILS%OʇPf#ZDWcnILmSd %'%:@n+9<ˡU]ԋ#V Yb1p Ft>Vt'˓j'^I %GqX0ׇgVJLzŲ-0^ yξy9+ypVE/ܩGxQ|0zrcBɆ3CM&Bg#Y*,ψ@Ef$dcMd-$/85O=.[W.Sz%fD{@xFg${ 7R^eJyo) /,-^dfdK 8)fЗGL&[XJh*uO73ũK9ibۛPh%eM'jj z1UStOH++ڀR*|3K %36MBɏuWVAM 3:E;N^ɵ!i)ann2YXze07ȥɹJcP0d{S=U%rO IR(K݀U$)SiIȬwDHd@Mm0K"$g$Y N]^II Wɦ6W0PRI(x8ؓ JKKG8zӢ i1{BW,)H/-$x^*)Еބ ޛ(+ۗzxp,~ļ׏:?Z ~{=pd8't`RJi_U z.ߟj@s&tuuݺuk޺ukaPdnS5exդ捹uRSOTsByy?#?v:Yv'&Uӧ)6oOU*F֩>O1;l^iJU֦Y:}yTyϛTu&ʩT= #Ӓ%*Q]囒16[N*ehkj7,Uh1 %eI @_S1x5zhS(Bi6(&p;BrV iܞ66QηMFkZl_YY3&USYWK^^Xxe5rTjA$5Em ϊRk텐x[bIK;  ֩ꎋղ*xa$_t _ۈ2P*Tuze@ SܠMu5Wש>OaǎՐԲ*5S)S|o%r7+Fm=UOTu&Rm<-J+dp|o[^^^{''~AWWݻw o߾իhOt .;{xf+ALy-3 qqRO!٠:/W7v= lBMXmR4g_0L B!d?B!+L B!diB!0-@!!´!BV B BYaZB!+L B!diB!0-@!cܹO;B=o~1 B虀7B!diB!0-@!!´!BV B BY1l.stt={ BFZ`ggw}h4g>IIJJ077w#A!~l~B0#C_nݴiEBx`3B!:i>OB2'B!deO-☛\ur_Iyw`xs։Bl=/5% ƟcBbM3%&Rff`ooKo@!όt߫u&R0*3?xLS!/Գ`̜忳=Bq#q};;;;;Yf8,_>:ݒe?Bg:-`y"C@t:LLJBlZ8#ˋ뵟W\nxbc' BefRvR9u_ u;0+@!˳~aBzZ\OQuF!T,҂ދo}A!g!a?@!O;-sS_<(B B!+L B!diB!0-@! ¥Ix'1 aș`ś>?[skٲnmvk?NB3I_-[siq>%Wf2_{{<#M u{=Zُt5PE| ye?%=c{7dgrroh =[| x0Ε3oWSԟ>I8w$͓6hͫ=_^kK_a9=ܹ_h^O9^[^ Z|.}~`m9nzU%np]5-n\*{ev̹XnC|ޑaR>zpѝb1+_71GoɎSu'x."2JNɧk>NK 7.6ߥ:N..\._>y$ny3ug l$Q EA9h -m[{_ŋsm{~~qy=΢iN5;t:*.\]/Nݯ;W?wUi*Ex*_gd[:BGG.짌!E8}>apG}t7~0~@^`-0|Nfl6R;z 8"@I6F.>#\{&{>Sݵ$?gg|'bweEqhO_-83`H@`Ρ__/ZBO HWViˮLOR`oܽajcF8>༹KS ߘZUVtީ  IDATZMUֵ.pm_B`f?@_ZdX,Ip`eTw[F=g'cR^pjh|'GV";ninEAixގN2(69 <<̙{B=ft wws№y`:ֽ|,oFre jIsjcLƿf p_RB=f \]]J4~Q?ը6<1Non zIr 4}^9B X麧Wis!!´!BV B BYaZB!+L B!diB!f{ f*4b @߹Xe݌4œ׋Zu))y\iC~NG␨Oi_uv^o{,` vyAYw< ]?_q{sn7ZgJ'(sSV7ݪ1Z5' 6V4U$=۷26w4z(Mzś?y#ӈ`]v"c'|c}W=5 wWX}Ay+OY|k[pGלlzjs0D ېW;#n߮C&A03    mC[۠>9Y|7?~/H|B}QяeWor'i!"ϼ{O+Nz+ۮ]DŒ4v+cfuLAEX$>z>ibޔ!~UUg5~GY? R-[~]ґ:WUXM j2q"#=X{G"2U qVm$"KbosfTM*5ֱ0qn~ 9ZZ5Ʈ,{#\~u7֤IM+i~Įv9z"Ag ^y73]M^5\iҗػeϭDr`D6w\N>IݎQx9}Vutc>~ɎwډDLsFQv,/?jǥuKIU|F ~"9;guDÇFGĮ7W H:߾/j=aSSU?TC6[QGď{|ih]6Kwz=$.+?t X1#mwv1١+7TrFI7O|F""S |=TvʕzZ5jEV_ڴUٕbqdtQHCEHj-U`6Wqxyq;DD#I$;it|єv#Κbkdy70Zq< FFʀp@W,x}UZXH`4h4.a\zu;kXVhH7W>qDD\GEDDzF'dpdHY_yK|:'+pmo#a]v5گ9ҙl""bq $PIk"`dF2iM'ïM#q"=H.EUzLMDQ3Β&UY 1ۉt\Ok-,+% p,Jt13:δ^{U[#kDd07g/v)5 #Q'ezhW}OE-(l]£F"c .n`"TDD:Ќm;-c=bcqNտտ4wxJCEu_ܐpWvs8ݞMx1o٢ -Q?U#8/z{\^0t!'':z^ )GuCYD_7ˢ6x©&Y@obDD@0Բ:ߞӹ,AUy@pnV3ndw6;.Xs@h^;$擹J 5_Y6im:r\0qqIڶN36'{^BI/y+~g b_$ZT=WcOs {r..,}y?DF6"Qss?ke 1t?A{ǑuqaXlD%p.x|#&?-Tg wD,csUF"##JKtю|~vI_+xz޴D"gU?jbV\D=rPED<ɊY9: Fu?1m{3 ێ(X/GL=|Y$ɼemj:i+]z#:LOFFq$oc+{l,8A^~μ:P"O{t}(+[2bע x7 vy!Myz7_Z=[i.t !DDuKW ޘ;۶ o[5G=hҽ`cŬ_'0u[>ƯrĤ2 `g"bc ? `Xf`Xf6.-Zps{yvmϏCvZ{Cޘݡޖwhcr٣Z!`ă%z;`/x.:F5ݢK׿ on9uZ|Uejz `h8x5#Á4;SoJ,*u0;6wXeؚ\N]G̏tMĎlcg{hŤy)42\~zৢ Lk7DbavNc]UL0c cƜL_ʬZAbX`j,)ai ~TTR|D<fyW; L&uڕj"ӶVӃcM?hdȠPuGtMuDT};T?QwX<0sCX6MzSu[郫zMS^^}0\-;m06*j3nj3u|)l""N K^3dނ qO1Ęf)| Ӗ'Èv>lT}E٦ޏZ|xB؞>v /6pTd@!xZ4iСwa&yzz^xqHj_suPb!bV6mMܺuTnww"׎u ~?T5"!b {h\ws~N^uow N>LrWp߲u,`NZf<goJ(>e.ch>cLpc V-cIXVSy@zc=]qq~\+;PQ`k6[`j-STx6ݏX!_Jܧl j _w֥z\}؍V(m A'^{vw .d1I}6p56OK=x ܷl}$k90Np~0pw̷3"Ҝ:Lp[X`><jvy! !\D30C,30C,30C,30C,30C,30C,30C,30C,30C,30C,30C,30C,30C,30C,30C,30C,3 h4ھ0,Ă+Wؾ0,ĂvIvvvs8 X@Dmmmmmm6 !  4u;ew\D30C,30C,30C,30C,3Ă0f_v{ a}N0pk#[O|uy})e'g$m}{SE|FSWV} Rӿc_NI/Wc[jٝh'\`ȥKDo%Lp$^j9sMdkOKSfpnVwHoUaeDDd\WOaY=aEe0L}[#,"o~<~]_|uo*`ۚH’h*ܿcO-:c盾#yUa`qW*Pt=䛦y쮯&Wߥk )îT6?fC"Ê "FkBob/np4_X@D=Ƃ*3eo- %[$&UqWˬ0eRM"Չ#"lUҘ3DDd`D|)ܳx^/!Ѹ'F~`w%j""{̙OzI]Yx`Ov = qsLy'cbfH}E+9gF">wfͨK9k(ɤ!BF]#?}Dӓueޞ댍Ey_k#QWߓWcœ+񎜛-d3i{ bL2ef+ '94V?4@1tuEӿ/HzinpΘC7}^h[ņy^׷ky?2ScSy/ N/Ug]c ?*;QKWNx`㯜zk\"ԍg{ıxDD''_ṬlSvh,ض2=6y,}53#݉;=y׬* iT[c\."$_O֬Y~V4c^+>IjꇟÂ[޹&_f:$-;)uMꦣ9=\DDsgzd͚Ov7 ro-uaI3Df{5ٛR)#$-0e8cgz?~ɮЛ_V1ӵ$kacDT+C&ނ0Qgͧ.ضOP%M?T,'"❫W;1)ʆDtruIISU_>eIQ[/&uU~J*b'Ol* JÃwX['FGD5y0l4p!ҔeL}b_}._\RCD5ӃDyyddb ~׺[o]]yѮUfUH e5[/2:XLDAaΕ;V_WDD5m52D/xSuIŢ2T3G~GzuR99q:0f[}!!vd{ ]֧Q%~WvM7߭:ĺOI[DZnǫ3֊ŎûB8QLbGR^U_?y"uY7cj׀RtEB[ llgscWHl![63ܬqpu 2b SsE׭;AEKW({EU%{ҭN ʼ O^6ɋdV"[U*jV{yUĎؙ B^'pź: wwׯ#/皷;MZ`pvuдia4P,$0gga[MHsv%/ mܸ\]MmDB/$IkXhhuIaHtuBg6ufgC48=e˾\.y]1Wƺ]b?*y =hR.j"MY^zYEc/;nγ `‚'"} d a|N{UאI-[JZA'[jDW"׉^m#39*~g٠o4!uEIK1FMl6[8M&"R5FGABv*)*s:s+*t~$,1+ 1Խ=yӂ3U/ĝFJCDdrk2X[DDN0D1ݺyELH8a7Ri\wV"1=Dl\%-[keWFtfҵP"&Uq{+מ1rVߘU[:ˣ0Ϩ k:E\&F+:,s3$ IWWvxnc ;?e k, GsriVebM)a2?9yyl롏npwYv}7&@Vj +5!7|uߝ[}e e_́j;{>wj}˙^ wA_̲${ ,v \?7c3];N5 ܇"ME0C,30C,30C,3 7?1r0w` ւ0 `Xf`Xf`Xf`f.D4|pPf f YZ*իW^5 fggr v~ns8;;;+d2 WZ@ނÇ;;;s8{ 6 fggz슻>={Y,ub~O!a`zc8^7 U۹\p zIG1۲&KY,֐,`Pҡ}uU `H`Xf`Xf`f.Ýfw@V-ۿ-'/>SO4d}90tf-YXJ z]c<'#B;D# VJ:S+Ks3L+*499tWک&+*x!VT˵)+(Z,ܲw[)'~UtÎ&rdV l5o>жZ5bҞ]qĽ)e<廸SfM| L,u4Ly]r) n^bBЦ]!M/4ڰ%3gVdö[* r[DK1Sr?DRӤg s}PR"pX)\cC>f @9)Y43E}sY"ri&/ v7qA.1c1˶ٜ5?0.Z*4$.T|0e}&vȼAśv I~)F 9=``>2&텼p?邒jv KmjG]ٚ Y-f<^֦ &"&/+ #MVS@\BLDȨ9{s*!mT^tx ol\N OXLxR󜜘 ٌڴ(fd+X~qsbIɩ'̎$"^@‚9a.-򬌌,ySXbrBLBd'=ڱ@@ܢi#(3]"Μxѐ¡.O 冓d#&>CD>SBm /_RVĹU͐Y;vg l{֢kӹ=p,%yHwY c X~V$0՝G!)~NkiZ*.0baP]Rk!gLDNPH@D̵K3fJD$/ a5ƺ(D׎,@:6a@MJ7T-/5޸Ԧب" +G`9|&'ĄILKKÿ,%ĸUd|Z'"9{UF.0o~d_uyEiIO q!FJX8K.IM|/ka---|͜9sK.ȑ#;lip!6ݾ2fY<'m (ܢ %yJak!*u!2-+(*S%D4s Ƞ"eVK \,UqmaO/koߤ( wr.W@&^S{nWχ ?WB|nnJG}SGDDMťڀp7eqb¢BxD,ɏtDu3/`Mq/ '&yA > \<ݜxDDlbC 9>$T+d}}?,ȉM$>o+5*m[tjo%2&DdqI l z)E/yL5bkpϳ/zq;)|M4diQ_ eДeCq_&leiC J ]0{KuJ`SB3 DdhQ;㼒H[W e4f\;kTwOD sfu9c7\HÔS*~ J5>[X^UDD 9EsDEE՚ "j*)$%4E %Y77Y* % S‰H]9O&-u<7㢍Kp*(0?!Ӥl!r IC-ss"(WKH7&Y%GL^mߟ/7viqtwnq}c%DKVsf)ZbBFٌ0'=Ew!io[іp $ARn(uf?PZzSM<@ݩ@8't+[~0uh ݆v5~:Ӂ9bn_LrluWn}7ly#B}N-m-!ނ  ݁ۄ'(r{3>! bY=իC]~C,B,hkky"h{#axϯXp߂+WP(dÆ ybW^z`襷{T/m7Lv>˷3rJg83v,襧dPn0g DNquz_zzzsdݶL+Wc`Hܙ7?uCCDLC    .][n}M wq, W?CU{."b!m8<3+Q/OUh2EQ4V*d븙ByV--ЧӟU "B٬yV.Fʥ܇l LU&뗫/>s 94֟ Amogĉ݇IcG;_ޓZhX`DW3G͆54)n=0ϟ( [,Aoa,hz=e8ZuW_D "柋I`0kp;#pu'oG "X[+c܃e  l9>>:z M9hU]o&l {Ջ{K޲XN̬g)оM= Z_KҮ\Ǔ~7l kڈأϭxl,͞B5w̟f=*Ndl8o+n֨/D㟚=vi?hX\^W]Ï'Ů7 /Go~9V瘊Zp$Lf[._.l7j)9[G6m9o ruiˉB/"4n~6(p Grrd-?@Ӓ+oyҲz꬧ZVN%A+!3ƸI[\n94ոɳ/-K;cW2k ęqmdy|ytjM|aMIvf5N> n;9QhGyx?W}ALg#iʐsz 9;;]|fm4gϥ^JHp}C<~]LLYSdo# …iE_ϏDz&L䗬%";NDHPӑZ"̚H71=ߺh$"BY#?o/z`kry+D.ȶͷl#˹ߩA:;Y wqO߮0V1.]P0`88Ev@ ]DI f/_y*e\-vGzk=p<.e|_ߚ޺TbH**pv$]CYK4\i[ tEYshh857Z2g6"w mߝye"O.pSɛ$nf]gsyFOD$Z@3Gòi ) ",#2T .%y D?Cb8DD?agr.P#DDQk n"[j璄}1sYÃmDT&/='"D~iXyl"}B\_Zy{ݢF`=|}}m$'G:}鷝߷vl 7~l^+_7?qfoyuT LɆ :N`o l`g"b!b!b!-cQ3]8=8W Oo\*<3k=-ݡ?3+NljO/NN3)9{>m)ܠ5G'8x::okߊ8dl1oEm~>nO~/m~>n ڰ"; </x ;W:?vִ]DB /1S %;ߗy%Ӿonl˿ݟ/|9{ɋ&:ǹ?}ŷUqLڼ|W:Xko ج/$7,G<~lZy{~~W'i#nǀ?8>_ i\Cq Nt":yT6xŢ}i rh~.c{UO[v1~YV+jd-?@41 J~ՑOD$^(D^+>/e+VccG:Hwl_C_V(ӥ_e^eOPubnitPK{O,S)Qsr_g[DSʗFG]CX\4ujXfvd:BvҸXkٍ$*IDAT7X_.s W!Lv2m£+Ƶֆ3 Yƽ\^>}_R,0oiom gFUptۘ=^UیU|-ظw[(2f+>QѬfÀ!}ZDAz)R9Aչ 2Ҿ?~H?~鄑"IVHZK<8pe+3M8|cSQS$w)7}KJQULO:Ѩ8-ss'~xl;q{ۜ IR*ߚx`6K*ܝYfЉΗj"/}nLEAİ2lkHD(;x洑wXǻDT,3f܄E!bqZᱩ"Vm^QfqڻxT" ;c)n#ub/~YioRYԮѡ+{Ǒ&"ӹvVE_QہZÁ>,Jv쇤Ņ:I3F?=с|b߾wӕ/l7j)9h'H[.D1>9oQ˶QIH<@ę1;-IrCdcgsjڈ'o~$'rVIe J7,ִfLהdgf\sT%eJoںOe6SӁbWCEf7qݹ;M$gUVLH۝]f"r `vhgOK1Uv_RܴS%A.n#MJ_ ; >ЪnbpڞMğ1qI,>NQ9|0!i|Ky[zbI^]4:W~@?|U7MW+|$+{}djC_p.aԝ-~T2;+UED1&" SdD$r:[RT{^7,UaD꛻\͓,~{DOA'ye({3 saqd{/*ݝyא^˵ԮZϗsˏPZz=ٻvʫx>ɡ1{DDÇFGĮ^bwv'l~T_?eHdl.hZMd?G={܈=1OqHvO$]x笮S' tbcyQ=.-O[( UuDwMH2!en`:_˥>eʿgPXK/^oa.-4/ O~onjʭeWX}?L::Nq,TNO3󗁝:ccKڞ8ɥ!'@D&B>!3%H`'T"QxC9p( -uKuklܷZTʳ )P +޹v6ahV+MDDJR9#$E."";{F'"j-{OZ7(K>rv.cj"*Eqd4.,u6)oוƄgo8Ddj9z/L:JGDT !hU"Z5CB{,;Hk۵SVOAwִ"ZJMsUM;KV+TzѷVUI10W#ғ2uY9=F*6wY\e!RFHU3*U:cEXڈd"/rLd,0]vq3 ~m9D7~;&XDʻ^DZd2,.Mc]>S晿i{#m0M2evm 9E/U;FT}}=v[;n  e=XX^Sڢ>uq7DܺWth镙%uzkogx}cp(`:8{۬[ߦ~pܝfQ I^ޝJUΒsgjGyNMG}VϚ&m5ەM`ax;uY]<&vuv$MP~~jeKݙW'rG]}Єŕ)jyv+V{$"@i}_H#V[ۍu|<~΋}[ udscl.?$sD.44cc ]9ZVc8wm;7Wٱ^u+:QuK283)B/%?2L$6"F]^~vH$c2&VTzD>ۓ.b2~cv"a:3(E~~W[C*_o2~Y,M zw@Qu9,V0D$q]%ѤƖη1{ #d"2;LJF,LsY,늈ɗ抉$l tQiaw4݌L5!ǿ.^!1"JQf2,D!"'3S>'JѭS9Mo{Da~wI +ԉxly1qzWX!>iނQGM*ƈOvb޺ڶ5\]oFompI;Dls?;?>rHOWAWۼņ7 ũK > ~gt_yB(^$kYm󚐧(}Vlt74W| n|0QubJX.2{\[Y]=jwu`W?0ll0_p` kU,xOy)-/py9Srs~vl7*oŨC>ptOjTd%~sGUKuYWMF$ĸu+eiY}r*}` IU8#$ޟDD-;<,ڷ'i W0C%?hijwpo챤HxuUJ~!I-Y^IoTdtqmY/S[ e:!pͧ7s["u Iŷ.KV8;;̛_]o DQi\joxg" |eƈs7 ^.3has<>b/LJ,t*on>@[9>r>,X<}-qP$"lyV3mG:gI(tlL'M9$n:#ϯcxXMg w"gN`@"=6A,bp X 8A,bp X 8A,bp _aKf&d3IENDB`litestar-2.16.0/docs/tutorials/dto-tutorial/images/simple_exclude.png000066400000000000000000000451311500564371300260210ustar00rootroot00000000000000PNG  IHDRK YM pHYs+ IDATxw\gBץޣ4/rri$^r%1F5 (Mz,e]XD& 1;gf?3bw \Afu l6|˂ 3 l6ɜ @iS E-r'Ai b(mA - !E1 "HAAA<󌍍 '''CCCsR*\.3RAR)JĐggghkk+--MII֭gi{g?/;III_QqPPPIIL&벌ðι.N>3fwqCԎ=#kAfUSSH&&&dڲXM6۷ظkux555^^^=xWॗ^y΍d[c-!!ߖH$=G}gϞ,ssssrr>%KR455EY[[s=7omuӛlllVXaXff&\… J2))i۶m$IJܹ ~._ؾ}{SS˗===W^T*FV`ffVUUu3<'(izJmx&&&*q;w!{zG zwnr͛76mzKKˆ 4 ߪh4 ]j'|rYP՟}YLL>F}2J*,,xwشݿC=tѯ?~K(@ pwwwss466xiiivkffv055rRTT'Ojvvv-<o``ð-[XXXp8 úҺG \p`vvvMM -[ښfTGJDwww!HN8 IIIJ233S+X,NkkkKNNh󁯯odd1/mll EeeeVVIqƍ>}Zܜ i|Z P)}of7=Ν i ճnkkagg[[c î\HVސ#w}srrfo8740 q"Y[[ fggGGGG t:'''''LZhllvpphlltrrjmmɣŋtZ 8YQQQUUd2CCC/3226l J;::LLL<==SSSX,`BB $>7---oq8Xkkk=.䔒gbbf͚~arZ[[Z6::ֿ{⌌ D FZė.]jiiD2`ً-Zh$'xb ALL :u os]NNNc?Kȟ)vm۶w}7>}z,VVV  jZHF_afaaqCNTB}okkwZ+++PFFF02l LN&.;-TaѧŪGJuuuKHH8|0B-((ww\MMM#m@.+ iK$_U_Rtnnn Z[[K鉉,K,#%tfHqFWWזv}c*((hdHґNksƚv;vl߾}ٓ7@M666oYc򗿼 |>˗/'juw0 L,Ҳٹyߺx.H4- "##1 377g0111w)Dᐟe˖pKK I#i;Bb322/Ϗ$vوrZZZ Q¸53ki}~@Ν<Ѿ i ߵkO?oΌFF`ii?)؆@ ʊ$[;Μ9EEE6mZvg}r 9~pT*U/T*gddD> ץK&* A:$I}GDFF3ѿKkk$;үP__?|}|gh4' IrfZ3֭<۷oe˖M>MLLLIIɌCXf޽{zꩱQ ,$ɑHXXN{4v3z``@$_NR;;;IпDPrss#B APFw#b;4.F,jkkk((¯jpp0"":;;]\\F2WCGefffoo?x ggg lmmbB~c \ݜ$mx@JJ NJJh$5ѝ4΃O|xdƆ~sXVc;mdee$&&899@{{{vv[ifotݻwKgPWW7P@@X,_Cs7NYuӲw/]ooݞ%$ AC@i b(mA -2OX,0{3E)ggg\c`9U::bߵfq]+j>>1G=PѦ~2o[yow}2FȽo=g͡QtLV_}A,wX_1V%w6+ 3Rsnr^۷BNfe#_hFN z+9+ .k'O?H-O;-YGmpSu-z/ZSOt)1kto};ك"w]hn 8=#K .UTy9E4_b#ArfL06a[rE$5qY%v^84SsX=S^iy'q=!u`ZA\µ#2PcC;vD})5)gyo]s+k/#F;Ҍ-nng´yA380dD>jKeK{t&o=N~#'_.ڡ)-^s34S=ZA቞r-6K|xcS;krNsNE?N ~͋5`G/>ڹ):E4>.Oܕxp0J]Ͽz`nzfO5^:9/n!jӶ1ƸOGeL~|Bg+&  ~Xzȩnv g3Z\@p0Z2q߲eB_;9;{6% c5=mLLx%ݵ}ESK;J~yV]ό%#zYPƼK ]&ua שrۆMkj=ďG ys$t$ Dju*qcOT2`14 j;K?feS\<݆70&zkjnLk_j\.'[<*) :Bʺx&eɮM7 @wZ}!ϝ\vg8 I(J3.9N|׮_~zBk9?rZ~ xE:1_z`ҙsWp_|l$$.tgU^I9Y%aԴWCUMA,WнhFvz8+57)M E=w:BْiN}T7TţzbatE9$~ëLC1yUZ s2wO[ZHt7sscgh)sL F:S[(E <<&xՊe@qHmG̙]ёW9U9Z`?wɤr03T765Ƽ%/x籏?[32m-$IdEpѬ7lԨ5@QV!ܰkw Iszx'Lϐχ-ppP)w9v^eŦbSi 'aGO`F^7!4b)oIa :ph5utMpޠXZ8+ُ{]5b0njOJKoVU\Ip>\ pǀCRWv/7Kyq>>F[j5 09fЛiH(J>xUw (+CF17 ǚ'%mS`\wkֲUA+leU62̴Uc-,<=MqK;zǝ4F@So+:mԖ3{]1uivU_>Km`xu$@ו׽#Ɲ+y>aM,{""w{ #ZqTUgspBiɩmydxe4=$ptߜ(JXQS9kokq-~CX=.8Ň?i2-+FRO]WP"JX*OשmC{lt7t&yS4-=P&[NK@]W\Mٗ}!!33i;2St~JKƞq10O4>UiK*{{D -?C nFT@05?_9ts-ر>0&F2")$jvkv&UQpq]=:UާogFt_Y#Ӱ=~݋ o}^8Ѭ(_7[9S>:)-=r0S qb $t:*1vi^j%е9% [E'^fCO"X/Tvv_ISՔHQYqfS8{$PL8 *n Wwt%=wᆵWGL`+u*`i:m[d?٠)J 1V׿HW-c)4 d7&!ʅ5po4@tN߿xu*,U'~lЁ,c8RVvܐǣm -oqWIIƍɩ萨Ig'R_E{R 8ʒӅ-f[hX&nގTuGWT@L^M}ItIlJ:4\J"wu} Np+ґQ/nMzm2~`ڻ1J>$ŕoQHMP(0|T]w`JI;_}7Qr-q^ɡO򻁥e } eMlaD52 7."J[AC@i b(mA - !E1 AP"J[AC@i b(mA - !E1 @ 2 0[[. ȝo_ʱp8n> d!o^j_ $ƭb2226LQFq\q?sU 'Tordq~Ùp Si}nK5/mTS^Ms9K8?z;گ AP"<8E@fJ[AC@i bS?݀55eiEb% ilD4s] a4)`ׄIU\ka*ӝ=xν :=0N=zlhhJVp*KI䷼?+w='>>QUF9|7O 3/"M.smQܖ?ٳ/.M+n߃ 0l)Ihe7loUx^B e~3-GVFtE+=Rfư]0ƔM%6~QN~d4s_7;s.TJzJrڛ^ԟ%:%<;Q{'YR*=֔^$|5g*Ilo`Vkў"j5$@IIYu-$?1 "j)ϗBRq =/3čYQ9-]sݑ9}0ͽ\,LXB ګ**e^~N6& U5=CW 3KVf\*UPrY[n3x4i1;W{⽵9=R-YڱLX[\5٦>EyMs]4=lkKB0l]}+rs][1L0 L D}?uT˻Bs `shMIfHjj===u>ѸX[Yzutv'X .]l@ASUA҃禓VWw,b|Lڔ=.=>.ʍll4#[G%5*{=L1]lљzi{$ooEF¢SFȜVW:;pZ0?. 3yk'4ض8{6uTJǎ9+g$6zRɽv-99Pŋsj`ف$U;[+)Ţ1nЍ1PZw@Uw~1QWjiN"ccit tqo 7]hoơcYkeFrvDz'O_m71J2<+")ܭ"#%A|k@s\@K@\=D0CH\l)]'n,"%0KM nk ϯS!fwmG m£YCg.Z uW Ȱ ]l$+rs`Lm\nfurQ{MNJZP 5jeji[]Afv@0Ҭ}zHVx淢^f]3\Ā@f3x[q_+$.hGLOsNTil,MX *=E G.Va>v&tUHLR8`70'x6U=Vkүd%.mnm]a崌Ivw0(;u~hmb s_jPM1$W1$ĈMõޒJ9Nj3jJF݈IJc|=JԄH/8SoLc9:H\dtcJJiOcѕ.^.=6 1@ҥc:9[c:Zc–&1qW㨩L9Ay[ݴm\9w;-yW,rZq}Il 7vv&3.\έ}ܳu5)heXNVY6T_2w9Qn޽&\Zu)=IaԲs.絃cM1vfXypDU9) 4vakcGӒ/>Au4!(RZU38X[Ѧ{fG1Y,6[ɠ@J/^jϸD?.TąY t+ 7sr1s2R/_qⷮ 4EZu[R`uΥٕ,K 3t{z+t ?#u745<=lmt?;[Ow~}CC=k8`ԛHb #%+#%+=bDɛ +pD/ьzZ@o}INqAqS[f̄WYG-2c^_ ΖIW7)vvHUoIZOB[MwM't'%YW_VWP%o$9Xs%uxj]YKc7fK]ܝ(~b9[ Ջm sj~/؄BѸ{3 WtB()k;<.)SC]WЬšݻ⣊R[UN& [n 721d,Y䨫?u;_R{q{Rޘڨe;i7<T.n**з 7[_~ll/==t`yc{gmR/#IhnVyzqjkCOƎl(ȗ]ҪK>{,[SGYf.w&&4RQ_M@Y;owHJ2<"%[^%=Ƕ8_^\+"l̊s BDNnD"Ukd0\\z}W)zp#U|G]z:ZF:)AvٵSZaP)2`L=Ti=ifK\͔qHB\]TMX݂_L۰u[".mlaaoƥZpm#uqcVuL7٘Q&,6AhNF;$U) b8!$ffc08frR64D`l&zkjS-z'ITJ&8lKekR)i1;6kڠ[/u0bPuJAE1$6麳]bWزƢYuڛ#߃l& XwKǕkZ7+CH@GK˸9GJ[^tyo$0jNJKR(T& jB3QySb--/6Gp[iZFtS)VT*nf^T5.4ЙL6 "1apS%N9k/_/8_siL$1) ;72bcP$n]BZU18"[~Tx::[S؉Vx+_1OƈI+}},K|==T׋|jh]m7B_)9H r99cY8T(IuKy /(-zXk[oV!lk. vHkoT+Z̞3cr8TR!#SuaXC̎A%AwYnD,#  4l*0rN޴?mjMAhIL2?ǰ`1#eqFKC-?6jNF-KPe`0.sgt!LF+g*4*}oOb24Ѝ}B]]gee {zne{t`w8JIcA‰uμi-Fm=a`ZS[ў~@n2D'I)a"'r}mLNbwA䊨r:G}0 #:wn}g͞o mK#G n˫ۄKK{—Ev3M+hipR|ӱ liU{ysHqYeH ~ S(plmAp%-RH&1=kVQ|eG{.ys{z;SUM 5 pk|$$:|@F/f%3$؉hעoº "@Q[ӭ?N ϥ8n_~nrL\9ߟ {[lJ% uV-RDGq Tt$9ٙV#Òj4$/ڸS!%~&+$Y蕨eh+rfsʪ ZF;1~GuG&hpam2DJ=,ndqBu{T.'!SSSfJG^7foR)*mzKvcYZ.Qa,3bL?etWT/wImiTڃM$F@F1๲[_\*'( \-ʁ^!P23$U%]սN!FcQ*K/fqON]6%P"Fsmg8ЕM2+[֡C hF.\9/kslLf:O7Z+L\8t*$}粲 /z ^ɢ|E4rխn/^ԍ4HIUq B8o}C4ݹ?}[ä*Kj8zl(>64$ޛ6])5yԱib9JY~1-\x% 1 o$@o"#|#YTR-t65q t_(/_I[O^ӟHiUzʅ!%EVq |- 2N×.-YFj T*I%lx31$h%-IIƠͼC|hF6Uu>3Y7G0_M[E(0.>RP6i_<Đ ;9'nϨHqAxRf5H]. VC T1EMA8ėE!ri_KCu00+]TQwAeUOT a]so~Rk]=nAwtbHY}E]mukBZwdsh;w4`O; vyA H(Sޑ!u=F~N gW[ǒ.7'G{.JjᏰNp9YhB!h-(Ln jK=gbO򶫫2Ʊ/d(g7-1=VhYzppV¬V`& ڿR`Ob,۔tWN```EEct KPV&%z CRk:$Bal q]Bc5%KǍZt:BBB,--CC b@(m'G%,DmΦV/̉ A_e]$ܶ_ $n'/ mp~B6 T w)F }pqܠ}AdCϟR6K8?zDáַ"#AglA 'G1ԓ b'AP"J[AC@i b(mA - !E1 v/1Gww@Ǚba،-! ~kT8;|ݺe@Y>X f8m1 0{pt 9`Ҋc 2uY$l?܌ԝ齆f F=^NKa"]Ty=P(`Ɲ r@\y;WYD1Q~KE*|GwO"VE9qGH&@3E<]fO_/.}UT;χnXfhΓ~,V!tw Ο#8Rսak1 k4;KhB#,>W1ܟHxI:ӏ֧j,,: M(F^$OrqyCVE_}~ϾWOZgt.᣺Zca iCky]SJlHy߮KŽ-J}psM:սg Orֿq:GNd7^D+2O9U"3rUˢ,Yk/ORW.$ pWO.Q}]kZg]rY6QG_f#TTjJ*ACyx̸kgsa}Mj{D@}Qwm:՟Е\J0Vmyqpy7Ouۊw{.=S#:s:g ?u@ J.)nb#K)9/m\#.:"Y*-I]x#+mX5!Gf MѺ_~ W $,)HĹgR%m*II0L1[Z-{* nN0 F9]@j%59UZ N! >֒^8!p#rzhب% ;ݺ[@ɇ-YU`x$zp /MB5Z̅Ҫs# i uͅҙ$Anlm5=I|.q;VZ&8 @$I$I$52g`T]vZN%&E;1R19# FfbNYίUϑi1PS-ca͑^(Đ1`t sC# IDATG?N P\cG3Ncn8~eŠn^D2aIL x@|[AN jPD*rgf넺OvT w9J[@+1(YΌζ50h@7\T4Ru0qVq{E35青,ْo L&~]^+ʽ2tskAo fV[]wjU]/rb&CQWTu-}QW,ӰYޖA xk1 D){I5.IJTۯb%E<^IU `(䳴un+&:ZMVm~hOВ!B'jH wnŲ7_sEeua4>Bl bHs :r>ZKTR7P=/˛P6 }iU6W^o\#r˩[MQާo?\Ĥ:A-ݡTΩ0& _oKφn|1~E!j-L?"T&"2.+CN:a]AC@i b'APAP"J[AC@i b(mA - !E1 AP"J[AC@i b(mA - !/ܙpg{8xZpJTYMyErwp;'"?1i" k4es`.НVwBf͝He[/w|`eeEd>ex?E5:"#2fZT@R [+:abcs :Iڃ)MS'%b8mqX[KRx69EF-Q]bo)%LukI/!t!Zԁ L.q;VZ&87@#R.֌ad2v8Kuk}g߶ԯm9Dk6KXQX  SV[Avt.DF\VCo- MbKn}PQ.{i ާj9Iűkh=\uqvlQlFpݦ6VRMݹB_әzRYs d}uvo•TCGWe*ߙ.-24bckssvӝ'_|)w gmT}<䱝5{jnkݿ gO$)ήgm]Ru{KpKv&?rznox{=u؉\OWLYdcqt~nGm΁ƺF{z_==[Hriә W>\kcuݥّ}N5<ۑmRNWl7uo6=7[_~XqҭM]m=;~?n ^C$I_=<}goMtmR쥥f7'؞6kuG:3L64gk خKu~Olgj Am"-@ DP[j Am"-@ DP[j Am"-@ DP[j Am"-@ DP[j Am"-@ DP[j Am"-@ DP[j Am"-@ DP[j Am"-@~y 'IENDB`litestar-2.16.0/docs/tutorials/dto-tutorial/images/simple_receive_data.png000066400000000000000000000653701500564371300270120ustar00rootroot00000000000000PNG  IHDR|ї6 pHYs+ IDATx}\W?o%DHbD@k`A$V %jŢU ]ן轂-Bw[xEVTR<B޵@hlJȀHuWRCXg3[+A64LDHB'N$xxuk->SOpBɉ nM;]r…I3zJOjaYEF:3W % {%{FxBډ.\Pr:3iOg{J<]r_pDS5< CCV-|Uo3*n6!U}xJy26{q^aݒv-f؅``/c l"ږMx.Vv,"4i>ݻ?<^mZA&/a( TA|gZ؋wߵ\+>_W{Sꊏw޽{gGw޽{oQٽ12iҤ!.)g[ JNAώܳkߘ)XvtR AXvٳg߅Ξ oÕ*s:x%.μw`{J?l.NH?}…'NȎ̳➁CȎaFʪaCHB钒IBّD0^}+/:@EWtNy p,Qޝ$֊0Bg@ aw$9+}/}2Hv@Ʌ{ gNKH>[rbGkLSw$d>[Rr6;%|8ԧ#"'##-_73CBfiXʨ蕑+ Ϋ6rb.ݳo_bLt_W یjS3222rp_&MZv)S.2eڵkG-w>3 #潸,Zu;0~ٻg{͋hQ:zTE$0|ǣ͛'v\x |f*+33@`~x[6AE^X_1sg&/#e-_\y{AzcfÞy/oX#*,{ŕ6ݹ)ڔ]> |y7HiOm<^2*DĞj|4*`꾕ozZ>MK{AL/NBVU\f;`{3rnBkUULbBw'o\¼Qf ]ʞ # Y+/}׶|U{:V`Y^ypL{'p!⏹( `t$I0$yoU:` V_XhCg\=ǯT~rQWu޿#PWHs'|1}z=ZxBU %VZ:`~v ?{6;uϓڛ;믿z_; a{MSIF>^P]a.ARWT>nc=[lP<;`;pՒ--:a'm["d `'mQ$Afc~[uDwv~Nau-u`0{vA Ztqo]l6`0 VfԆl{M^g`#1.LG)tfk{ՁN'v<6t:t9ѯ{18b bԽ7wF?qFAssu^>+]^ݪW+,ԭSkA 9 7 QȨ=ۊeZz RVdF2o=gƠ8|B,n!l8z"ruìY,bL]4ft߯YZ/Y,NUYㅀ5ؘG|[ll+߶Uj#! _1T-NL];l.<z K[sP8$I͘ig>:` f'Xi moOV%l q | 纯||x.߿⪜BÂM||6m]+̩"B;V` ` C9xʜ}6t =cՎs_WNR3E@g0B6}*V*uVcGaj4_nh=upp!ٴu0xaM>v[s3Y,!Ysal٩Αwrc? ]&YxӲ 5Bd4=xH1s3 Ԅ0d,О9w>psGJIɟuݕ/l){p׎M֜wݕyrAs0o`+nN+ R,۰vAS-N![,5&!2c e++?Ğu>/ݷ;|pٶk‘BR:!Ja (BR:!Ja (й{.@N :t: ~,^< B=,ϧ,sgF.;޵@h< :WGG9zBhW!Ja (BR:!Ja (BR:!Ja (BR:!Ja (BR:!Ja'TgMAbŊat>\Wǯ;:YTjnƭbSr1i%u`ll_[Z7m8rD=g.+;7e^zp=MVWuҧrSȝ}JFX2!ai!7/@"[8}wUH}KHQٚ숬$ ش2H]T>: hh0[Z~ΆTC2 ' _Zڗx2cVaK^7y:#nS g re'qHTg]UuXM*9Bd,˻ bt UP`G?S4mtzs:L19M͚FνNԶS )`uڗUN(>v::3<@Qp2A,?)ͯ%P)t=gwf˒E^\II4@(.6Vw:vb\-/1ǁPS|=PA7psʜGO❫3PS(^d1hD645*{/NvS]dŤ:{dZ]xTI% ,0N.ś#gN#.:-/!Vhac \%Ga ͤQtzɼp"OSXR 0$A<^]'3[k%)Yȝτs}S/x 7V}@Ǡ6:A\'板n)-`2(n}{y51a. $#vɱINIvUif.0TC*c8S7:9M.go`O[;ڹӽƒš+B.~| _3xG髻nmJz0$W.蕲aNA3i/ihKi=|Iduz(r5dp8mw#SK?M۫WKR%wi ?ܴ?"X\&w`I*+9K!/5t( b~i^ &0x@C TM5@ŒN\gJNv/,H:1E'S$Y4LL~c%왝m*0A=4ЗK m&k/^Sڲt: i—%N(nDg?w 孒nh|tƜ}}4Oɀ}1ul-Ʋe :Nqf1}ioD_';V'u^ߛyL&΢tXM|J#Zkz Nj1G 9=0@*J |GG#KKfn`,/8}k{X, k'S F!BK%&h4_!^޺Wd(N]mMIM6 ڮNLD=W9&ik&eywG` ŋ*y͠rIE'FL&4OԻ;&˞_ kCcTw68ܓ|Wo,KӘ,8HhޥκN Į\)譍1 ظu'i2$:n3\}S:mT՗8g'ݽy{My 65Ȣ|βw6ЄhQ4,#"@8Dx>,+u$H\JJ-Eg~:@C8^X! vR4'KW4@“f4uׇtQvau?h&\:zpkϫV4*˿)t]N_jYZVu0s1ڤɐIVn\z|(`eÚؿ~Sk#w٩tQ|7J$۷^N[7n4Ɩ<`w&.{yΡN48#G%wH͍7#  !D) 0tBAQ C!D) F=D 8' tB"(#0tBAQ C!D) 0tBAQ C!D) 0tBAQ C!D) F8@ `際[ZZFbc @z;N7c=ұѬz{bd 4y<6OH1'KҖд2io`ðCgٓ'O|ɳg*T,kz)=5>!,s,;K0uy `Baɓ/DKeiRYYeeY^H+S(Y/(HM͓UVICsVPPTEyUV9*8("<2BQV BSRd Z*KOL*+eiQAfY1e8Qo*J49M.EO,+JsFrya2YDPL:`czƄy e34(%9DdUGxYʴ06st^YZ]XZYyBU*z VfEA"(db$Y*37Xb3`r鉩ye~>iyʲdIPGYޜp>?ߜp ,{>8%f*h?6!tx#FR/rQ/^_/ %A c8Y|g-MOJ>"ǂ |_MՊb$iD,e1' mnHX,߰0 IDATEe5߫ìS Sº%߃_淚E.q%؅~f1"VZ:ᗰ?}T {`Y_rw)VJq c󵥉 "қWFk^uw_n1o P%Ջv&ߎIM,/((ՊD_e\1HĐ-z bA5EE5A<B?6R/+R )[%׽Cdw(α4vR1qp]Tfpq낼&6'l {i]?&fpN`/y]9GO_:kǸ+dNֺ ?׳ =%_1޷cTo$/r_$v1 vPhFֺ}_OՊSФ:)_?=oY]{q-g|q­UV ~Qz5A:1tv\ Eyr8NRl.HNHQe7@6]\KD5iy $YɹZPp_g#v/X_@ِV!6?>?]seKsH ȦRPaBORZVj0/oΊN4`00]HH8R9+9"'Dғ@ `9e_`_^PJBCQ#HD lG \jd:Еj\ p PegtP5?H"f䧦eesʳ2=*ԥePS )7pZTEY ]g;K$dY3I>I+;q}QL @* .8K$ $*6%~NX,S$HeO`UҐU/ '`I*+{ajV0vWЬg4ɲ$$x|wt!v@)5?`8JԽPW_ ]|CeB Chmj9(ڦ&-G`~{^@%pdks72UY}w6TZsywTRB?ߠR5i90 M[&TMMh@-'EfzM~bs2_]P)/E.xaY@g!)Ã-ے';=d\$k w@,Uyx< zt1}sTeMO78~|& /JZ 2/'/ q\\ܳ:*|z1z:a.j&t:hU;Ԥqp NCYV#䙻 _["RؗV5k9"GV1 eq~q&TZ jF V& ^})"4+NX iIYCCsd?*ҹ ȼhKȓ$|,U]j %Ț9'&, @P×s"拕"Vפ4$GZC@3nT|gGI \ZjxN8] z,Vmʍ]WַkRi(HjةHJ-I8͗ZV+O\wIiwdl4˝&|DPLX_9:3%0/<}x3(+,! 4!ZT/u#:!ԙ MKG:_*wJs&pKv)[:RWdǼ!2wn `0x~oGtywDw B!ɶG(Fv\Nt!ЕE@^ 2UpdVsD##z ze rHz9JGyAN4\ox~1qbmTzE_#RG!S  Ri`7& lC_rS)cxގ6 Y$-rJQiyi Ew6a1#LU_HhKO{=/tZZZnݺ5أn꧉t-ڈ#努 .郾,}VP|JOnrb>RT}kpeC努/c yMunrVYBq2NP/"ӛe;(ғ_V)?6#HZR +S=sUw'n~;~e,!ҒR {:߽[.^5Q @|||p}֭ݢoܹsNhw޾}ro6EOhuM.;qāOB%>B~ C!D)!D) 0tBAQ C!D) 0tBAQ C!D) 0tBAQ6iҤBwvBw/BAQ C!D) 0tBAQ C!DA|„ 'N|O#pY &ܽ{Wz+4\7nСC!4C^uƫ&|Oo"У:x!4F+}j <Ѕc>yzɝ/W 츷Z5C/o"{ހ?Hm7-WYިlAmYVU# ˖w&ZcsւP_T{e\SY[WWhʹ23zVV8zP:]׾gtuO5ռO/nN|ͼ~i{vXTʡ`%Όޤ{Lm]o ?QLl'_N?mVObw+K;.kN96|ں.{]=I+h_]iǕ^n4b ]Fn9uUw8g _0Cy6~~C%Oh,t'9\&3nkƾV\ԵuzeM] wCO˾7煮>0V7tlLA8/f_*%l1Fdg zw+>|]lEeW m @˛Zx<`MfwӚ}S?NquOvm0,(滖S+Su mc7Jϋ{7q@3.@ z] @4q곩kl. m xG[d"}sWstut׶m>)Sl++`#Nn+H ֦N T&C]*KkCh%tTItɆk?3vfõ!_T`>wKM?XYqsdfܨ jKjtZ-G-sbwb8;og,@._Qp^8|2ߋxzw ook uT* 'bĚfK nM}T'6#{.}zCvrV.坬 GnoW]^&/ ˷\Vאg0`P*<\`b'cG۸{:n]> Hzۛ|ܡKSdj͂[?uUV"˗h?[T`[)GIUnyo#{m?^g.h3BpRk3nW_]>oWRcvOGDKd4|tww/1qHH!D) (BR:!Ja (BR8EKyyM_;ic@υ~zՍl"B?T0V|v͝SY2)7jdzic>Y# 7ǢΎϤ %7^u)|[`G*;?{UN0|NGKeU-Hf]SD?k @@{)K 8f{$X9$&b n%:Y}@?z5m^6ݼXN],1`=e>)BfpVm_2:[<́+E4_(5l67^Y qfa?~Aa)F~'Շ} F]:rIu u@v`\͙ԟ:_)Cvk[{Bh\Bג㟗lɝ̼GaG9֧I9-6 8 @;a:Pg'Gz`i L@W[C;ghZTs}xd#6Bq-sR}{=<́;3#kJO[7wM-**;n$<׫$6}b3nMkcˁΛuh܉>B:t mJ 9Z-k0``*gF>>5͏\K7iކWVAu]& D{_}j}狎 |n {uFܺqgKGOʧ xϱ'Lpjqaqã}j +u+;ʎ*jt6g^iVOƲԤ6ry}IL`eBH6cNol7l9űޱ>7i_c'ΧccGk|yd]uXèoh$z׏wMqX34卽L&)+9x$̚Ze &z2H!D)BR:!Ja (BR:!Ja (É۝/pḱVgo5161u?log􆲒{,| L\46kgUyw՝os]1W$^6y)y׳|K?cmYVU#nDE5=M/k%DaUsfL?G=8}"+hZKMi@/ڞɛ>| K37kX^"<{Y$L(kLqerќuʊ~篞~2?ʭ5/]˶#b\^lϠ?U =>9d LF8l>!7hnN rv/~"9D)Ս$LZ0`y7SmWlZ2ޤ}=w &FF~ueoNwd]a]__UYVzں.[sӕ#)7790ޟkV)|F3oX~Wv\sl@u@OaWzш)6t UU{)\Z:u OgC*nl \csFߐ&w@`ρVKm\Zd_5Q]vvXjCkZup|k;~ Hh. K)/ ZtU qPl1+}m!}NFɺW?<]}4f3.9͝=#_S`5a?.'9|`Nj̚ԅNAqפ[~'vUSw:p|rjwe:n[_:e^_9v}n[]= ]*0„CK3ś0}Nk_{!tк'Kn]~^gi Ľ=ms7#; "商|?Z0ܹwVӸVgWثdV7u 5n nM&fm?Ʒ: ϼw}_\8Ф_Rz?am;5`s&z_-ڞak?}\ AhjLhRU@yX9aȿJ1́ 絽 \zU1D\iaKl`0 t,8uy]~;:4s)Sl+Nt]]n &MV 6.vO.2:ϻؾwLbO|ukZ0}lmjx~/ijV9r.NAfJ/.M\ںm m/ {y+Aњ?=*{*abm~00y+ȯ3}]m\o}Y&[\,.[>{3y֢0:Uo$kjntwx;H]| f?6}׬CVݚ3Ajjʓy0 g½>]?7T3'72_|'.f "F*Xu6α[utilVYx燐j^dy]]gUl^>s:ck3H0-/Н8%nCxe(vu(;ܳa IDAT ~S m1. g cHxrba; `""@Ƴg sSEV.0d^jߒ:O=|"ϣ=ktJ*Vݫz2uA=:̾._9^5A=^^!(BRӱ x#܃a06Kh :aAH9cgr43ԇ󻆑Mue± wuL"/QK^mn?ߪEC$^ _hfԗdß0XFß0tzQ:]jª/ lK#|&MblUFBci=joǵ+7 o n%_uׇ dbsE(sRv ii]A^?m3'zQ:_juKޜ0SѸ.9~?Bc46kOә7ΰ2G|ÁUrLwOAt8!D^!(t*$O' ^^!(BR:!Ja (BR:!Ja (BR:!Ja (BR:!Ja (BR:!Ja (BR:!Ja (BR:!Ja (BR:!Ja (BRCh4R\йs@NXY\z &0L:NqBO1ˡwt: zBR:!Ja (mҤI]]^^!(BR:!Ja (BR:!Ja (Ɖ>|y;3 2Z~h z(+$B~.[*IRpN\I=M7N~r!JjJΉfmjeН8v}ɡ?E[s8Ϧfה8m745_;< o:U^ \taw{[vkZ}^^Қ-[RjIJ/܀I{mٲXKp²HɏOL͚ܓI"%ܲ%5dr -;nݶ!nJJݲ%-oBBF*/KۺekI(($<⽛wnٲptGďmܖ~*zKMi[SkLOۺukz{D?u[Yojg,8mmYM҈d^I6@⠻mli83M/V֝S%ptgU,C{`f Y *~wϴ8 j |Owy~:nН:[7, q.Pt3~?|2&@[R u[TSp>%Z&)/)<#-<ܭ`o @SYV@&))AuJEMMeY ݸ9k:ΊޚWd5YԧDN vHD&E[sYV܀HY[I珕)^@SEw+J%5&f6gN5[\Kle ]Nׁ T>概/բ"ioS%22C(7ر K-g.uRW\oɼ[תyLQi(?EYg[k(@kj]ik3x]$B0M,hH\fz\S P_UPN%\|q9CKCl"Zw*a[`ik\$S[[یຉ ^ܬ& ul=q *%LcLwA׍ p[͜ɒ.NJ*r zJB zEU%22C JwzGB! ʃ(hiA: :TkS'ܪzz B_,X[=,3=J?7uZ\zێ$$LYi-D@"v;$.4Yrv:<˝q6՟/ŲU˕; nq5߳appsmn33zASMIVM V-[x9;ƻ^9_Py=3Է s.pC* k Pq [-6uvUa,cֆUjdI銗Wh 'N杞zB#SsD|JUH$LHvS3h0rW|*9hS_^0܃ TZ`]35}Y媾cF͹`2@D IVr! *)tY0]*US#W`.\+ - ::@JTԸ=?Cp="% ݺd`,063}7K.D%pp eLqQ}xƼN۷Rhs~76Vy}Ю_GONv({˰;-ecG{,&Ee:[k)ib4dͲe)"49k^X_U~K$ `e_pc}&8w$x*c|Xvc`4dMBRZSvc+RcMyOzkY1e lY.W\NNFY|,kN䜻zLMq)[\KkMIOΏaLNS!7t(nK15i3KKڒV]sw˦Ux J_Yei*/7o-Ϩ)̈۲q w^ka6UkLc/ejcVA*{#výxJ,>ۛ: O`eʋAWrt8q\h;sy8e 7aWbJ>d)dz5Wz3]~y.[^hHڷ6rrZ?kګܥikaS,li;8ظ!"0Gs5|:6-Sc_bv^ʧ,h5{a6B(E &coqq ,/W{$/5Óֆh1qrMU];qpl\Gu#uf6ZQgj~ܧQuՆvK"}D\T[r8WmsdSLc{2f/`l(-}ȢG/1QϬUx\b~ L<-)ϯFcUA.cU8ع2,Z^Rb.wO{*",{y]1ڿ MŔV2ݶm۶mvh,Ӛq8C~o,'06Q*-*1½]1#$ {# l{bq?OwpB舦>=oR) O}z?ꧻr7Xhcؼp'zp2kk)b=j.WX uU&O'S_Vaݽ1O2Vux[;Sv _'S'SۧS\MKVx3$?ʹ7hj2;~PY}+rwsЮ,n( g3iE. vߐ_mIJ_kO6륳'!΍]"BXP0AsIbV)>yB'+ G >#X4y-$jlVf <ΞE^.ik?ݻ>*' :)R$Rz3pcj?عbX| u"OOsmV$xW4, {0Ũ?ZnއBQP/e eyG/ qUZG$y$t1oAS' vGubUT*1T'!ov\Lio70Q?Ϻ=uVJėBz4ž}fmٷKk66Ui5:qQSws>0L|OjÍ7kcJLL2(Sc^:{$_V)2Mq%bWV "Pt2:Z$..3_zRk0]SVHT._*8]>[c<1O+չ',eΝ8qVxʔ~r2tOtd.wvvywyl\TFH~u; {0F+~Ϻqͦ ={!/Xb PO* B(t!!B* B(t!!Bª}FrrrO޽Bh|V=7V5! ^BXECa!ULY<'ժ,kuP~|SU$V{:֒6?W}աBݫs9% ltM&iN.2X {녺V5= !v $w5kQ_{x?9i%Lr| !:]R 8EAbKǺ>;`7t<bC:#ny3VXr?!ӪXbΏ"e!x߽}%FVj c,jh]eN=8dN7}O6/;tN0ֵ ^BXECa!U:V:<^'V|w`v7>,Aٕ,>SmIfD;G?X]j/ :gW|z?'V֖?pe!s= 3g@~.X {C?.nVT'Z IDAT 30~Bgfϋ3œl8z<CcT\t4~r~Y0+f_k[ġRݗg)=elgG?$3%; >m/)íL+X p7%㋯li&Kv0?Hs׃sɄNή{ɢ}d*ZteD/'0D%FO|Rrnfh!vUɷ軧;7_kfyҳ]B)i*ћUId%O܈Y1{77-* ݝU񪆧n0Q~`j>\=>-|9=w/|[8˔3w|#1kÚ;RIG!J)-񁙪(Յie-'_Uw@8/Y6Nd>`ȘhiUa]%jmYgiVo$@ 0d1 ZN@b\G515x $-Ms}͐jkOf]Nw0.T:=83!Dw px|83B'~]$ _쇎K`ԯjYʍ+2iҌAWVgt27hXNXfa~zca~W5} \3;_GsTmVP;dZ=Fmsy%v;`d!+u]٩ځ']^~+j;Ol/ߖ+e*Un6R76)Y#i?@c@8,SZFMг{S=?NyLA)tF2= _|Y)%z:VQO* B(t!!B* B(t!⸹ |(\>#78wwU`\g. NɫG up'5?=*NՌwHKYQ DWMV[40ԗ7unO};puΣ!άn~|gR79]7/vU+?XO6F^0ofFw~>7 9F+K.SlVM像gwlt3b6&u 7eϦ9I]O:}ٽw7 $g}:幤x_,X.,5G*SQK`Gqw!.G^whL?WܔBK2k#Ϟ1_E[>|cp?Y1}U-n>;IvOh.x0ݴ5|G߶_x^JT+'I}T/\ܻUtn>X폝$npal/$d`ΕO3xhQ\yX;ע 0ج\7>\ tv|{;Wjj:/v#} ]6_]r}ISluA +xyU[xѳjx]͍RݎLBF2Ջ<5f W%g޹sSXq tF2!<6C7oF^!s}ǯ84BȨm$^os!tPqQ4/990=M'2992^еWV"d߷!;{Ea!U:VQBXECa!U:VQBXECa!U:VQBXECa!U:VQBXECa!U:V* IENDB`litestar-2.16.0/docs/tutorials/dto-tutorial/index.rst000066400000000000000000000051451500564371300227060ustar00rootroot00000000000000Data Transfer Object Tutorial ============================= .. admonition:: Who is this tutorial for? :class: info This tutorial is intended to familiarize you with the basic concepts of Litestar's Data Transfer Objects (DTOs). It is assumed that you are already familiar with Litestar and fundamental concepts such as route handlers. If not, it is recommended to first follow the `Developing a basic TODO application <../todo-app>`_ tutorial. In this tutorial, we will walk through the process of modelling a simple data structure, and demonstrate how Litestar's DTO factories can be used to help us build flexible applications. Lets get started! .. literalinclude:: /examples/data_transfer_objects/factory/tutorial/initial_pattern.py :language: python :caption: ``app.py`` In this script, we define a data model, a route handler and an application instance. Our data model is a Python :func:`dataclass ` called ``Person`` which has three attributes: ``name``, ``age``, and ``email``. The function called ``get_person`` that is decorated with :class:`@get() ` is a route handler with path ``/person/{name:str}``, that serves `GET `_ requests. In the path, ``{name:str}`` represents a path parameter called ``name`` with a string type. The route handler receives the name from the path parameter and returns a ``Person`` object. Finally, we create an application instance and register the route handler with it. In Litestar, this pattern works "out-of-the-box" - that is, returning :func:`dataclass ` instances from handlers is natively supported. Litestar will take that dataclass instance, and transform it into :class:`bytes` that can be sent over the network. Lets run it and see for ourselves! Save the above script as ``app.py``, run it using the ``litestar run`` command, and then visit ``_ in your browser. You should see the following: .. image:: images/initial_pattern.png :align: center However, real-world applications are rarely this simple. What if we want to restrict the information about users that we expose after they have been created? For example, we may want to hide the user's email address from the response. This is where Data Transfer Objects come in. .. toctree:: :hidden: 01-simple-dto-exclude 02-nested-exclude 03-nested-collection-exclude 04-max-nested-depth 05-renaming-fields 06-receiving-data 07-read-only-fields 08-dto-data 09-updating 10-layered-dto-declarations litestar-2.16.0/docs/tutorials/index.rst000066400000000000000000000002151500564371300202500ustar00rootroot00000000000000Tutorials ========= .. toctree:: :hidden: todo-app/index sqlalchemy/index dto-tutorial/index repository-tutorial/index litestar-2.16.0/docs/tutorials/repository-tutorial/000077500000000000000000000000001500564371300224715ustar00rootroot00000000000000litestar-2.16.0/docs/tutorials/repository-tutorial/01-modelling-and-features.rst000066400000000000000000000102301500564371300277630ustar00rootroot00000000000000Introduction to Database Modelling and Repository Features ---------------------------------------------------------- In this tutorial, we will cover the integrated repository features in Litestar, starting with database modelling using the included SQLAlchemy declarative model helpers. These are a series of classes and mixins that incorporate commonly used functions/column types to make working with models easier. .. tip:: The full code for this tutorial can be found below in the :ref:`Full Code <01-repo-full-code>` section. Modelling --------- We'll begin by modelling the entities and relationships between authors and books. We'll start by creating the ``Author`` table, utilizing the :class:`UUIDBase ` class. To keep things simple, our first model will encompass only three fields: ``id``, ``name``, and ``dob``. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py :language: python :caption: ``app.py`` :lines: 9, 11, 18-20 :linenos: The book entity is not considered a "strong" entity and therefore always requires an author to be created. We need to configure our SQLAlchemy classes so that it is aware of this relationship. We will extend the ``Author`` model by incorporating a ``Book`` relationship. This allows each ``Author`` record to possess multiple ``Book`` records. By configuring it this way, SQLAlchemy will automatically include the necessary foreign key constraints when using the ``author_id`` field in each ``Book`` record. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py :language: python :caption: ``app.py`` :lines: 9, 11, 18-22, 27-30 :linenos: By using the audit model, we can automatically record the time a record was created and last updated. To implement this, we will define a new ``Book`` model via the :class:`UUIDAuditBase ` class. Observe that the only modification here is the parent class from which we inherit. This minor change endows the `book` table with automatic timestamp columns (`created` and `updated`) upon deployment! .. note:: If your application requires integer-based primary keys, equivalent base model and base audit model implementations can be found at :class:`BigIntBase ` and :class:`BigIntAuditBase ` respectively. .. important:: `Spanner `_ only: Using monotonically changing primary keys is considered an anti-pattern in Spanner and leads to performance problems. Additionally, Spanner does not currently include an idiom comparable to the ``Sequence`` object. This means the ``BigIntBase`` and ``BigIntAuditBase`` are not currently supported for Spanner. Additional features provided by the built-in base models include: - Synchronous and Asynchronous repository implementations have been tried and tested with various popular database engines. As of now, six database engines are supported: Postgres, SQLite, MySQL, DuckDB, Oracle, and Spanner. - Automatic table name deduction from model name. For instance, a model named ``EventLog`` would correspond to the ``event_log`` database table. - A :class:`GUID ` database type that establishes a native UUID in supported engines or a ``Binary(16)`` as a fallback. - A ``BigInteger`` variant :class:`BigIntIdentity ` that reverts to an ``Integer`` for unsupported variants. - A custom :class:`JsonB ` type that uses native ``JSONB`` where possible and ``Binary`` or ``Blob`` as an alternative. - A custom :class:`EncryptedString ` encrypted string that supports multiple cryptography backends. Let's build on this as we look at the repository classes. .. _01-repo-full-code: Full Code --------- .. dropdown:: Full Code (click to toggle) .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py :language: python :caption: ``app.py`` :emphasize-lines: 9, 18-21, 27-30 :linenos: litestar-2.16.0/docs/tutorials/repository-tutorial/02-repository-introduction.rst000066400000000000000000000367601500564371300304140ustar00rootroot00000000000000Interacting with repositories ----------------------------- Now that we've covered the modelling basics, we are able to create our first repository class. The repository classes include all of the standard CRUD operations as well as a few advanced features such as pagination, filtering and bulk operations. .. tip:: The full code for this tutorial can be found below in the :ref:`Full Code <02-repo-full-code>` section. Before we jump in to the code, let's take a look at the available functions available in the the synchronous and asynchronous repositories. .. dropdown:: Available Functions in the Repositories +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Function | Category | Description | +=====================+================+=============================================================================================================================================================================================================================================+ | ``get`` | Selecting Data | Select a single record by primary key. Raises an exception when no record is found. | +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``get_one`` | Selecting Data | Select a single record specified by the ``kwargs`` parameters. Raises an exception when no record is found. | +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``get_one_or_none`` | Selecting Data | Select a single record specified by the ``kwargs`` parameters. Returns ``None`` when no record is found. | +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``list`` | Selecting Data | Select a list of records specified by the ``kwargs`` parameters. Optionally it can be filtered by the included ``FilterTypes`` args. | +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``list_and_count`` | Selecting Data | Select a list of records specified by the ``kwargs`` parameters. Optionally it can be filtered by the included ``FilterTypes`` args. Results are returned as a 2 value tuple that includes the rows selected and the total count of records.| +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``get_or_create`` | Creating Data | Select a single record specified by the the ``kwargs`` parameters. If no record is found, one is created with the given values. There's an optional attribute to filter on a subset of the supplied parameters and to merge updates. | +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``create`` | Creating Data | Create a new record in the database. | +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``create_many`` | Creating Data | Create one or more rows in the database. | +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``update`` | Updating Data | Update an existing record in the database. | +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``update_many`` | Updating Data | Update one or more rows in the database. | +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``upsert`` | Updating Data | A single operation that updates or inserts a record based whether or not the primary key value on the model object is populated. | +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``upsert_many`` | Updating Data | Updates or inserts multiple records based whether or not the primary key value on the model object is populated. | +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``remove`` | Removing Data | Remove a single record from the database. | +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | ``remove_many`` | Removing Data | Remove one or more records from the database. | +---------------------+----------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ .. note:: - All three of the bulk DML operations will leverage dialect-specific enhancements to be as efficient as possible. In addition to using efficient bulk inserts binds, the repository will optionally leverage the multi-row ``RETURNING`` support where possible. The repository will automatically detect this support from the SQLAlchemy driver, so no additional interaction is required to enable this. - SQL engines generally have a limit to the number of elements that can be appended into an `IN` clause. The repository operations will automatically break lists that exceed this limit into multiple queries that are concatenated together before return. You do not need to account for this in your own code. In the following examples, we'll cover a few ways that you can use the repository within your applications. Model Repository ^^^^^^^^^^^^^^^^ Here we import the :class:`SQLAlchemyAsyncRepository ` class and create an ``AuthorRepository`` repository class. This is all that's required to include all of the integrated repository features. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_crud.py :language: python :caption: ``app.py`` :lines: 14, 7-30 :linenos: Repository Context Manager ^^^^^^^^^^^^^^^^^^^^^^^^^^ Since we'll be using the repository outside of a Litestar application in this script, we'll make a simple context manager to automatically handle the creation (and cleanup) of our Author repository. The ``repository_factory`` method will do the following for us: - Automatically create a new DB session from the SQLAlchemy configuration. - Rollback session when any exception occurs. - Automatically commit after function call completes. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_crud.py :language: python :caption: ``app.py`` :lines: 39-47 :linenos: Creating, Updating and Removing Data ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To illustrate a few ways you can manipulate data in your database, we'll go through the various CRUD operations: Creating Data: Here's a simple insert operation to populate our new Author table: .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_crud.py :language: python :caption: ``app.py`` :lines: 52-61 :linenos: Updating Data: The ``update`` method will ensure any updates made to the model object are executed on the database: .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_crud.py :language: python :caption: ``app.py`` :lines: 64-68 :linenos: Removing Data: The ``remove`` method accepts the primary key of the row you want to delete: .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_crud.py :language: python :caption: ``app.py`` :lines: 71-75 :linenos: Now that we've seen how to do single-row operations, let's look at the bulk methods we can use. Working with Bulk Data Operations --------------------------------- In this section, we delve into the powerful capabilities of the repository classes for handling bulk data operations. Our example illustrates how we can efficiently manage data in our database. Specifically, we'll use a JSON file containing information about US states and their abbreviations. Here's what we're going to cover: Fixture Data Loading ^^^^^^^^^^^^^^^^^^^^ We will introduce a method for loading fixture data. Fixture data is sample data that populates your database and helps test the behavior of your application under realistic conditions. This pattern can be extended and adjusted to meet your needs. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_bulk_operations.py :language: python :caption: ``app.py`` :lines: 1-3, 34-54 :linenos: You can review the JSON source file here: .. dropdown:: US State Lookup JSON You can download it: :download:`/examples/contrib/sqlalchemy/us_state_lookup.json` or view below: .. literalinclude:: /examples/contrib/sqlalchemy/us_state_lookup.json :language: json :caption: ``us_state_lookup.json`` Bulk Insert ^^^^^^^^^^^ We'll use our fixture data to demonstrate a bulk insert operation. This operation allows you to add multiple records to your database in a single transaction, improving performance when working with larger data sets. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_bulk_operations.py :language: python :caption: ``app.py`` :lines: 5-11, 13, 14-16, 18-26, 27-33, 55-70 :linenos: Paginated Data Selection ^^^^^^^^^^^^^^^^^^^^^^^^ Next, let's explore how to select multiple records with pagination. This functionality is useful for handling large amounts of data by breaking the data into manageable 'pages' or subsets. ``LimitOffset`` is one of several filter types you can use with the repository. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_bulk_operations.py :language: python :caption: ``app.py`` :lines: 10, 55-56, 72-74 :linenos: Bulk Delete ^^^^^^^^^^^ Here we demonstrate how to perform a bulk delete operation. Just as with the bulk insert, deleting multiple records with the batch record methods is more efficient than executing row-by-row. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_bulk_operations.py :language: python :caption: ``app.py`` :lines: 76-78 :linenos: Counts ^^^^^^ Finally, we'll demonstrate how to count the number of records remaining in the database. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_bulk_operations.py :language: python :caption: ``app.py`` :lines: 80-82 :linenos: Now that we have demonstrated how to interact with the repository objects outside of a Litestar application, our next example will use dependency injection to add this functionality to a :class:`~litestar.controller.Controller`! .. _02-repo-full-code: Full Code --------- .. dropdown:: Full Code (click to toggle) .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_crud.py :language: python :caption: ``app.py`` :emphasize-lines: 14, 27-30, 37-55, 61, 64-71, 71-75, 77-79, 81-83 :linenos: litestar-2.16.0/docs/tutorials/repository-tutorial/03-repository-controller.rst000066400000000000000000000056471500564371300300570ustar00rootroot00000000000000Working with Controllers and Repositories ----------------------------------------- We've been working our way up the stack, starting with the database models, and now we are ready to use the repository in an actual route. Let's see how we can use this in a controller. .. tip:: The full code for this tutorial can be found below in the :ref:`Full Code <03-repo-full-code>` section. First, we create a simple function that returns an instance of ``AuthorRepository``. This function will be used to inject a repository instance into our controller routes. Note that we are only passing in the database session in this example with no other parameters. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_async_repository.py :language: python :caption: ``app.py`` :lines: 78-80 :linenos: Because we'll be using the SQLAlchemy plugin in Litestar, the session is automatically configured as a dependency. By default, the repository doesn't add any additional query options to your base statement, but provides the flexibility to override it, allowing you to pass your own statement: .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_async_repository.py :language: python :caption: ``app.py`` :lines: 83-90 :linenos: In this instance, we enhance the repository function by adding a ``selectinload`` option. This option configures the specified relationship to load via `SELECT .. IN ...` loading pattern, optimizing the query execution. Next, we define the ``AuthorController``. This controller exposes five routes for interacting with the ``Author`` model: .. dropdown:: ``AuthorController`` (click to toggle) .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_async_repository.py :language: python :caption: ``app.py`` :lines: 116-194 :linenos: In our list detail endpoint, we use the pagination filter for limiting the amount of data returned, allowing us to retrieve large datasets in smaller, more manageable chunks. In the above examples, we've used the asynchronous repository implementation. However, Litestar also supports synchronous database drivers with an identical implementation. Here's a corresponding synchronous version of the previous example: .. dropdown:: Synchronous Repository (click to toggle) .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_sync_repository.py :language: python :caption: ``app.py`` :linenos: The examples above enable a feature-complete CRUD service that includes pagination! In the next section, we'll explore how to extend the built-in repository to add additional functionality to our application. .. _03-repo-full-code: Full Code --------- .. dropdown:: Full Code (click to toggle) .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_async_repository.py :language: python :caption: ``app.py`` :emphasize-lines: 78-80, 83-90, 116-194 :linenos: litestar-2.16.0/docs/tutorials/repository-tutorial/04-repository-other.rst000066400000000000000000000057011500564371300270050ustar00rootroot00000000000000Adding Additional Features to the Repository -------------------------------------------- While most of the functionality you need is built into the repository, there are still cases where you need to add in additional functionality. Let's explore ways that we can add functionality on top of the repository pattern. .. tip:: The full code for this tutorial can be found below in the :ref:`Full Code <04-repo-full-code>` section. Slug Fields ----------- .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_extension.py :language: python :caption: ``app.py`` :lines: 10-23, 33-34, 37-42, 101-102, 106-108 :linenos: In this example, we are using a ``BlogPost`` model to hold blog post titles and contents. The primary key for this model is a ``UUID`` type. ``UUID`` and ``int`` are good options for primary keys, but there are a number of reasons you may not want to use them in your routes. For instance, it can be a security problem to expose integer-based primary keys in the URL. While UUIDs don't have this same problem, they are not user-friendly or easy-to-remember, and create complex URLs. One way to solve this is to add a user friendly unique identifier to the table that can be used for urls. This is often called a "slug". First, we'll create a ``SlugKey`` field mixin that adds a text-based, URL-friendly, unique column ``slug`` to the table. We want to ensure we create a slug value based on the data passed to the ``title`` field. To demonstrate what we are trying to accomplish, we want a record that has a blog title of "Follow the Yellow Brick Road!" to have the slugified value of "follow-the-yellow-brick-road". .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_extension.py :language: python :caption: ``app.py`` :lines: 1-8, 14-23, 43-44, 46-100 :linenos: Since the ``BlogPost.title`` field is not marked as unique, this means that we'll have to test the slug value for uniqueness before the insert. If the initial slug is found, a random set of digits are appended to the end of the slug to make it unique. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_extension.py :language: python :caption: ``app.py`` :lines: 1-23, 27-32, 101-102, 106-126, 170-180 :linenos: We are all set to use this in our routes now. First, we'll convert our incoming Pydantic model to a dictionary. Next, we'll fetch a unique slug for our text. Finally, we insert the model with the added slug. .. note:: Using this method does introduce an additional query on each insert. This should be considered when determining which fields actually need this type of functionality. .. _04-repo-full-code: Full Code --------- .. dropdown:: Full Code (click to toggle) .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_repository_extension.py :language: python :caption: ``app.py`` :emphasize-lines: 12, 37-42, 106-108, 177 :linenos: litestar-2.16.0/docs/tutorials/repository-tutorial/index.rst000066400000000000000000000012051500564371300243300ustar00rootroot00000000000000SQLAlchemy Repository Tutorial ============================== In this tutorial, we will walk through the process of modelling simple database relationships, and demonstrate how Litestar's repository modules can make working with databases a breeze. Lets get started! .. admonition:: Who is this tutorial for? :class: info This tutorial covers Litestar's SQLAlchemy repository. It assumes an understanding of Litestar's key concepts, as well as a degree of familiarity with SQLAlchemy. .. toctree:: :hidden: 01-modelling-and-features 02-repository-introduction 03-repository-controller 04-repository-other litestar-2.16.0/docs/tutorials/sqlalchemy/000077500000000000000000000000001500564371300205535ustar00rootroot00000000000000litestar-2.16.0/docs/tutorials/sqlalchemy/0-introduction.rst000066400000000000000000000111301500564371300241570ustar00rootroot00000000000000Introduction ------------ We start with a full script that shows how you can use SQLAlchemy with Litestar. In this app, we interact with SQLAlchemy in the manner described by the `SQLAlchemy documentation `_, and so if you are looking for more information about any of the SQLAlchemy code, this will be a great place to start. You'll notice that we use a couple of Litestar features that you may not have encountered yet: 1. Management and injection of :ref:`application state ` 2. Use of a :ref:`Lifespan context manager ` And we will continue to learn about other Litestar features as we work through the tutorial, such as: 1. Dependency injection 2. Plugins The full app ============ While it may look imposing, this app only has minor behavioral differences to the previous example. It is still an app that maintains a TODO list, that allows for adding, updating and viewing the collection of TODO items. Don't worry if there are things in this example that you don't understand. We will cover all of the components in detail in the following sections. .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_no_plugins.py :language: python :linenos: The differences =============== Apart from the obvious differences due to the SQLAlchemy code, there are a few things worth mentioning from the outset. Complexity ++++++++++ This code is undoubtedly more complex than the code we have seen so far - although a crude measure of complexity, we can see that there are more than double the lines of code to the previous example. Lifespan context manager ++++++++++++++++++++++++ When using a database, we need to ensure that we clean up our resources correctly. To do this, we create a context manager called ``db_connection()`` that creates a new :class:`engine ` and disposes of it when we are done. This context manager is added to the application's ``lifespan`` argument. .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_no_plugins.py :language: python :linenos: :lines: 28-41,100 Database creation +++++++++++++++++ Before we can use the database we need to make sure it exists and the tables are created as defined by the ``TodoItem`` class. This can be done by a synchronous call to ``Base.metadata.create_all`` which is invoked by ``run_sync``. If the tables are already setup according to the model, the call does nothing. .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_no_plugins.py :language: python :linenos: :lines: 28-41 :emphasize-lines: 8-9 Application state +++++++++++++++++ We see two examples of access and use of application state. The first is in the ``db_connection()`` context manager, where we use the ``app.state`` object to store the engine. .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_no_plugins.py :language: python :linenos: :lines: 28-41 :emphasize-lines: 3,6 The second is by using the ``state`` keyword argument in our handler functions, so that we can access the engine in our handlers. .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_no_plugins.py :language: python :linenos: :lines: 69-72 :emphasize-lines: 2,3 Serialization +++++++++++++ Now that we are using SQLAlchemy models, Litestar cannot automatically handle (de)serialization of our data. We have to convert the SQLAlchemy models to a type that Litestar can serialize. This example introduces two type aliases, ``TodoType`` and ``TodoCollectionType`` to help us represent this data at the boundaries of our handlers. It also introduces the ``serialize_todo()`` to help us convert our data from the ``TodoItem`` type to a type that is serializable by Litestar. .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_no_plugins.py :language: python :linenos: :lines: 2-3,14-15,47-50,91-97 :emphasize-lines: 3,4,6,7,10,15 Behavior ++++++++ The ``add_item()`` and ``update_item()`` routes no longer return the full collection, instead they return the item that was added or updated. This is a minor detail change, but it is worth noting as it brings the behavior of the app closer to what we would expect from a conventional API. Next steps ========== Lets start cleaning this app up a little. One of the standout issues is that we repeat the logic to create a database session in every handler. This is something that we can fix with dependency injection. litestar-2.16.0/docs/tutorials/sqlalchemy/1-provide-session-with-di.rst000066400000000000000000000066661500564371300261550ustar00rootroot00000000000000Providing the session with DI ----------------------------- In our original script, we had to repeat the logic to construct a session instance for every request type. This is not very `DRY `_. In this section, we'll use dependency injection to centralize the session creation logic and make it available to all handlers. .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_session_di.py :language: python :linenos: :emphasize-lines: 47-57,82-83,87-89,94-95,103 In the previous example, the database session is created within each HTTP route handler function. In this script we use dependency injection to decouple creation of the session from the route handlers. This script introduces a new async generator function called ``provide_transaction()`` that creates a new SQLAlchemy session, begins a transaction, and handles any integrity errors that might raise from within the transaction. .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_session_di.py :language: python :linenos: :lines: 48-57 That function is declared as a dependency to the Litestar application, using the name ``transaction``. .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_session_di.py :language: python :linenos: :lines: 101-105 :emphasize-lines: 3 In the route handlers, the database session is injected by declaring the ``transaction`` name as a function argument. This is automatically provided by Litestar's dependency injection system at runtime. .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_session_di.py :language: python :linenos: :lines: 81-84 :emphasize-lines: 2 One final improvement in this script is exception handling. In the previous version, a :class:`litestar.exceptions.ClientException` is raised inside the ``add_item()`` handler if there's an integrity error raised during the insertion of the new TODO item. In our latest revision, we've been able to centralize this handling to occur inside the ``provide_transaction()`` function. .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_session_di.py :language: python :linenos: :lines: 47-57 :emphasize-lines: 3,6-10 This change broadens the scope of exception handling to any operation that uses the database session, not just the insertion of new items. Compare handlers before and after DI ==================================== Just for fun, lets compare the sets of application handlers before and after we introduced dependency injection for our session object: .. tab-set:: .. tab-item:: After .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_session_di.py :language: python :linenos: :lines: 81-105 .. tab-item:: Before .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_no_plugins.py :language: python :linenos: :lines: 69-100 Much better! Next steps ========== One of the niceties that we've lost is the ability to receive and return data to/from our handlers as instances of our data model. In the original TODO application, we modelled with Python dataclasses which are natively supported for (de)serialization by Litestar. In the next section, we will look at how we can get this functionality back! litestar-2.16.0/docs/tutorials/sqlalchemy/2-serialization-plugin.rst000066400000000000000000000035731500564371300256250ustar00rootroot00000000000000Using the serialization plugin ------------------------------ Our next improvement is to leverage the :class:`SQLAlchemySerializationPlugin ` so that we can receive and return our SQLAlchemy models directly to and from our handlers. Here's the code: .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_serialization_plugin.py :language: python :linenos: :emphasize-lines: 12, 75-77, 80-83, 86-91, 98 We've simply imported the plugin and added it to our app's plugins list, and now we can receive and return our SQLAlchemy data models directly to and from our handler. We've also been able to remove the ``TodoType`` and ``TodoCollectionType`` aliases, and the ``serialize_todo()`` function, making the implementation even more concise. Compare handlers before and after Serialization Plugin ====================================================== Once more, let's compare the sets of application handlers before and after our refactoring: .. tab-set:: .. tab-item:: After .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_serialization_plugin.py :language: python :linenos: :lines: 1-13, 73-99 .. tab-item:: Before .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_no_plugins.py :language: python :linenos: :lines: 1-12, 67-100 Very nice! But, we can do better. Next steps ========== In our application, we've had to build a bit of scaffolding to integrate SQLAlchemy with our application. We've had to define the ``db_connection()`` lifespan context manager, and the ``provide_transaction()`` dependency provider. Next we'll look at how the :class:`SQLAlchemyInitPlugin ` can help us. litestar-2.16.0/docs/tutorials/sqlalchemy/3-init-plugin.rst000066400000000000000000000030341500564371300237040ustar00rootroot00000000000000Using the init plugin --------------------- In our example application, we've seen that we need to manage the database engine within the scope of the application's lifespan, and the session within the scope of a request. This is a common pattern, and the :class:`SQLAlchemyInitPlugin ` plugin provides assistance for this. In our latest update, we leverage two features of the plugin: 1. The plugin will automatically create a database engine for us and manage it within the scope of the application's lifespan. 2. The plugin will automatically create a database session for us and manage it within the scope of a request. We access the database session via dependency injection, using the ``db_session`` parameter. Here's the updated code: .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_init_plugin.py :language: python :linenos: :emphasize-lines: 12, 28, 76-78, 85 The most notable difference is that we no longer need the ``db_connection()`` lifespan context manager - the plugin handles this for us. It also handles the creation of the tables in our database if we supply our metadata and set ``create_all=True`` when creating a ``SQLAlchemyAsyncConfig`` instance. Additionally, we have a new ``db_session`` dependency available to us, which we use in our ``provide_transaction()`` dependency provider, instead of creating our own session. Next steps ========== Next up, we'll make one final change to our application, and then we'll be recap! litestar-2.16.0/docs/tutorials/sqlalchemy/4-final-touches-and-recap.rst000066400000000000000000000052631500564371300260450ustar00rootroot00000000000000Final touches and recap ----------------------- There is one more improvement that we can make to our application. Currently, we utilize both the :class:`SQLAlchemyInitPlugin ` and the :class:`SQLAlchemySerializationPlugin `, but there is a shortcut for this configuration: the :class:`SQLAlchemyPlugin ` is a combination of the two, so we can simplify our configuration by using it instead. Here is our final application: .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_plugin.py :language: python :linenos: :emphasize-lines: 10, 82 Recap ===== In this tutorial, we have learned how to use the SQLAlchemy plugin to create a simple application that uses a database to store and retrieve data. In the final application ``TodoItem`` is defined, representing a TODO item. It extends from the :class:`DeclarativeBase ` class provided by `SQLAlchemy `_: .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_plugin.py :language: python :linenos: :lines: 1-6, 12-21 Next, we define a dependency that centralizes our database transaction management and error handling. This dependency depends on the ``db_session`` dependency, which is provided by the SQLAlchemy plugin, and is made available to our handlers via the ``transaction`` argument: .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_plugin.py :language: python :linenos: :lines: 1-11, 22-32 We also define a couple of utility functions, that help us to retrieve our TODO items from the database: .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_plugin.py :language: python :linenos: :lines: 1-11, 33-50 We define our route handlers, which are the interface through which TODO items can be created, retrieved and updated: .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_plugin.py :language: python :linenos: :lines: 1-11, 51-69 Finally, we define our application, using the :class:`SQLAlchemyPlugin ` to configure SQLAlchemy and manage the engine and session lifecycle, and register our ``transaction`` dependency. .. literalinclude:: /examples/contrib/sqlalchemy/plugins/tutorial/full_app_with_plugin.py :language: python :linenos: :lines: 1-11, 78-83 .. seealso:: * :doc:`SQLAlchemy Plugins Usage Guide ` litestar-2.16.0/docs/tutorials/sqlalchemy/index.rst000066400000000000000000000025661500564371300224250ustar00rootroot00000000000000Improving the TODO app with SQLAlchemy -------------------------------------- .. admonition:: Who is this tutorial for? :class: info This tutorial is aimed at developers who are already familiar with Litestar's core concepts such as route handlers and dependency injection. If you are new to Litestar, it is recommended to first follow the `Developing a basic TODO application <../todo-app>`_ tutorial. Install SQLAlchemy ================== To follow this tutorial, you will need SQLAlchemy installed. You can install it with ``pip install 'sqlalchemy[aiosqlite]'``, or let Litestar install it for you by installing the ``sqlalchemy`` extra (e.g., ``pip install 'litestar[standard,sqlalchemy]' aiosqlite``). What's in this tutorial? ======================== This tutorial builds on the `TODO app tutorial <../todo-app>`_ by adding a database backend using `SQLAlchemy `_. We start by comparing a refactored TODO app that leverages SQLAlchemy for data persistence to the TODO app from the `TODO app tutorial <../todo-app>`_. We will then gradually improve on the design of our app by utilising more of Litestar's features, such as dependency injection, and plugins. Contents ======== .. toctree:: :titlesonly: 0-introduction 1-provide-session-with-di 2-serialization-plugin 3-init-plugin 4-final-touches-and-recap litestar-2.16.0/docs/tutorials/todo-app/000077500000000000000000000000001500564371300201345ustar00rootroot00000000000000litestar-2.16.0/docs/tutorials/todo-app/0-application-basics.rst000066400000000000000000000127461500564371300246020ustar00rootroot00000000000000Application basics ================== First steps ------------ Before we start building our TODO application, let us start with the basics. Install Litestar ++++++++++++++++ To install Litestar, run ``pip install 'litestar[standard]'``. This will install Litestar as well as `uvicorn `_ - a web server to serve your application. .. note:: You can use any ASGI-capable web server, but this tutorial will use - and Litestar recommends - Uvicorn. Hello, world! +++++++++++++ The most basic application you can implement - and a classic one at that - is of course one that prints ``"Hello, world!"``: .. literalinclude:: /examples/todo_app/hello_world.py :language: python :caption: ``app.py`` Now save the contents of this example in a file called ``app.py`` and type ``litestar run`` in your terminal. This will serve the application locally on your machine. Now visit http://127.0.0.1:8000/ in your browser: .. image:: images/hello_world.png Now that we have a working application, let us examine how we got here in a bit more detail. Route handlers --------------- Route handlers tell your Litestar application what to do when it gets a request. They are named this way because they typically handle a single URL path (or *route*), which is the part of the URL that's specific to your application. In our current example, the only route handler we have is for ``hello_world``, and it is using the ``/`` path. .. tip:: For example, if your application has a route for handling requests to the ``/home`` URL path, you would create a route handler function that would be called when a request to that path is received. The first argument to the route handler is the *path*, which in this example has been set to ``/``. This means that the function ``hello_world`` will be called when a request is being made to the ``/`` path of your application. The name of the handler decorator - ``get`` - refers to the HTTP method to which you want to respond. Using ``get`` tells Litestar that you only want to use this function when a ``GET`` request is being made. .. literalinclude:: /examples/todo_app/hello_world.py :language: python :emphasize-lines: 4 :linenos: .. note:: The syntax used in this example (the ``@get`` notation) is called a decorator. It's a function that takes another function as its argument (in this case ``hello_world``) and replaces it with the return value of the decorator function. Without the decorator, the example would look like this: .. code-block:: python async def hello_world() -> str: return "Hello, world!" hello_world = get("/")(hello_world) For an in-depth explanation of decorators, you can read this excellent Real Python article: `Primer on Python Decorators `_ .. seealso:: * :doc:`/usage/routing/handlers` Type annotations ---------------- Type annotations play an important role in a Litestar application. They tell Litestar how you want your data to behave, and what you intend to do with it. .. literalinclude:: /examples/todo_app/hello_world.py :language: python :emphasize-lines: 5 :linenos: In this example, the ``hello_world`` function has a return annotation of ``-> str``. This means that it will return a :class:`string `, and lets Litestar know that you would like to send the return value as-is. .. note:: While type annotations by default don't have any influence on runtime behaviour, Litestar uses them for many things, for example to validate incoming request data. If you are using a static type checker - such as `mypy `_ or `pyright `_ - this has the added benefit of making your applications easy to check and more type-safe. Applications ------------ After a route handler has been defined, it needs to be registered with an application in order to start serving requests. The application is an instance of the :class:`Litestar ` class. This is the entry point for everything, and can be used to register previously defined route handlers by passing a list of them as the first argument: .. literalinclude:: /examples/todo_app/hello_world.py :language: python :emphasize-lines: 9 :linenos: .. seealso:: * :doc:`/usage/applications` Running the application ----------------------- The last step is to actually run the application. Litestar does not include its own HTTP server, but instead makes use of the `ASGI protocol `_, which is a protocol Python objects can use in order to interact with application servers like `uvicorn `_ that actually implement the HTTP protocol and handle it for you. If you installed Litestar with ``pip install litestar[standard]``, this will have included *uvicorn*, as well as the Litestar CLI. The CLI provides a convenient wrapper around uvicorn, allowing you to easily run applications without the need for much configuration. When you run ``litestar run``, it will recognise the ``app.py`` file and the ``Litestar`` instance within it without the need to specify this manually. .. tip:: You can start the server in "reload mode", which will reload the application each time you have made a change to the file. For this, simply pass the ``--reload`` flag as a command line argument: ``litestar run --reload``. .. seealso:: * :doc:`/usage/cli` litestar-2.16.0/docs/tutorials/todo-app/1-accessing-the-list.rst000066400000000000000000000231201500564371300245100ustar00rootroot00000000000000Accessing the list ==================== Intro ----- The first thing you'll be setting up for our app is a route handler that returns a single TODO list. A TODO list in this case will be a list of dictionaries representing the items on that TODO list. .. literalinclude:: /examples/todo_app/get_list/dict.py :language: python :caption: ``app.py`` :linenos: If you run the app and visit http://127.0.0.1:8000/ in your browser you'll see the following output: .. figure:: images/get_todo_list.png Suddenly, JSON Because the ``get_list`` function has been annotated with ``List[Dict[str, Union[str, bool]]]``, Litestar infers that you want the data returned from it to be serialized as JSON: .. literalinclude:: /examples/todo_app/get_list/dict.py :language: python :lineno-start: 13 :lines: 13 Cleaning up the example with dataclasses ++++++++++++++++++++++++++++++++++++++++ To make your life a little easier, you can transform this example by using :py:mod:`dataclasses` instead of plain dictionaries: .. tip:: For an in-depth explanation of dataclasses, you can read this excellent Real Python article: `Data Classes in Python 3.7+ `_ .. literalinclude:: /examples/todo_app/get_list/dataclass.py :caption: ``app.py`` :language: python :linenos: This looks a lot cleaner and has the added benefit of being able to work with dataclasses instead of plain dictionaries. The result will still be the same: Litestar knows how to turn these dataclasses into JSON and will do so for you automatically. .. tip:: In addition to dataclasses, Litestar supports many more types such as :class:`TypedDict `, :class:`NamedTuple `, `Pydantic models `_, or `attrs classes `_. Filtering the list using query parameters ----------------------------------------- Currently ``get_list`` will always return all items on the list, but what if you are interested in only those items with a specific status, for example all items that are not yet marked as *done*? For this you can employ query parameters; to define a query parameter, all that's needed is to add an otherwise unused parameter to the function. Litestar will recognize this and infer that it's going to be used as a query parameter. When a request is being made, the query parameter will be extracted from the URL, and passed to the function parameter of the same name. .. literalinclude:: /examples/todo_app/get_list/query_param.py :caption: ``app.py`` :language: python :linenos: .. figure:: images/todos-done.png Visiting http://127.0.0.1:8000?done=1 will give you all the TODOs that have been marked as *done* .. figure:: images/todos-not-done.png while http://127.0.0.1:8000?done=0 will return only those not yet done At first glance this seems to work just fine, but you might be able to spot a problem: If you input anything other than ``?done=1``, it would still return items not yet marked as done. For example, ``?done=john`` gives the same result as ``?done=0``. An easy solution for this would be to simply check if the query parameter is either ``1`` or ``0``, and return a response with an HTTP status code that indicates an error if it's something else: .. literalinclude:: /examples/todo_app/get_list/query_param_validate_manually.py :caption: ``app.py`` :language: python :linenos: If the query parameter equals ``1``, return all items that have ``done=True``: .. literalinclude:: /examples/todo_app/get_list/query_param_validate_manually.py :language: python :caption: ``app.py`` :lines: 23-24 :dedent: 2 :linenos: :lineno-start: 23 If the query parameter equals ``0``, return all items that have ``done=False``: .. literalinclude:: /examples/todo_app/get_list/query_param_validate_manually.py :language: python :caption: ``app.py`` :lines: 25-26 :dedent: 2 :linenos: :lineno-start: 25 Finally, if the query parameter has any other value, an :exc:`HTTPException` will be raised. Raising an ``HTTPException`` tells Litestar that something went wrong, and instead of returning a normal response, it will send a response with the HTTP status code given (``400`` in this case) and the error message supplied. .. literalinclude:: /examples/todo_app/get_list/query_param_validate_manually.py :language: python :caption: ``app.py`` :lines: 27 :dedent: 2 :linenos: :lineno-start: 27 .. figure:: images/done-john.png Try to access http://127.0.0.1:8000?done=john now and you will get this error message Now we've got that out of the way, but your code has grown to be quite complex for such a simple task. You're probably thinking `"there must be a better way!" `_, and there is! Instead of doing these things manually, you can also just let Litestar handle them for you! Converting and validating query parameters ++++++++++++++++++++++++++++++++++++++++++ As mentioned earlier, type annotations can be used for more than static type checking in Litestar; they can also define and configure behaviour. In this case, you can get Litestar to convert the query parameter to a boolean value, matching the values of the ``TodoItem.done`` attribute, and in the same step validate it, returning error responses for you should the supplied value not be a valid boolean. .. literalinclude:: /examples/todo_app/get_list/query_param_validate.py :language: python :caption: ``app.py`` :linenos: .. figure:: images/done-john-2.png Browse to http://127.0.0.1:8000?done=john from our earlier example, and you will see it now results in this descriptive error message **What's happening here?** Since :class:`bool` is being used as the type annotation for the ``done`` parameter, Litestar will try to convert the value into a :class:`bool` first. Since ``john`` (arguably) is not a representation of a boolean value, it will return an error response instead. .. literalinclude:: /examples/todo_app/get_list/query_param_validate.py :language: python :caption: ``app.py`` :lines: 21 :linenos: :lineno-start: 21 .. tip:: It is important to note that this conversion is not the result of calling :class:`bool` on the raw value. ``bool("john")`` would be :obj:`True`, since Python considers all non-empty strings to be truthy. Litestar however supports customary boolean representation commonly used in the HTTP world; ``true`` and ``1`` are both converted to :obj:`True`, while ``false`` and ``0`` are converted to be :obj:`False`. If the conversion is successful however, ``done`` is now a :class:`bool`, which can then be compared against the ``TodoItem.done`` attribute: .. literalinclude:: /examples/todo_app/get_list/query_param_validate.py :language: python :caption: ``app.py`` :lines: 22 :dedent: 2 :linenos: :lineno-start: 22 .. seealso:: * :ref:`Routing - Parameters - Type coercion ` Making the query parameter optional +++++++++++++++++++++++++++++++++++ There is one problem left to solve though, and that is, what happens when you want to get **all** items, done or not, and omit the query parameter? .. figure:: images/missing-query.png Omitting the ``?done`` query parameter will result in an error Because the query parameter has been defined as ``done: bool`` without giving it a default value, it will be treated as a required parameter - just like a regular function parameter. If instead you want this to be optional, a default value needs to be supplied. .. literalinclude:: /examples/todo_app/get_list/query_param_default.py :language: python :caption: ``app.py`` :linenos: .. figure:: images/get_todo_list.png Browsing to http://localhost:8000 once more, you will now see it does not return an error if the query parameter is omitted .. tip:: In this instance, the default has been set to :obj:`None`, since we don't want to do any filtering if no ``done`` status is specified. If instead you wanted to only display not-done items by default, you could set the value to :obj:`False` instead. .. seealso:: * :ref:`Routing - Parameters - Query Parameters ` Interactive documentation -------------------------- So far we have explored our TODO application by navigating to it manually, but there is another way: Litestar comes with interactive API documentation, which is generated for you automatically. All you need to do is run your app (``litestar run``) and visit http://127.0.0.1:8000/schema/swagger .. figure:: images/swagger-get.png The route handler set up earlier will show up in the interactive documentation This documentation not only gives an overview of the API you have constructed, but also allows you to send requests to it. .. figure:: images/swagger-get-example-request.png Executing the same requests we did earlier .. note:: This is made possible by `Swagger `_ and `OpenAPI `_. Litestar generates an OpenAPI schema based on the route handlers, which can then be used by Swagger to set up the interactive documentation. .. tip:: In addition to Swagger, Litestar serves the documentation from the generated OpenAPI schema with `ReDoc `_ and `Stoplight Elements `_. You can browse to http://127.0.0.1:8000/schema/redoc and http://127.0.0.1:8000/schema/elements to view each, respectively. litestar-2.16.0/docs/tutorials/todo-app/2-interacting-with-the-list.rst000066400000000000000000000114021500564371300260320ustar00rootroot00000000000000Making the list interactive ============================ So far, our TODO list application is not very useful, since it's static. You can't update items, nor add or remove them. Receiving incoming data ----------------------- Let's start by implementing a route handler that handles the creation of new items. In the previous step you used the ``get`` decorator, which responds to the ``GET`` HTTP method. In this case we want to react to ``POST`` requests, so we are going to use the corresponding ``post`` decorator. .. literalinclude:: /examples/todo_app/create/dict.py :language: python :linenos: Request data can be received via the ``data`` keyword. Litestar will recognize this, and supply the data being sent with the request via this parameter. As with the query parameters in the previous chapter, we use the type annotations to configure what type of data we expect to receive, and set up validation. In this case, Litestar will expect request data in the form of JSON and use the type annotation we gave it to convert it into the correct format. .. seealso:: * :doc:`/usage/requests` Using the interactive documentation to test a route ++++++++++++++++++++++++++++++++++++++++++++++++++++ Since our example now uses the ``POST`` HTTP method, you can no longer simply visit the URL in our browser and get a response. Instead, you can use the interactive documentation to send a ``POST`` request. Because of the OpenAPI schema generated by Litestar, Swagger will know exactly what kind of data to send. In this example, it will send a simple JSON object. .. figure:: images/swagger-post-dict-response.png Sending a sample request to our ``add_item`` route reveals a successful response Improving the example with dataclasses ++++++++++++++++++++++++++++++++++++++ As in the previous chapter, this too can be improved by using :doc:`dataclasses ` instead of plain dicts. .. literalinclude:: /examples/todo_app/create/dataclass.py :language: python :linenos: This is not only easier on the eyes and adds more structure to the code, but also gives better interactive documentation; it will now present us with the field names and default values for the dataclass we have defined: .. figure:: images/swagger-dict-vs-dataclass.png Documentation for the ``add_item`` route with ``data`` typed as a ``dict`` vs ``dataclass`` Using a dataclass also gives you better validation: omitting a key such as ``title`` will result in a useful error response: .. figure:: images/swagger-dataclass-bad-body.png Sending a request without a ``title`` key fails Create dynamic routes using path parameters ------------------------------------------- The next task on the list is updating an item's status. For this, a way to refer to a specific item on the list is needed. This could be done using query parameters, but there's an easier, and more semantically coherent way of expressing this: path parameters. .. code-block:: python @get("/{name:str}") async def greeter(name: str) -> str: return "Hello, " + name So far all the paths in your application are static, meaning they are expressed by a constant string which does not change. In fact, the only path used so far is ``/``. Path parameters allow you to construct dynamic paths and later refer to the dynamically captured parts. This may sound complex at first, but it's actually quite simple; you can think of it as a regular expression that's being used on the requested path. Path parameters consist of two parts: an expression inside the path, describing the parameter, and a corresponding function parameter of the same name in the route handler function, which will receive the path parameter's value. In the above example, a path parameter ``name:str`` is declared, which means that now, a request to the path ``/john`` can be made, and the ``greeter`` function will be called as ``greeter(name="john")``, similar to how query parameters are injected. .. tip:: Just like query parameters, path parameters can convert and validate their values as well. This is configured using the ``:type`` colon annotation, similar to type annotations. For example, ``value:str`` will receive values as a string, while ``value:int`` will try to convert it into an integer. A full list of supported types can be found here: :ref:`usage/routing/parameters:supported path parameter types` By using this pattern and combining it with those from the earlier section about receiving data you can now set up a route handler that takes in the title of a TODO item, an updated item in form of a dataclass instance, and updates the item in the list. .. literalinclude:: /examples/todo_app/update.py :language: python .. seealso:: * :ref:`usage/routing/parameters:path parameters` litestar-2.16.0/docs/tutorials/todo-app/3-assembling-the-app.rst000066400000000000000000000053021500564371300245060ustar00rootroot00000000000000Recap and assembling the final application =========================================== So far we have looked at the different parts of the application in isolation, but now it's time to put them all together and assemble a complete application. Final application ----------------- .. literalinclude:: /examples/todo_app/full_app.py :language: python :caption: ``app.py`` :linenos: Recap ----- .. literalinclude:: /examples/todo_app/full_app.py :language: python :caption: ``app.py`` :lines: 28-32 :lineno-start: 28 A route handler set up with ``get("/")`` responds to ``GET`` requests and returns a list of all items on our TODO list. The optional query parameter ``done`` allows filtering the items by status. The type annotation of ``bool`` converts the query parameter into a :class:`bool`, and wrapping it in :class:`Optional ` makes it optional. .. literalinclude:: /examples/todo_app/full_app.py :language: python :caption: ``app.py`` :lines: 35-38 :lineno-start: 35 A route handler set up with ``post("/")`` responds to ``POST`` requests and adds an item to the TODO list. The data for the new item is received via the request data, which the route handler accesses by specifying the ``data`` parameter. The type annotation of ``TodoItem`` means the request data will parsed as JSON, which is then used to create an instance of the ``TodoItem`` dataclass, which - finally - gets passed into the function. .. literalinclude:: /examples/todo_app/full_app.py :language: python :caption: ``app.py`` :lines: 41-46 :lineno-start: 41 A route handler set up with ``put("/{item_title:str}")``, making use of a path parameter, responds to ``PUT`` requests on the path ``/some todo title``, where ``some todo title`` is the title of the ``TodoItem`` you wish to update. It receives the value of the path parameter via the function parameter of the same name ``item_title``. The ``:str`` suffix in the path parameter means it will be treated as a string. Additionally, this route handler receives data of a ``TodoItem`` the same way as the ``POST`` handler. .. literalinclude:: /examples/todo_app/full_app.py :language: python :caption: ``app.py`` :lines: 49 :lineno-start: 49 An instance of ``Litestar`` is created, including the previously defined route handlers. This app can now be served using an ASGI server like `uvicorn `_, which can be conveniently done using Litestar's CLI by executing the ``litestar run`` command. Next steps ---------- This tutorial covered some of the fundamental concepts of Litestar. For a more in-depth explanation of these topics, refer to the :doc:`Usage Guide `. litestar-2.16.0/docs/tutorials/todo-app/images/000077500000000000000000000000001500564371300214015ustar00rootroot00000000000000litestar-2.16.0/docs/tutorials/todo-app/images/done-john-2.png000066400000000000000000001011301500564371300241230ustar00rootroot00000000000000PNG  IHDRL 62[iCCPICC Profile(uJBQ?A-"ZH I! 3livѮ'( z7jU7%YE0>~̙3ѧ) ŲOSK.zNT J ~j,z{^!g|ڟ;vzә..וQW8UV2XϜD<(|-<紴{϶p!`Mߗ)&E%D ") N"Yd6kdQqy2xp OYæ`HzlzwPݦ7,ߙsfmҪo tǠ^5ͷi֏.jgf>A8VeXIfMM*iDL ASCIIScreenshot_iTXtXML:com.adobe.xmp 268 588 Screenshot .@IDATx\g^T@@T {/DRh)i4cI$&1$j5v{],Q^ǞqEgvggfl:wza Pvm:pj*55tttJ66JB @jj 툛1R( !`ViE&J|xv5YBMId.A!H=y"Ae^J5BRoV:mK-tR% ӃYНKӛ W'eъd !`厳B Da,)sOb[9M) 5Ⱥ#`JE^#M̙sr<=GL^!P8 "HPD! B<[q#9B@! [B@!  qM$! @JJ S&_t* ʡ  &(KDQ*GF%#F@~2݇,Kc/0+{ '"j`0& %c"-??ҳB@!!Q2L! B ”g! B!! AɎa K)R{9:p`ǺB@<8~q7"-!uJ:yz~܊ѸiڧSiVdڵ/͝+M2ݻg2_#>}=rc/Y$EGf~ўbcb-R( 4:tlO5kP>|֮YG@PEY4[G>Rҷ|K͛WV`qz_@]s6nH[l-Pd*OǑ$! 2Ä ,ݺ8~eU˖V,֥KgZv% ؏2x:Ґ@/Եkc, L0~m.X6mFfNʃ;s(4}QfV RxkԩCFPD=C*VΝ;Qe|R28+mQv\VN[lђU63]q%g''8܌nŊQ@rק'o//dvVmRƍ1.wA|3Xc)S 5jВRF=rPp_j5-\!'&w4m۶ݻ?D,}:m*} \:^}e+X ;KѵkhJс7rS47N~&lСtejV4?UorSO+gϞtÕ{R~ h 8X%&&I~m2Fi'Y)ϟ͛7"閦ƞo@i׾-[Nu.^"n֧ٓJI旙۷߯ϯX"hقT;t{ߊUXr!V;wL%J)$$N:?XIU]x٣5kԠ~K%Բe[we<-ئZx҄&f謠$@ArR҄1,iLPaFڦ,¯^3gTbEsp. Ѻk?G*e髯 !۶mn!KKoƌ$JYi̫cBnZ,E&O{ָhw39~r k6[A,Uo0CEyjR(KO6R5hЀŅhNPjʎ[|ZjUZҥKӍשYfJY:m!۔̺֒Ĥ$iٲ=֛eIۇP yaid`YƂ嘱c}],DYgΞUyH[j-+ UV.Mb4Ku%v{ԳG*ͮ"M5iOӧ.c+ի_cӢ֊̜53]a 4fX~95'74€Y)⿴)E OXXV2{YSv:>%}Tkת%SC||J۫JڰaJ֣^{S5 TJrttM۶*w7*jtqu'NP tm۷\]]KŸ ^rUʠ/+`v~:DPftnZyy{Qqt@[T>>Tre"oߡ[̠(.a7"1e`uWY</^6oLUVXqkZ7oޢ=z ;׹tSO 0Kߕ4kK|vtIϷ,M >Ig4F_E…KTFu'SqAb Wdvű5~3R믎y d(alɨaD/Tr-3r*PҪUSXp4ٱcZpW˨OGGGGqOΊS<" %!JMMQ뙵ZV-JtQw(X ,ڹS#(pBte L+W欌'oܸAZ%;e P,o=wkNSsb S!ndckGX1{ۗׯNa==R Z7mSO)Wh>}d*CǒP( X=L'LTx,~b]BVYtVʇD`"/OGx,vP$IJ)Zz׆c`$ M+)W#GӔ$(MmbT^\W05kiз!. &ɺ0IY*aeBoHePuf;ǮOs2LqM]Y9ܲys:뒦mQ uֹ֭Zʇql 2N9p! 3oάxmj\q6x[ ^ wEΖDwu-BܪP ժѡÇz*n/K`2Ke0|n' ܼuS#&)j~+{V9tޒ8➞-ͽgW2(Ϸn"OZA9ҔR|׶M%iBIS ҔBzTPw#",!S6kޔf`ޮs'Pa?S֘&cǎQ rZGT}6]mO\v|!W3嚇}z)Wˬ3U;G4y{_?*#ބwLCǓ&Ѵihƌo}:yR) x ُoܺuT*~JU.=ŵ>ݺ|uԹ;@19x])viaʪҾUP4GO<,WZ[Km۶3]ӽG:X[ժU(-8x-+vbSB8`k (~"X`5kڬ)uc~9*n=[ "Kj&cC l Ѳ eI0f8/,2c=Pw@Z}Y "So~mk> 5!%SAP'2-l&^BIU=~,a@aBMǎiӦM8TSOq2Ǚf 1ܔx.]HP4+ O8'X bZD~Pǎ;v苬YZiV26"I6)  I``zz+[ gj޼z#b؅U#6/Gі%| \Xyn Z}.kʰ;^hcʅ67fcmxodV4A=Q6mZ˲:nSP-IʑGXrQJ^ W'!mӍ~Ɔ' 88tY/lߒ;z3m_਻X}fdӮ=q'ׂA_i*XcR2=MXiJg^O+X,-9m԰$}OϟL~5)o['KOd&P^ytKf#n:O~oO<hg~\ ]F"nDž ܄":ed (0O Lsk8+Yet;}c TqWVI}vJWY6ȵ Y*  GpFȮ¤Q̏E{kd';0Eȶޣ5\4AI<ƮY !Cd&[1"s0yȬ(H "B@LJiQ! @`8wHB@! @A'`[(B@! @~ҿB@!P GܾF׮_* ! B ӀDM aӷ>TB@!  a*d;T#B@X=-@t[B@!PHHB@!{Da=ҲB@!PHTHvLC! =0[iY! ($Da*$;R!B@Qr,B@VW˕+)3,%KGLV! ]vI-]ï{7|Ce˖QRe ϯ EDDPF oҷT|:z?}I>E}988Ru-*[ڶmKF>ԩ!= `Gxq'B +09::c԰aCz嗩]4o|3fZGٳg?Nϧ޽л mVR(UTO>DmNYi@۶mc%o<}4JLLwyW??isg>?ERY1JWx{{ /V;;WƊc!wSN%A! @խfti;w޽{ҥt i4}Oo߸q]VCΝi1b-Yo߮OˋΜYqNp;w֭GzV'O?&7-V&S\\j՚4zz*:qx>}RY]/Z\e / ѓZh#X.\|ښ8q{9e8H)aaaJ1 =]|)庽|22y>=͍6oޤkڄ=n8:{,իWf-駟UI1N:5%K! "@&ÞԬY4x`j>}::t 7B˗/v֧x"Yys޺u)J??O?]u<Ǖ5vQʕո(((H]vJJ YJYPu릔EQVt]Zjj_~aٲYŋ']\\͎ Ι+CoRѢToC~oSPN{9:~j޼=kKk](C'(f~:N GGGvkݺ5;, p9Ap7XuȋG<,mذN֚όvnݩFŖ%?x"+)_|RR+ :ut(Q4Ry,R7T۸>|n#Up}=ɌY<@GᘧTdZV5K%K &ljժя?~ֵ<3ԲeK{.zaggGvbE:)u8FCYǏPeNDŻaM(nOp-RTAB@!MVS/m_>}zU`>Oe(e˖% RӦ}FoFQ~!}9\m\G|TPBGa7P(A(RxZ𻥮1dzHŕG_sYRְރ9sFYo ۙ2eڄygR%ڄ X8s>ƍh}y_zevNU֬%K+%\_@VB@d@M GoK !_}_A!~'G]\r*ezIB&=+gvuВɓ'Q׮]hDV }A+be&>Ըqc P}e?]kׁ㋚Weaׯ?[ee6L[[;5W-+wرc(::qfԄڶQU+lzf '譊x 1:uGԃihmD&X޸qƏ\]]cNOu! Yͪa~8g6A܊ѱcfݾs6l5i҄&O!,"/snϕW_U=Jk׮5F\~87nTu o(wt]?X}6:e)"* 惌o(x9MKͥ<|A(!seUf>|$&&aÆ$uUxouB̊'/#?ĀiV(}%m ,KpīW,gXJօB@XF [-!]i֬M6MFx~WeiB&,OphOͽKS&CRCW'A @.?Q`=xt?SoXaiJ;vl3n|Gͮ].hӭ[7UҞ=U{]DZY*־}{iƌ֏ ৾ftUUP>u>eVsC`IB$&&~$zѼ>PBykl 2D'OKxE޽{U,Yjj\EQhCoFNoFVCB@! a .1UH=L7d\Txe9e%|s?nOvNe=Řf+B%- 6ͫWõ KXRhIKe N)[PŘ󢏂S!E [1L5iW! (Ly؄B@A@dB@! @A& SA;26! B@@B@!P TM! (Da*A!B@d~~e]m؄B@!_#W+w~ LB@! @A!雾Knny͋AލȬa1:.m|a)L1pqq-G @ S)%V)i11݄1mA6FĄd B@!C}b_sy5FsKϫq~̱0^n—B@@AH '1#vhn@hѢ+Mt8;;STT($(C7oޠTl8wk={P oLq&-^oe 3]] ߺyc+>.ְHؒq&P|,%$ě̗D! @A#-vԡC;dڴi3޽'dooO};k ۘ۴iEu!(|J:}mrٮ][:>:D& AbͨiӦ4u42QbE^=- ׳3m[Sppp* EΝKWO=5vRM曯)S>gZjty}+fI+rXyPPzJq:s()ͯ?HVۆur]^?)z S2eTsIIIog]ƦAӖj\)Rʯݠ yyІ1ʔ JUi+ն|!  uܑVXE'NewǏ;ۛ<==iΜ_޽hҥ3=S_/1zQKAT[uRŬ~^ٲtla=;v6Hhy,VX ,\ٴy ֍^t)B ף5KmذԱv%''ɖ (Od })K%}}iǖtN$U]5jF,P]7kMn{&獊UNpSCw"oWo,ȰH!  SbE8,E 9XO Fԭ[OlD-[R˖B!!T֭[rھ}5n܈4m%q- Y–@=VBnZ/6P֭ÃN:MVV1(VNNTT)^:޽h5^el Q_%ZѼy3ZnbcN…dRJa 4=.sMWPڷon"t9Zl9hjhjժ+gPN֬YȀrԶm5X6ldҥKwb5jhU FXѮJAʕ?$X?KCgF~w$;Pn]EcݺtR +|;TcB/NݻwCGSi>ԩVE ځ}ۑsјjVN{Ese#t{w+TRJ ݣ8lZu5"^JڷsroԂ]zZ]8{NzE)WU`kSY SoZW06 +Uu T/HWtZ~"O߶EDV#WJ.]V2l7o>Bv4Tq륁k,UVUdpa\buڅ塸i*- ,R4pVv?QJSUnnŨ|_x6 VjxmРHgg`B8ak)\f2,rCjTeWzZU Xk&Pe^2\6l؀] S˚6իxqLzy--|-_8kv;wܭ,y`B 1B>|([T[ ea-ܵk7QmdVAN8IwRR%Ma=:OTʷqttbE+gNp9]6)Ei뎬lEV(7XmTYg)q{o&,vEj%O/|j Ɩ+/W }ya.k{wNdE!M+L8Ï֝Ӌ/,4^?/0\tq k׮Qǎ MXyURl ^|U*[,E))ߚ5B(w^bx1O dfdW{i!bLIV2ג%Kg(P(І&恾akժsKnbR<,;uHjlW^S֞qu mDnݺI˖-ii6TYL0طZSiZDڦK[1w2zW"FenJ"frL&x2/pCi-v)J1sOi?O ZJnǙ~ΰ ! r&4r(|Ý|DD!/**ZYj 6}Pڵ}b`VfG,]p@k֬)0 =˗MTmۆ.^Iunܷ:wx4UV8_'>4DJa^F-1ٳK<4A)z՗9 F7D\UVB7bij-#^%2ԻwO1bcΝaE}zR/G>0g}ZQh ]\s"x\aZzb̍ .͕߸qCYze5#,+pMf( Wfk`c(/\P4A/_fRdǨt +◴X,[/ڱr]a ȏK.ۢEs|e%Zn= 4ʽ053-m?E?XK!exP2?(cJ/taSe Ry,/`qmϟ-RKlXvmk(uZj&Tگ,uGC{wRnm0:otm۶q-7iI[w"Xb;vo\׹aCR~do:}Y ! ,%`ӡCT<-eJ|}˘JV.Ƃ;m_]c17 v3g ɹSZ`8fXH-]L15q'S zfȪMk+㾲ȸ6,=O=?3/lN擓:ZY-MX%%6MaȪY95(l%~OT{2pR2kTS/E[2f6Brda5!P8 '"(Lc?,B@\$¤=Ul\M6 7 C(Mca.O(|ڼxmsc60?cF\zw2y;Ggjگ|7TiڇKjwNFޕkYmvj4r>-W<ݨr@J=+{P;\uD*͉lx_ڑo]tȆB@BD WsUh|?뻔L&PIq ?K>MK^ǷR:H[ Qgeͳ\uU&9)}~W}\?-B&-\s Ui{U-9laJVgǖ&! @(L!vz\>OQIiyc~3 +>Pҥ0}ՊU:y{ /P3,H~pQɇB@J>7&hLySM_g%ޢM5lp#+PQ+e*9UGuBF!=A798,0E^Ζ IgmiߢXMX .NoyNկV:piӛKokXi5%~?i&=Qr#Pe 8? PL ee(1B7::p>oD'SsG]uVq:hÐtoJ't6.?ZtbRNZGJpS{/jYG>GӚ{4J}5(< ;|;FKe6w돢KCBz : n@{WffLʶҚeI|ΙcэV1;z`u|l5na/ SvzWO@>hIDATjNU=yʠtJ4Ț@xuKJݿ>V-:*Cˣ~@_lPO\f+d\mbDtB 5?&~Ld!+$X̟ӃI``>l;=-K[Lߨݲ'huViӰ(pϣ aTcQ'j1v)ᶶ:KfS%{GQ}=cæM(L{o6ķꌤK}{AxoRi}ضtA `,}M ?\]݋jBNSTi>pjSzh6󪡬k;ϡ-Ứ͡ ]2,Mn#GHi~60hj],I MKc nU֯ =[1r8mF pC3wj8Obj/j8LcW5>ގdˋk7WeH {*F2%q4wnzVlT@vNtVeŹ('͠+g+[ޑՠĘyɻr0%FStdѧ>m1v.Bn\ߏцΎڼ7ݻyv~7*Ӳb+_P_Cp۔/VI2R[Yu-Ao}Hg\Ӽv_.m lOF)IV{T԰D ʼ3Kiʁԝ"*_:!W/^hѪ3AQ76j}+gI)jnձL K}$Wv.D]ҾG-n33hB=_|W W|?{9*Gϳϋ1;&~Zj*5+ـv-SO)x%Vt.ӳ[ޢsw.?}Ml wԞ-(ئV4G7z)#q_Q@g)PތPIok&1bEh\ynl>F)%֑~a2+%C>PϭQ`2jt9 =m}>݌MmF߇E]v3@1zgtc5z[017(G+/lqΩTًn֧G%#.ɩɬ4謇~T m6^١kjeH^t,4-8K?"d_)rΕ+Dz9N-י u㩅4ެn-&-;j ƀ﷧;-s׆cb$Raб36e%#L-LǏX#-ĉ&lYi8[:d;WȿQo07m:X̊𧤸4y]YH׎nV!_<hZT3v4>c*ג\<+%Y-Yir(}ܻ##t[7P&KԦ]Vz~*/hG[YYN dː&Pbf5&I7NBZG >d\AԹlkuywXaV\')Ԙ-kҵ\e5 {&N>5Yq~Prs,J*whsQHV0턷Fj/59ӔF:+ʚts7M?_ +'V,B'3DKQWXw8H?^'_Nbߕee('gMXYzkGC6tOΰ^^)5 eKlMs7d۵}f*Wԏ:2-WNWp%}|ݽ@bnu*^5YbE VF]^ KP5 ,+PyUW `\!t=&IԲz_wcBE eZnD!t7 ZmZgY+J?LoX6u.ӒFlyG+jr i˦{_A3UqTakI=˶Ȅ\aqo|G4΢)~hOϣҏ~MoS3m>]b%&9;1|3C˦S ("ͺ>hAK-f}s}8+ n) b%tESy4bjN={ IOjűK;~Ou)J? UF8tHwCZn;.pnU+NoHe|o!֏9'݊i[Etҟڵ)(((]αcyx _?(l*g+jų4@9Pf/()> {G@Tg]PǥԋqA+n29)}&&ZT0[S3\9 QrЕE .UFiʂAPPjY;RKT+/mdXId3[P>l*[AW>ں{vpт:,а'.3F}cggU!ú4G$[ -%ᬀP ]/SkiҔ|EŸGT̖JiƱ7TZ5Kce#CXmX溯yFY>=8]Ahw(7rL([ !YMIԹM5e]%jE }+U+!S l⋧fSX57A〼5\Eؤ|U=lOCZ#JbJ([$\xc|X`!^kSJ܏->iBlrYXtT7LW10XFt=6NPm݄Zv˪㻍况[јr$+Pή"Hgf uNͩoIl{ /dlM_hbVHc 7>F\kV~廨y|7pMlqteKjV7,|a1WWrc6(QoJ(K&(l??*ʶ7vƲNnt\9AM̍aJa c@*dsA839Y.'P/ҭmMi+az$Z}dNJϹ :g(4--@ػ"nGvˎGk4Y@Io@g0 ;NқA.1?fM>`ː kҎ-L0mk8NaBuْߋ|B{*Lo[04I2hr;Zw];6#Bޡl2OU<rPMpA2$[4єSkǵzdw=$Vr I3j}F\tڶ9DB 6ۯS){z/MW<^}.#p҂Xcl91J l,nVle7U;jMGqbCjwVXeJ' ʩuX4w :|mO JkӬ }n'CEmP pobVݑJ0VX U7?ي-_; .fX4ɛ9A1T߈oF7޿_ϣ %/pBA8nr'`eԜR ʚ/?l`oc֣;hgu>o2>?2'iեze i@ ۂU%mؕ7jQ:,QU+БklÕmJQč{?rփ)Cs<,coT2b ;P& c@Z=VO [:6,L?3gR %4qNw[z"z]-+֌\QRBx"(QD]=G>A-9: [ݹrE'׊҈g*3>.zoSp_p uF%ESj27 ljAc8^ҡLse=Zmɓ.pc,<+_^<;V* .iwNln=VB1&+vrԞ"|e_]otMa<}0lj/d[Rys*]Dw"qY?S,[s 8}%ʍq=^rjwV){Lg)|'sUU?ڃmu sC XYJ?=a,GKkyo+$0*՘ ;< *Q>L¤̪./5}']2_R_ N]R "U\RBl?c^;Zt濟ɏ_Mgi$N[g@ j ~?][wӆ4eJJw_)*?EL@|dqh DKl/&"N|g)_KR *\zl=13}!2x–.M_O ST4U[A}&Ue7QxlP* O Aєdbv|i[pP\qi#o#!)8hm yp!zԕqXWJcEcvNTfN.aQ @0&K2NW&E]ZEb Xib-j3cI;] s54,%q/hE V8= A4ٮY6>}.i ߢ+P.Y1~]FCQ|i{We/oH.YE3uyԾlN9~|'R`zF= Y!*ucB>z[:YmK'Oprz fPsep=[4{oho}\8ۉ yLkv|scY%]Y14c_qsC-<::tjhr4K20䜚lud[v'% D=h뙵gI|TvG{8&5vs}cNʝlkmF13;y<;c2W/\q6oaQlځE :Žh#)އ%% 9.q,X6v?$U-ɰ愧 }T}QGgVVƇzklS ~Fq d L ”i) Q}wQY! ']l|"/K^O{M|&-B@! @vXZ4(eB@!PTG! :QTB@!  Q ! V' ՑJB@! @a# Saۣ2! BDa:RiP! (l TJ#*Ӡsac,B@! rJaTJl# ! @%7|0rtt298;Wy~(xė_nC۾~FP3n :tq-FǗL[Rq O;{s6OXt}"S)eZ3V~&w*}=]ܱT??biR?JNJԧˊB@! C STv8qd:[7Z5}$U 5| 0:0=:mܸD-SR| ];]i r]3GSjj}b[3[?־ӎ\|ubhTmBM_nFwn媓GDcD0ʊB@!-*Lv&G Jcyx rpu']dPKr">eE V͟6-؈pZzS}I;"Oׯ>&'+ QGsVƐG)Uԡ^afD qfdE! %„BBBTeIK3ՙJ3ʭINW^~ji쪔nic~;m+Q,f65X!8bu]z@)`! BAX  RVq %Őoݎ%]ge1 F/T1E׎lo/")9aKt2+DQn_8D1l*Ӱq6[P٧غ!OB@!`)&4)3˒a*+6۾N5I}~8KgJ0";V`ѿЍSi笗ީ<^J6P̭ccA}zjr+_c&m܇TvU.;MRV3*[ZY ! B +6:tJ=tPVr% &(Q)6@Q;m! B#p?'WC,Γ&B@=]r"B@G(L ! ' SI ! B# #eB@! @ ”}fRC! xpB@!}0eB@! 10=b;\+B@d(Lg&5B@G@V'"B@! o(9g@ j9n;ҭӻi(9)}'B@(&/Hw-o JTiI ! Br4|0rtt298; yv\ų4[9W%kTEKSÑ - 2ԗ! B@Xy|qD& b$HԨZ1JAyըCKnE/TUEZ+!9'mfO=3=<_rN*P{UGet"j򐱭cU   0%kt66 7ITTd]a]>m5Xe&_šq-L+;HHHH ݻ!!ɼ,+*eLRUзoկ14J, )4i5E r   (S&LZh*I,ɶ6;gQey8v_n3hEyD$@$@$@&'`Cy/Ϋ^Ůφ0bw)Bpq짏M\ϒNC 3$@$@$@&%`SiIJ;J)HHHH< ܗ\iHTZrG$@$@$P*`*˅r,   (- Ғc?   JCl5J$@$@$PZL%~$@$@$@Sj.HHH(JKHHH*  J\( @i P0 TLfP   `*-93R֖V[ZXZ7:UPDkvX+!o;+[-h~\lmaK葯_ߙuMnjQ$YtRK~& T>LoՊ3G4voqy{7U7:h.ߗ~ nOi2p)M^KC/M oFníN3\< h:u}h(&yz,0W!upF&f>fg'ґ-Nhg,q#.ۦb`W߆~Kl+qZ9.+ ]N&Ғx|V\}8{j!l{E';=ppދu<^d\E||SZYceoޡ8p%.a2uN-.m/N<&o•'gE:j[99%5:z4jB$gu8tRپl>{sžcX@D:n D;IYlL=wo -ދ^.~s5ɰ‡>E[}OMo f*ī9f;7!`ηɼ3Չ]I_EXW|el#ncؠ*c:L9bf=Y,>?6YPߥ6jWEăCxȻ-^܋^~ ><)nvXs[ {%oKiWaXQދ[ 6Qȫ]g.h7?#l9&v;?<KNGZv|2n椠`]抐%]f`:Qb汅j¯}g8t ˻k<3m6Ex,aiej:nFvAh2$_'!,'Y  ;~@;%+R++׆^s[Wm/l`u5ѣeLg6Eo? ;jgސCʛeF%E!IOL24@yX}~3D=o`qtRBG]7A}0|xeG~V_U1޾K!eH:agi G^B~zB|Z>J{UjM?Ů`a/;Ƿ nzJy>=: X<_pv,Ej-W3H`=;B=~I N6 FnS"ylSEo%$ׄFޥe&b; CR fۼss 츴/oQkɐ?2]LK,+HH^`ҋNpulF/B1۾ñeSt9mbQ"~Oݴ3vV^hIh7bL;uNXZRxғgF2$W&SX{RuQ*lNꪸ ɛ!2I2[cQϘrd/2T$œ<9cya,{5a⦿<='}eF[FÚ:Fvœvkn{b*%60yH8wPI{I'Z$pyl%Wo ŮexjX~[mފ ?畷 $@$p? uzLTq^I#)IQ2Upj] !Jp&쟗S:޵n.VH_U&_ qC:TU/c]%p9[#ѲQޣx2)mUǵ ?R(4@U*tn׿nOH!%=0## ЩF0(υ->E7VIuEYXc֒*X/×+!;KOgdX2Q~qoaҾ|(Of$ ORzǮHr2Ӑ|~LYsHN>8*PH@0;G[ƖLZH og3e8Ҭ8V TOs ' ϹIHH(D#IHHʓ|HHHHE<]LV @"\oHHH(J]HHH*LU}`mPv pV~ugRLQb;ā؀HHLH,SPa[b`ma?{8yI(%= Vpq  Gf) @>f/ ++XoI9~#Ѱ F8'| Zj3Xok;m. ݍBg[+"z!kkW :;19%cOznN ނ7ctа|cHH*:Lvx9 釽GCբfmmñGx૨RY)7`z]5u?Ե%뵄{}?ؾ,mB7Ѹ׳U QtePo_͜}\;~mODu12`T]/}~y~7Z7!ߤ   r&`ͨ;? Mf(lܽ)h軰_O^FÅ+p9oWMudk5_+]1[cIo|91]?=:Q63k2oO!4uoo8j:zᣐW~񰱴 >(̐ @ `]l0 "+A[el]/ vUDƥJ{0&p8\l1ĥ&fvMaS`8[WA&^`geWXS @$`&^u"ԔSHؿOp_<ɷY۞U24Cs @&`SY,kdh "$JyOP<0kzt,ysMl-!Q(ɾ1r  (ofa* <τ[?ã~0.yb6݊%9<-G='  GR vcBvªYF$@$@$P P0+׀K'  (>%WܢYG$@$@$@wCnh- @$@T)&  LwCmIHH*% J\4 `ZlK$@$@$P) P0UmIHH2.u D~˯.(Z$qoE1v TBLp咥#7{ػU-<+l^v-0  G9WL$@$@$pR06DWptXZY!x4_aF\O~ :MԵ ZQy@qsb,5F\ݮ*wY'Wr,C}mRm+ڋ 8 NS7gx;VWfNk6x4TyW9 BН~ lx?E,6@;ufh'lxxŸL$@$@$P`Os+i]F '#E1զ7vΏĪi=V6εns!j'yMB'syCð\N\Fܭ<4خ̿oT'_'oW}ʥP2ؔg0C$@$@Y _G Cw9 FXAmd^"k;u戼ː\Bֶ U0Y掳gSIebɐ ٶ zISl L2$gHy|%<Co޴⮼*[ @y0ː3\;{Pjb,䆍b['u#ާz+5Ψ뙿Ll;W=G88ʻӲhR 4va@y8 U֠J 'GEaP݇UntPcb9ueh2ageojz117q29SZN.ۖy  (/wʌA1_ѳJn\{:~_BGU]ԪEH-C7U}uɉlk5B=|ā"ܖ_B{Dj,y))o{ot"4W1\84h3̸oN.spfV$fd'sX95aH}4z|v|!u~,Xk  ,BC{###+1ei<-=;B IDATxe|WZF$ ![ Җ:BB>BKq+{v7YC"1HlLJ0;;?g!v΀BHOqLuBuYW!P'”G!})B+ciA:g]wCtBP z4U޵;cX|$bULK%L慿XzC}l}aRRS/l#QttL^=;G?kt\yGK>h\S6j4ѷw~{ $:r7wn<ZT6_9΃_pwkYsB ӾSUc\݀:GYPKΨղ-MH+`*!;AR̫ zXܼRU4閼2]L 5:j!h)" :1*2"93 i-4u\˖@Y̛lw WI{ سΊ:{VqC} %v3$UG=_s"r7WG^p* ]+DTM/N!ulЇn߾ɗ}3Y JDx'P$e}uVCǰ}*PrM=OٶVVe\ANe4Z-O7ruږŶ"#.e X <ȓI\VV+cxbJL+XiKTsR(>Sj' u[B,b岻#LVO4Ry:(31-1=v:tZ,&Vϝq''GGǘ:aK_zQXCRŃ=vb>l ɷ/6u S.wBrqibde+-PXU?ٞ[[[Qx[ZPP@Je *--yU U,0,}M{{ȌfzDHƬ%ZJP** [/Wg]p$TGlXe XFb^BϏ:_-_6nܘP999٫".\8s洖;v̅ ;8pG<Ր~Fx"e;jrpݑqqn\D;Xpak7S4W7 νC [SZ`#3qs+*4,NGҷMEbR 2 5S|SFy r[WGyv}g204Pw[.Lͤ{)"kŹ<%vc'}@.)ƘJǾ -:[ SrcצM7>o+lee#DإW_/*#9_5G/G秩#o5{Yg6w3 ۚBe _$/ҥ߸ ;Ôٍi'dR8'g, 8;PϋsqP7~U?:M-YwW!aA!_ sA#+I?sF):qaZVpHtcƲē$O3ɍ9t*I6vvRe'Q֯&&;ɽp$zhyUBec^iw_F? [[FjO;|Yg~_.[i!-5>!{oɓ' ?s/BYt >ѿ=|Cr=4mDDDQQqo1?Yo߾+-- << YW!^x8bB GlBHa_!Ô7Μg]Pi-9>\+5M`67J*Kryx =@dFiEeqꍽ4>[0nS*G.萪zqێ۷o۲i?%ľCKL,ݴc۶mٰo>y}\.-n?3ʪv5{T41χ3B-i{_fmc4m g#/w#K h﯎lݝ6V2]:夁)9ls^]@;&}syuv0p{QgxXt4ZPq}oIcθ_.(2N}?#?s&oԱ{>˲4Mӊɳq@ ב _ic,`j2^=,!0bOWTש~5pۿE8Vߧs臷4SVğY_5nXk㠉-X8/P$ pM^Nj-i+RXvM?*ow܉}]MyC7QMt  LU+rMCQ/VBפ]*s .&ow҂Y]MZzͷz?SߎH? ">)V9to`@co9t贾 GdӻAyIEcQ[Vn/~e^)aS˜\y@LxՊ]@Yx3SZ*&6:Kiir˵Wn;pc/^0T)K5ַ_ګ=4oAho/xg ~wF|7_E_l YK[V,]鶔^Q_}팭f:{B3Vu/6O˼כԔWSQ@Pj*eF&wMo;z bˎ[֯z3' uN*5j*c],rtSqz&Pх#qMҶR +XHH#k"sk5 .va;/GfL i:; 7$ Sqg.B Ob{496AnU:e#kS_e[#_G2/}}C ʘ&+_:|5)6nۢ;|7_rdȩ#}.nߎ՗%gFw(M˨dZH[Rd=\Mgy&(jKr#~y*(kzty^D}ޚ7j0=)܅Ґ㇝^rQߙ4][uէ^}or3,x3g|B)G(˾yhsCÀγsJ;lz671{,zLj!K-'P<: ,Y!Pgi[QČ)hR(OgUӤg;\)ߩpLLʣgRӤg;k1J=&vv4<FϤЧIvmm̦j໎|5dۮXk`W#.!te>ԯs?JU,:YNd w?l)@]{ԭ5҄.e:_*e\8U8߫t 2wU-\װ!=@ O\S6nS!]E\.){I.;u2Dr<&S}V`&TE=\{Nr>iTKmkS{߼^Xqؑ#- yuؗUd ⤫Ky3=Y!(X$V}Gq%*\J]" he`Բ 5Cw.Gڴ[#|< Kɷˍ(SVR3Q',S_\RQdg$t,pLd|qZbY__gaf .`{tuy_*k4mMZs-L늋>Q.6(҈G;ED*Ql;YZ>n@3RB^` mm}By!g؅3rGjKbnf]F _Bc]yjb}AG /ߛH|ٜ`8~g (:p_5]elbLMk; "scM%:k?cLHYu^+;bk>Z#;#hYY[#/쩽RJ&V53%Jʓ.rFD^(ꀓv66v6] KΦKm]ݣc8-lt̓29N璔,[pwiŃE* cwB%5P\9hJO ;(7FrL̤F4f0iú`ou銘=SllCfv;xj=g`?/ K3|C)Rf$Sצ0 $R3G_l@ MrKRf3ogsIDATۛ"6'\ \z )(.\g7Sm: nPPw.XFg(C?s7?j2.!R㖶IY:XGP|V+`j K:3.74 }bI;)HI =VﱡUUy3銾5vl/h`9.# ssCީE;PͩCf=򘃻n7}$"Bw?KogqF%054j\CG9UhNyn0̝cuM ݘ,<:dmco`bʂA@EӅ܌Pk4-lmRT} )nJ 5#G[q KSbYE?]V["5G!-/O5b!:k֬[f~$˨-GZ^ڶ:;js Ӳ@6?w@[WnUHťCkR>kWYfͺ57)uwkg)bi.˯4T؆!=sce=J}ݍx\#fe)i\K}\ Az徼ؾwhh1("_-bhNUawQʆj%]ƛ4UQO ðϻvw1}C$V$߸fT[ 9U.Eŷ-`kJ^3Ǐ6 uI_VR`Eѕ Ǐ7 |u3>NUʪK#^m,SNNk`%'K(]eԙTN綥Tp_=Zm2xO]ɔUt.UӦukʵq˧g}L/? Uohv{`~O`b6[RO/n_(iҳ|ÔG!})B S!v 8.7pٯg9?0WWͪo6i}QU~?FMr񈞯t+YX1/t뮚OήF#[G?y4 rYפYu,%fx榶#uc1ʨ6$h{3_bZ nj?9eQjV;RJWyL[=*2BQVYHY`f(K !.o߫ʇ5j`YZʮI%}!/ܙֲ,.{ظZǭ  \&hxivX]Xڈ?dk9aJ]:ׄuTԗmq®`?t5JDSs+s1aSr0{%|gcYr'8w{m+Gz"BXXNqwWFz^Ck#OЗD~\}XOnHPUU02ـT0P?ݫ!ӎ{D*6;SrRbmZɈ mcK,p݇{.Qi5{1 u5|s[.1fnNP#4f Yw` o՘ LzsbdŠl0t(@ #PQ\m'HCrF\/=yGWziӏn~QEݡk˭z :G*PWz9XX7jqP!7ƻ:s ɖM=#ETW.'gJ%- -쳵}ST혛ƮХ*k:"(,nmKV+kx}9BSu,:`|V%B7NpF M戌[@f%q&1ߥ/6}$S@QG&tS vgR">۱BH3@L= 5ϪJ!<g8bB S!ÔG!}!)O ¶N?mT.mk5wf6!}WaO!~['V;Nľt`Y'{ yM]S7 ;Š]bh9vPF?;_-? 8{ D6ߦ IMW{-fuت+EEV3֙o}`a;GLK׵ksqoO>%bU?? nbd9v5; 8Os/G{mʼnuw> V׋UW:;ӄJee#}" ^xH) Ƕ lCꚨ-d4oαm7ܹѓ>J]+ZF7{Z{.)Rc)DyޘwY:aߔ}~kl'7Ne7OF:o>(ٜ>\WIE12hjkg`ЛI:cTJB?`lݍ_-*\t//qW*A/W<:6i|y] qV&ꓟ^ٝxx~"W.D8a inVfb7p_<ܚP&&45#|ߔ֭rX1+ xVvlMQl qPhk{T K>p3@7[T>Jz: $?&]\HJ͒qY4SOpNe9) st[ hAj8M52-'/uu8(͔cgV&o4 0M@&gkd,{;Pߟ|g~]UIPuM:m] E3:yލSM54ˁնKNv]ٙ%4]"n^dAߞs磨NJ_=>N c=^YZyje IY Չ/Y[\##>ٯ25b|Ϳ|rX;l9<|+4K6_J`:9|b+o6w'gL9b +3zbxs A9?>:qaOhv=9! \P4 VtWz{jp :-5~{yJnnfv T/ޣ m)E%N+ XtԳ֧s}iVtMBBӼTcL*x,lӬaܯ|1v>c,!m/t62Y#pR[`;eg"85n]f]d6gPZnW" Q2yzLL9#;h䗆'F6USxc۴#zV06U,ԃJ4P6A&R+z[i ިC'e}d ,@m!;K?ne(IO~6PZWlHmg+405Yk琉N lՅDSӫ~PKstT:U>Tnhhׂq{>joEXoGOAbW2Ӑ\ZGmD1St1sh`*\i}$PPa`jtښhD|]CojE({<˾Dn]BDB :=d@S7 է_1+E28b%PVMt72[Sz[j̲\(++CD( 4i5Fv1% 8B@ACsk\hψ ˲wL-6=*qJվӼ.^#x}Xn=v} P6|axlѣ 7Y;-{1Yn[?\r9U=p~AȖ`y=iEu n8E8CGT]X8\{ƺ޻:}>[ִ *ʲ ,2 0B̯dDWsݥw@XN s$WysѦfٻBs_5Z˲3-3K3 {@ϋ'W67ڪ1/IR؆r4nթU^U\ @̇8Ԉ[%gck?,ߐՕ&CK)1t*a@SI Zy(˞V]-Tatv:YˊST=VPo!bj6̐Bi+ĦEW u1 $j!z=y_sW';]j:V oޞ2t(VvV +i7_]@v闒aJ#VT_{w[[IW_q]wiMY%]|iLPK};UbhgvԸνU׳#1*we,Y\ d \!Qwτ :rOAɳWrV.0*T-iM8s+#W?MS'/kw\oLEN/~k/. ޳<W~`[ʎĶ Lwy1[彔X7 Q*YNqFY٭A++7[F}/'}|/1FW luJm(Q7[h ܌^RsGJkBW\$Hr~bNqj]ߌ_PgF&|~y]Y8΢z$˽?Fa]`3{՟%)j>3d=QUH`_!!>ÔG!})B S!ÔG!})B S!ÔG!})B S!ÔG!})B S!ÔG!})B S!ÔG!})B S!ÔG!})B S!ÔG!})B S!ÔG!})B S! IDATxg\W6vқlcQ4MLL5јI#j**;,mePQ=;sfe{=  !& (fZ}Aݏ{'ZUWWߍ]A6B  !^P!twBH;C!  !^P!v!iE!Uj BPCt ygyӢc<='0ӇV HG0PL({aB!  !^P!г&8W_}þG{g766vK!>}pJJ]Ve]~X}{9<moio"|+~b#)H<JA3D`{B$ɘK̈C}Lx*IqE2A˻ϔ߉NCO(++J<|v xҠ'ߖ{fƷO!!Kǒ?r"-?VB}}K=\&}}mTzQfԏ4J=R)U O' x*t+$͛7 ,,,,,,z@31xmwsttpttyoaxzuӠb/b|; ~9޹5jKf2E[G7냬Iip=59Tx}ƫ2gXI{7*Ƙ%R<>*@RCx\Z:8 UPP}TuQcBB|t#33CÆ?ӍfW{GJ|Դpʼn75j95YYM^Bȗӡ);Jm 1]I,Pf8ŞX__]񶴠 B KQCE$5 *F@[Zjkд\s(!!o@''srr7 Ξ=7{0aٳ:es)"'yn64qoyye7vjH=W<|D7.(dIk)M{}{Pxpᙸ7UW+F ^!}(ghmuJj54f 5S|S&CIY:= rsfϙ;[y0glC8rt̡y @ȑe'>l؂|_.mmxN{Ի2ค/OP6w>]ٗQIP}ڥ%*A?ЎNJ*Q0-Yч{%؀ G.Wh@}0vKэ%ǎK4LSʉCQf6+H8t2U<`<5IWZZY}ꐝmpppS+67lvg6͙;{w>CtFѣz57O8w~N:98$x!]5:{yvvJS:0,,,|^eĉW\;33<<<̙idhWb<*8W_oN%%+WjZPX-xHnlʌ}VڟwG;{r$Uw^Ә&͂wtxMS~×e$~ 謭h"ϟ?e};Ao}CncTNZϲ5@evl Szߟdnq߽;fC-?eLXF䝾 4MK.yY2o@Ssi+UxV|{AJ9O_Ȓo'ٰP=tjhV$7Oo-Q6/fShY%/|7BCm9^SLj-橪ӣwn{˿ȷ `r)OV?m9xeѤ Mmf&UCϱe'2Z5znmw9C\MEcѩBO7{{y^PXpKK; CyT?]ѷ7[nW.YYKM ӜQbƓH=@Yxs2J|1uBWkkß=QDQrvGo,^cgv~&Y7~^epǼ1[u{N[6—o,~:K$Ö9e k\Vޑ<>;W-]~WD;stOƯMœ dLk9U5F&WOgq׷ls&϶ܾuëE;SP_ΩUЌ&عR'OG.h3=WӴcg\=l8"wO\U[adMRF/$Z|_̸zQ]:'ui'з~ڼe-7oٴn{+B_{"ִ^'Rp 7& /UZE , 8:뇆9u5 ~ҟ}܇۶QZwJ$* 3NCa[,tKo:=*;潿 }g=JLA mCdch3KӣT9V=ӶJOq3/ps*.@#Bk5y 78N >ܕn@^}S= 76k} OH%Q- %PM 113{ɭptm>Izo?}Pgǡz )ԅN tc_eiF?EocWݸkl߾~ҟW<=򊫻v[8o ! 滦s5=@^d4a}qiQs^35I='K4]~̢)_u8M|E'`MuַB[` -i_x.b6BKoF᭏Z#2'`dIxMq8w0۾>#v _rqDmj@GFSj8Ē]_^)U |iΨr6XYQ妗{(I/: EpB^~gk#Ɋ֬iʍ,- JIEb֯k>ļ"%~Qn<)Aׁλz^B;_3ifY{q:hycL0A.c(;`+}MxkGVd 'M/Ϭv1f$ٗ|2~tehw߱e杻wV_~Dc|q~Y-ilL-nnbQBӜ^iJmM|lZ)//H飓4@5y.[V2~[NNz/hHJtiWKίvMyqS,Y]IzUdW^苉͍9)/Nՙ~͞K2ڡ?l-x%+:#IRQ-s{嘢!ԩ:8xvCJOL.l>_ʟ*N~x BHװN9BV!„B&B0B b/o@w!#ֶN"Č钲4MJM:bwmTl;xsJ{Bu5111ks,u-եglߝf%~-y}_n٨.=c;Twu,!^P ;aB!q#gL07PuMοqP0xa#B\(cɒM1v08VjnO8{>?F-4!s& vkKgӐy/ bv]+aҿ#5ÛBQV_$e_,~s<"x4m`]s4wb]f8^/ؼ?E}GtTtZFfޞzݳ1ґR5˗cO%֊|ƇcO%րAV¼G-HL*qRk38hiq5.`$JjQ#1*ĨzIG.WcA60y1CI[$jf6-]#>fz8G* :u3ZuWblP^%/_+ϖiN9tAy+|v­NGEM()+klCCqӶ5Ӑu(]8 W>n.Meժ{Fa#6@ BTcT5::Ι'6%@H75HymǜpcrQVu+A;v'J ] q5B$GR"O~.I3$灣½Mē'+'Tz|:챫 +"$42\ [ͶQg8jxk>UMb eڞ-p1YreWNY&37\$<); )b T=.=hΟm;?Ozz$ˋ:o(I }'ؿ;Jcw׬'{m1Kp9tm 1魾3^핣'z7ĉ\Wv1O.Om~-׮M3:)9?+׮U>w:onHv-_cիwC(FZt!LFBG6pEzfTzM " hY(@ =ljRd6o:0,NjEazobW$ɓ/-*+/+Hˮ1MgN6)̔P_aN=}R5iqT_ 3zTo7tEݫy(:f=z:3!, JH,;xAMp-IG"kpH!`( ቓ~RVv  @s΍ܰ&ϭR:koB+nTLJTg/5rֶQġsPHohI6%7<5M5biAܢtTE [}/*4dsGZ'e`e`,8 3*mZ}no}P/;eqzP6}xv(9wn\u*#;֓4t쓘x뙘8(>t 3NlJ~}<Sw&C~BJ;奍她%1쭸%Tdi*/|s 0iNb'롈1p dMM4 x͒:DA_09W:O\` s\#$Y`OTyXkP\>"t))cazCj ?Fꋊ9BDϮJ[UCm 1uu}= ) |+?oҢ*ꊳ.Vn`m!KrK;.\C ]{Z߼i7n#Y>^FwSye ƒqvAA{۶]UiaQϞ6mZR>佛~ڸytCMC5<>ʢ#/{2sf‡+[r[#27k5k s w#ȣYez=tEfJOO=V"ݭ1}a !iaoԙurZ"~AL IDATpu{S{I,h4^:ZYa5CG 64 -+O|NgDxaKp)(~LSW4/PHjn?R~.IPZ't0CF1n0lE$?€6L>sZ]J%kym.o=)FoR=Bkΐk) *_!Rϑ)bc^t5δ0=o`S" l:/CTEHoÉ(EoȎM1cwJ(anh1zƌ^ -&UCc\0#N!hҲGT(-:S24t.Rlε}c4*Ij| !(0 =s P}:\Z@^{կ\+M:Z g yM< =ÈՒ ;aB!K{BtN޿bwmT|;:ڮ.]եgl>~<»Q!؋܃z !^YB0B b/L({aB! k^E^42o_ow":ϓ8~+v?IZhwDPK.%eӦw/!Cm .&H))o`CYROZh1ֲ!P_f 16UЁ:kiV 0y.BuD>_}gIZZ1Zn;%{S<]:PPO3JKK;}m2Z,5L3BA3!^<Sqy 7b[ô8qzOas]|=Pnݲ>}6쵉y,)G=6mb/!^P!„B&B0B b/L({aB!  !^P!„BgޚsBՂ,ZBx=lnWG xlүOX[1PnŐ<08n;l߽2aKGv{81#ӆMs2K /&98]D@<~u!Q|9e+3=p/u5\egFO3wpB7,mom%]&k MG(֞YvNFW|EAC> MeqW|PXrK=8=C҆sw9<)rw޲k6|lHȄ/:@{3M!3&|]#Ѳ\:&^oa۲oPTT0ۯ[Yѳ.3C2?Ͻ4j4>n,il3Wt2bԌU@ /g*=)Ҥ,4߈Wc9YCVn{ͱL}WSUFKq!wӆIsoup2L )dzBÍKUXlP\X\w z(Z[3-8fso89m넞ԭJm55(.ET&!BO3jĢ1jghrOFT֯T݈1(m\Af4"$<˜K(~AI|!kI*WP=BV:YrQwkS"N fѐǪ6\ӱ<_8bg<'[=ɌgM',Y'š|M=3Q l4;SoVuQ59'sX9. #օM)hx-qjx^ݬ\5P"~]gw]n {BLz'TGۄrkZrRT;bljS`ꢊ93iR]$Q\ +~sJl]t(4J(22 PSX]e9mAAJMQlD>B&L96e !Y0t빘=^VhZ2@D#tV\5Oc2GUZX:2jPV]l;IW0 L}MNEPb29:O끫qƘ#aZKbDjkyWAtEJJxeLE26/~s^XVZDp :BO9= ճIW@]wtUy@Z{`efYcuژkלd4(޳̶QmOMy(- ۝7 uPuHCUw$_zt+ &r(s "&Br{aB!  !^P!„B&B0B b/L({aB!  !^OaÂL2?N!<iNz%$@'} t>_fBѲi{Ѳ܋2N/6+1TZZ v ];nB>:>x{{Yv|îj#1C>}H*--ӺB#qp0$B='5wgd5?~C g^„B=6Sb/!^P!„B&B0B b/L({aB!  !^P!„BgޚsBՂ,ZBx=l_ ؤ_c}9!yDl9 .<8G}:nFP@ 3rDžgepk}!_Z?fIE蹧`&hɱEWIZBn=mo2i홵oit:{ǎOw$iÓwX<}/.Yb{8ehB@!3&|]#Ѳ\:&^oa۲oPTT0ۯ[Yѳ.3C2e\./~iB@#_yOtk3닽C;H:jUCEkM`kc/2iEҋSVQnF LeP5HZhcyڧ+/ڞ\1~gZ nhd¿&9z+#\W[-^( ۦ$⮽?c^|!:©]7mؠKJNm x`״1(c|ꏏ?tUަOgUKy~z/)|]`ȒLQFGՇ>rxOr̮SYf[u1e:N(q؆Is֔40kkCGyb;SɞWU0Dl6rmP/r_U%ሽF9SoVuprrQ(|TiDHx1Q6fx5F;&=^:ZCגT{u(3ii'SGYӄ"f3f~4$Gb t:4:ɩ VuyO2Y ?|VIz%hAcLb<ԛUc|3a帀GZZ6]J:|Dk ũbh{Avs@V vuEch7> 1-E6T$1D(u.B(LTDzW?ʿ߻#xƜ[.$*@}r]E+!={3=̫ jkVWz{[PRS[PIs`N![4!;iMkCahWٛΊCkQIXyLb^C4JK^Sf^\ ʪ-az:j AbB1) #@LF;gUi=pu7.Bs{[:z88L[`[erAK}" Qo%R"C\9\,Hr-|"x@Ff]\ONPL힄ÁYפ֫cd;*}Q߼ =2\:emɵk2BWYV] pCfۨ&u<N:U:e]f$١;wzer{=P\r˕BH^_9`VaZOBX!bxM9B0B b/L({aB!  !^P!„B&B0B b'h߫i%-?]ٶBOaQ{YՍ sŁӏxZb?cK6 M6߳Hz8s.j#_jRE*ɱ8Ÿ!=1b_N>}\Ǟ~Y< uW!jyyyxl^Yɹ״!9R{WvĴ=DTW4wrBϓ6h7r\'K髱IrB T;!:!kB b/L({aB!  !^P!„B&B0B b/L({>x&!K9.Z->%:φM5:j?}ܿқ?yd\ܣg8k^}Lu&b Sw8˴a74#s͗=h  l}BC^umPآ$w-!AFڷs4gTq^ ktBY>ZnaL۴gp|˵*if=e(<L0J!3&|]#Ѳ\:&^oa۲oPTT0ۯ[Yѳ.3C2./~iB@#_yOtk3닽C;HZPGuDjӑƋ 5@'ENeef=+ߛg*`HˍVedAH7oz unT~~˼N2+':J *b- ?B'/ꇳJF&Q +Ts8/h{ʻrGSOnhd¿&9Zmk:H?=וun.'/I/,zOyrj9 bj"s /SI.w*y/_YN8_I3V$ xfX =h=,I`w̼-UΞYfp2'GrZIbQGB34LRfl`wWMQynD 6vsۂy3MLwƤʾN p `_h7efrgJ ;}Fzqć2u䇻z Q$F|gG.z2UG~'@-9ڍC7L6fƂv s $[V+;[qm gyV 対A"ۥw-pz{mз}z>`r{5!„B&B0B b/L({aB!  !^P!„B&B8";K?&+i)U]>㝭Zs{!VNCug2R7@=>T!{gvmSYf[u1e:N(q؆Is֔40kkCGyb;SɞWU0Dl6rmP/r_U (gcͪ.QN#;9 EԵ/j1 0 ƬhgФgku?_'BkZʕuV_IDATN`]8-Ŋiӄ"f3f~4$Gb t:4:ɩ VuyO2Y ?|VIz%hAcLb<ԛUc|3a帀GnktLAǫ׌nSk #8of %0:rn}b%ZmB95-9uF LqIb16sQ0uQ]Puٙ4.(wG9%\HT.܋VC{fz %W֌) (R"KG {,a\LCrha4  ^\qfo:+eeG'{ST`1iQ{ ܣ*-,zM]vxar5(b\Fӫ U }Ӄ&"(L1aUUݸ Y`cJm`|0moNKrAK}" Qo%R"C\9\,Hr-|"x@Ff_O!ݓp80]=zu uGW/^?wVfk5V96p@FC=˪ylԤBQ)|P 5]ˌxZ9;TuGUO9 _nGKWnRik+̊ 27;:Y "B#X )G&B0B b/L({aB!  !^P!„B&B0B b/L({aB!  !^P!„B&B0B b/L({aB!  !^P!„B&B0B b/L({aB!  !^P!„B&B0B b/L({? ͌IIENDB`litestar-2.16.0/docs/tutorials/todo-app/images/hello_world.png000066400000000000000000000113551500564371300244260ustar00rootroot00000000000000PNG  IHDR_sBITOtEXtSoftwaregnome-screenshot>IDATxw\JXEVV-v{O뀶j+{ m]jUǺZ;2 +!?\D >~<^yV"˲쵂|[xq̽SB*P;Ό0 U5 T=~@V[ԪV^ /E"Qd䜠 +OILSWWX4 M _BzoV۶G,/X0k݊]arrJf<{Lrrʁoܳ7P̝3]3;EVw઩X.C]dܓVWט|ӧFE/OLܛOqswl>zC$ymypzaw%<΋/ x9RxZO`iV\\"pqׯɻ6}z蚘6%K˗/9G{DͼtEC?>ZM }북3!dQ e~ݥ~&s_,RWpv+!In 1mWm.a, @*2l^{Z#3?~=<Ƶ6UL?["$6?O!^#/;s#Vo]2JqZCXCcNP󳸸/ GOfO1ʡ/7'Lqy!t=B^|3Bo5?o;uL&?F&ݺSTalǮ*vW;%|[;,HS}+XY[1]?Z^sϽ8CJՄp>R>8S)U,_(`HW5)ٶx|]ۻͨeffRuQr{R@Ti\pww'9AWZR~Q]1vV|Yy+qNEf^Jg 0VVk1lVn!a,ltg<#lrR x v/3-*J Ԋx*}ۻͨϛ7RSRڲ\7ܗ3T*MM߼/Ͽ{AK N0 >{ڗ/O;.x%tcC-G^sʕ:0wK-\)tuAIoG~"V}+i˸wVWhea,lj+hݭ:8i_:9+V:w7 ]4hAhSNϝ1Ӧ:u$P[[V|BUVnf9)Ba8>\k#]g׌tHB?0inn:Wd)%lC8 )BZގY嬓Ή}+.;c:_imrK Ch鲷#n8rTCr a ,dw#{Ba벏;1E=g{ڸ$''߹Sl~~-7}KFG/}u?1U?&E{&,b;G7<i68z˵1*P7l%-e^aAUSkc`<&e5¢+^ xc|5ݒ;<`,e--6b=@Bg8Ǐ8NxxȬ,̽xR\S:@D U5 TA*PQ6j\{Ν?v,m+?g\VĽ}}8WoN(Kx^ ' w"Ho?~e~roSgn[UI @pkglJUw̶w`85wJN?띉}5\ȳ} Pm|\Or_sa]@ny؇ȉ))IStpNw!+#݅][yETB寧]Z=2_ĄR 9f~t<'/sA]"[Q1. e#)zvW/Z1QAϾkІ,N^"OKBoԌPc _^*oU\\Icu6~A>Ev-kUW^! ~VS'Zgd2;ސク1nv* !-%74XטQKT+oXi뀬Rb[ZzC zI(;=wF=`v_2>K4w;g]z98w]pXMx1qC[3s.lʸxB}z+ѷΩRX> hJ׿~̙3ѧ) ŲOSK.zNT J ~j,z{^!g|ڟ;vzә..וQW8UV2XϜD<(|-<紴{϶p!`Mߗ)&E%D ") N"Yd6kdQqy2xp OYæ`HzlzwPݦ7,ߙsfmҪo tǠ^5ͷi֏.jgf>A8VeXIfMM*iDASCIIScreenshotmiTXtXML:com.adobe.xmp 219 642 Screenshot 5@IDATxxUO: $$HwBQlDQQ@JBB5̲&6}98;#؜   Dt݀G+#@@@@ 8c~3G `1xzbἷ XJwKQ/M_ @2 cg3 ca  I!CAE' `$%G[6ANhig  c,R     _C 4l' wHC@֬Y魷ޢ  8ۀ|EUO6lyyzORiwciV١ ӄ)((d{3gIcƌG܎L!С}GYݢ\rQx#[giم\\)2"YIsݬYQ׮]ITlGh4w<ūpBчT=h'סG|_fEx_yez;t9/kƍi˖-ςI,\P8p^~I "@@.8ԡK‘#"dfM8ϛo;Q00J(N۶m,juEO4ct)d_%///04=ʞ=;)]YD L!8biʔ /y00`*+?)BǎӅۤs@ǽ{)2*_|zPBBܮoB:u[tqMIn\0}6J^,}ʕkh";`qmLCG(5r*HW\yʛ7wAߍK_}5¬8>zfzr!%!XpaZnYQf ukWz qㆩ")Y*?b `/gϞ=^OISɒ%U֯_R}=;?av޽Աc'}YX0UZUD)S$*mӚFEїtYܹ+ZDlOTWD6j܈ (U(]}^m)Wj=_2o~ڿ~{EN:ܿG/ȧ76a@ d :t^Njb/<2ժW-#puw*iU3NX\‚LmâgJJ]ß믔4Խ[0uÆ 9Cy>2>#ɓ'=VÔit_9k$‹gQRHd'4<>;Fm'9s椖-[x!%gQ."PBy=KzuM֨QCPڸa#=(F*UHڒNu)[9\{e6Y܆ @2%gkpFQ1(IĠ]uV䃇3(vݿo:D$ې@@>sFawOBuՊ㐱4ngŊ)@nۦ ɛW_vZ<?^]tú[R<6r6-_}YYiSՋCh!*ٳSګi0IԹS'hj|~ҥQw}l/÷2$EW֬Y>Ş.}{Kbw6lؠ]Ttq/^\]]iӶtIUGP7Ԇ3,dJ ϾMj{^M1Z>%.`Z!ؼyy-$KYciԺUkƩJjԾ}}!I׮^i Yxuz۳gM"x\ފ+y2+T7B@Y^d|PBmj$gxK׭]G{x$]rU}Vt6*T|}Xj[v&Dxfq<٦pٌƏ֭v!K72dߣ{7nIK~>OfK>r̙7i2qXy֬c~̨Ǐiԩjx!,ɓ4D;'M*T@/PL-:yRGB_z502X7_߷=h(eJ"bOyC_~Yx7gʯL5_,ymn ZHX8(0 E{Yv"b~3K^=#V㟼K%gXPK+$hy6d x͵cIF>K?1⢲Ξe勵`Z-($Id  AmwoZ=j4dCS:>z6/R ꆹ\)0fg#`~63P@#{Vrb t  ` V9k6@t_@[@hT&dF)'   V'7J[*@9,ݸy)# A@@@H:&e)b $/A<20 @@@lL7!:pݠy{#ho!!> A;$0@@@҇`pF/    `w  H̒ŝ ,쎦=ؘʕ~f*Y0G;w/B=7-^]J7oΟ?߾tRʝ;' S|),,Wnm?LO4dȇPv4uܕ|||,ŕʗhQY[jذ! n+PRfA9rX&dV$]]M>|8UV{=jԨ1͚5?L/2͞=ڷ@}6MyHD/^F右999lB۶mc:~Ч~?|fW_}I ̧B b7nwy^2''>V.IlsŶ)IH3ݵ˖-C4sLe޽{ҥt-oa.]X NaÆ7n+W.+AԼysZ?Nۗ-ZD۷oǂ{-ƌ_Y'H={*VDڵW'O/̿@C~uEEEQzJ\;e{ UV҉ǓzRyMoܸ?첟|B~ԦM[S|%?{ƼSjkoنlYe"ܻt\ ~~9^WC._L$ʕ!///ڼyS"͵)C3gPJiԟ|yJ(6l:uT  3y h+[, Խ{w?~<:tE.9g*l2pby}PReTۇ "##YNR㏇|o?~~mѷ' yҥːx={&}7N%p1N mm7ޤ رcػqj՚ʔ)K G/^duO׿;BPq(uD|Ć8*Zݻwbcc-͛T"j>~}WW ƌXTG,O=ɖU O\hĈ&[]ɒ%_7$z֭vE~:?h׮],JsEyܹvǏcPRɵy Ufj}Zy 3fZ&O7էG.ZP=\ `@@@R%kժ͡@x>ɤ I&M7m2>^-S``A5QDʼk$۴Եk7f-K9jԷԲe 9ҴI_Њ "RlSkԨA!![YUV^?SBQ&<~/X,"CNTYu؉=AO=&gWm,anUyȐ΂x(bԄZQU+XT&F ``JHYfj|}(o޼jlqZJM-䬕[n្5mڜyx b@@2+x֭x$N^Iǎ&BHw ԫW/Y&5J< 7?OPAG5k֘4CB#I8qƍj?(I&hV֭fEEEݻwIY"& 5?9AƳɘ82;Y^"3$o?ae$q%\Y9V{\~_*~:GJh7P=ThX+cJE8qyf޽jfBB\YfUۥ -M ;NL>A@@ p`p@lr(d<4f*WØK Knz?_±OņIM)k{-!=lN>,[{#*f״$KI䥴=Iv"Pv:5Mӣ{ ;@@@ uR5F0uM4=@@@@ mM=@@@@ mM=@@@@ mM=pΗ=x2{6X7Ubp6y$6yxx$m{K>>4cQ8hќxu0pIEsV+ZR%\*,˓{Vǖ Mrʪ…Və;NXh내mS}jJ.lڼڶjEosQZYf*+k+?u0]ɑqXbDye6#n)^*TE{woxŃ  `} AOlw(7ovIQŊMo~ڲe+խ̞x ٦ԯ_G;FTJen+nQ@;{X\ꮪp"%ʞKy~zMNʕ'S+Ν?h}ukWƒ[W Mk׮{vI"(rʭ`Pi5]pajܸҙ3gieU~H;%J JJWDk8 ,H 6PqÆM&͓'zzjV=?@TfSX1$W_Mϙ(eyʔ)M׮]%K*ϯ jB=Zv=>}/Bd?ă-)GԺu+>t1gO K{NzAsE){sjMeK=|JsIDm`Ǐݡ/\k@;lZ*U'J۹Ul+WáeI΄c){l^wu=;;X@ 9ZE&a렢%nuB* %uv,Xŏaawm%Kte% EPЬYiN7|>!*)_0ͼd*4(7˗S˖-7U{'Ej"P*˯:̛@@K,:w/PbM֪//O*TBeojU[Uoo*UHa5rwϢĈa5k*Ojrvזطnz%z\jծ]Zj#%X6a"B?fŒ֗gjU8D}DĊf6+U/NZ!A7;&cd[ yڴwz T]wܭ<ÝͣSb֏ɶ޽{j+}ݵk7ߣHn;q$ݻΥoJݿo6dϕrͯZyIvVr׶MJժߔE=ڼn Vnf-7wf% $6oG%V$o_|jW|G=W/_$ߜϱW.]sm>=`@@@X_~Uu܉Gyd5( '-rE7.HK|`7nPӦMԍZĀxdaѢERuH\\M\ pWV^K [7j^OIWbOq`ᢊx e bm5g̮~6R=:fqbQ;6gRK"~jz"M2l3GJmZXZF3OHriٟKSH]0+JlweӒ"ث=}oT{25%(kX<ަ^.mmҞl}g^G@@Kаa愍2,oeR(DDDcȲ2bxޤMXS퇩ymօ =dnsKZ$מɘ@;$&^)"P2'-sۤl0%&`zlmlWh#hc6@@@iZ0ʀ@f' aa$g|t`ؐ@BPehM6m_s&Ao svCi003k H1///o5/lk*={4;.>{#khL.p`$"]zzҊ٣햽ڥgQddGi 6sC3{ױw    8ߺuS"scރ@#ḋ{    `9SHDB0@!!y5@@@LW| #'go([pqwUr;܆LM bײxRoѡ,n*5;[\/ >_JW}\j H#iKTԠ"Mqt--(GPvVbS@@@2GܽsQᆯKGG*Ԩ79dIU=NLʛe)!i#iVO&%GW' *PSq%Jg]9=(xpէ   8 kw@q* /YݺHFwV!܅͝}Tuit]T#7;H{Pq6b)EGܧ3ѝ@Gg >v؏"$s6ENŚU:B[f@9U*o'7O_ur?>$',T锳D- ;7Q.Y[?P)m5m Օqaңۗb )!>F雯ԫ 98:ж{x+;^iAg_{*/oe}],d|6#Xw|{v?׷8 P *3JM:"%=dѷRtVj2W2Z>MU'aտy)oѪCA_ĂDz]Ѿ?+Ѕj.l֏hb5mGm+ҨeɞoJԗQ+?C@u>?H>+OPDulR^.vt0Q^)atdnR\zHκSˍ[,US}3y'{y"2~qK祽2't9ZYr-LD   ωM^ST˧v&K&w/:i65Y -WZaj DbaxCbT8 vx3*sf*LՄLy d>*+xͧI†Eц/]Z\M ]{^xqe*P'?5SWM1y!*԰QӦ6x184i,СCV܉ d<ֹx$]dr2ApY6}vUBC4L>7՗9l=3nϰ,S.'^w!JI"{ TGP,HIԔ03eCTZD{)vjSg$FL7fS^mJ}7;@@@l&nKKMl2Y]     `i.     `i.     `i.     `ͅktVurul $jͻ@ /Qr(?3dw|Ys]KY-?WʧS*JtCRٌDtQ8Δ T/Knf j6wlgP;r.Tѯt$eܕagikዖ'Pzd%͉ ru5׏9dlϖ<]],6m\TkEn"WinQYB~E*Q? (VTGT3J2=ivIjN_Uy_-貖wZGn>]U. |_rN*hNWlOrY&CVT({m⦿Pbfjؘ">wYC6nEG9c$k$+I>i:.NoM_ )ku6-`KlMtZDhGe嗷Q5F?{mIq:0gm;%gC:niY_*K=BBV}{B>~@n^*&?ћ%6G ҊfO +Zr*S{h#|O T7I*ȗ/`.?*Y\]}9£#N*Tv<@$YȋO sSP u?3HӔQТskk7i}Nau>Џx$^=WEj׺7nERt| -Q"FVBFbq|4]Aqj;yp-jO5D+iɅ*_c)3?˚Ϡ-vQU'u-:jۈC({NhBrw{揉aC+*~¶hLՏJooG?Tۿ>.ڑN?Gq މ a-৵6Qљ F-~OnO #cT39hfqT+/JG)Sj- ̫.=έߠCw!} lK'/-:ɓ)?=O鏄 Gk8۴yQ*t$_0}wjҜ Q< nܦ<=~D}We[_Iy=rQQ>[ {Z olN/1~x.HztOpRGUh4xH iOv>G1mCfRoCvP6W_aޚ9ys]|c!焄̝+b)F+.lMNmӷU>X~d-6~_SjE}"Zg";.L|}ё;Tmjsrj7OuU=Rb~/ӛ[>/.;(翹4g4|ޏ2;(DҋyݽPk:5\]m1צ|%+ 6H:uG# :+AΉ!_sw>e˯>Ilҏ,Jw!.[+X"Zo.PztoƁ|.\+UIڐB'!lR2qj-fw# u+҆x''%xv,[NT' 8P-*e7Z|qU-;LmZ~y#U_#Bc<eвY;G(H]MYkQGpdpz?|j̖TS#vb6ESE/=݈A]0\ :N=qXɷҪJPmk.M^q 1"@2uT{>?(&lâ*O凱O72t/Z߽R^膩&N?/%$.aOZ:*~b~:>FVBC3ꍥ[s68~tov|zp+I#vJl2Pk8rj^|fO/ӆ+52ӻ wM..TTD[;FT7MW$g J*]rC{}nN.TvG:p">oZڎ&G,Tbchɻ(!&Ƣx俤+F_|?_؜"^ǡo tt\,OEO_BImZza>"~X-,e'"?EJ#OVOc,Q+)61mSyH5LU5opxkS" 7μJ#St(OSVgr$um]jj _Ć~r[{Vxfu3׭>[64b:z'󓱄4RJ]Oc.WomS. έAep(n8 .}?QA)A~M]x$  :%}]Zk|Sbe[^6^IK[eꥄS9hxN>nZ-T94|ꆯyLK)Jβ,P"(|$׼mĢ@iy%Fkd<}xwwUt' bOy\fA3US&,|T/7N &([&ϊ1+GVpd8GW$^wFH:ϭ0jI;6vsuzxf>')g<_]: Lާ!|~Ͱ@,V$e-}NzZNJ^($/ɜ 9XhC`Slg<-k6ݤ<(=UxΑe#`2A;kl4 ;m)-?1=$DĠ@-TGVxQO W(q]X s7ȉǩI1~z/$= uHh21Ē4't `o_xxr{SQNs7QLRw||.Lmp7,:^h<_ $=0$OZ#OcF"7%8yoIq/!sIZ^\@%q;Z2oV8Dʓ G~6 hFCh$I7W~F_Tdo-{f봹\% [seŻoȧ4_E8{Ŵ$JD$i&6$lac̝+eL12nɲQ;^2++鄠a5xڴt&~Zrl)TgmkIcq +ZxR^.A[UC\F\ca%k"2 Ӡr}3*,j/ky脩T咮?}H'a ?1I<)F,Ir>c70\Ń%\%돉Lpx1 u>T<Ϗ#"\0|X6~@GC0Ql[\w~19DJ(rb?%$LNOD;GsD `)qdHD4vV7~+oFᓛVjoI_6{Rlt{&Zzx,۳Vx|w@ Wꂔœ=k)_ǧPY)jzh}chBƃLcg~dς+˥ (虏D g8FDl] Zze&d|Q_fh{:gSO2YKӳhS"7dy"aJimŞysUP ɚS-K(5$7A$ajU<;PKOwx Q&j,W/7-A.h3̓'`feL-\-E㼮R(|JM46UkCں6kq]|wykh~J8VlM2Oa:y,{^9i\1gvKXIM+hˀj]:n<2]@MW:f/2Xc#J̽Yic{>-~096LSrw׭SPsՎq}mnjQ'jZ& #(et^ڄ$d8Ư%iaM*_fPiKl9}T}*%_o (S?VZ4;<Xky)mw%{bemVg' VQ WÃب%yxO<>;+c7CZWU&lW 1Smzě!dLi2nY۞wBk3ފ}j/X l;5ڝ _iIaRz {!CB$!ڒ&'yëQْsדsRWM(:yR &]}gYJ h؂]e|`eF';Pު]j[{j_tC~؇M9Fiqrtz_APz0byH?9J ,C | t't7mF~[vOHUA *JqяGA9$/+I2^K;QgTcd<ղ^la&yHm+[ִϕw^Zʋ#G.ti=~<@fDx; bІǻ{iʖ1'2>MZơ-grXE7>Q' [ ۤ13Oj$4/3V q7^v|>Y9cm\O;9PX{~pb(rmT]$D}}aq><^W򒶲ΡK&2^jғ̨-sWH@$8Zᄑ_k.ۧC˗7u%],g-4œN$;U/ϓ lKȇL*49KSd,I[oUڟoL/+" /v6X@wh5W_ ͗k'O+ Tce<_ec4Z=p$-}CR~;JdEmȅ9{ +O}oFۡ\ƓqW^$V+PR~]+4^O&M%:Y[s4ϜqHVPؕNIfV! _9ְ {j O.‘ p,ɜ/o7O5$S,v'S+-} (﹪"G }_掅9C |$mo% ۑ}MnI$ss̝+ʧ5_dGn&ͥ[so~z|Se]G'KyXGȓW6ʱHX,+-];Ar?1]Y†v7pz(*կi_[ֱ#Z#L @ -"PAjS}x-2%nX._tyʖpd~r]yy3@לo"aޒ}2,cSZJ=.zlжkr~ 3癖MlH˾k+%ܗ IDATxg`ހM6'B zEt&"`P|իU )JSD^S!!Nfwݙ3@QaE;VTT4"q)Vd2iVvvUHnbWbC7|-s(@1^~a3t*!5 T,g|(,nd1@d1 d1 We8{,e2(d1.0t]?rmEƤjެwYVi,֥'=ϭv:{,lݶsc0N)}nVN}-U:[\Z41npk5l19{_:Ykˮ>yX_XC#7QTP"\3t _ú.?,[Y\'rl;&^TVOGW]v/lC_{rD_#|0F<`#j&]ڜubbtm& կJj9b>h(?|֜.t5?㜚@RұQpTswQqz^C;0Uq=v?rɳ'sX7w\9m]n+"QJ3Yjwx۾ɾ'/R?j&g*JƐ!F&E۵ERrL}vLD%.^ӥ@IF~V'd#NbdէpȒS>*&}:S<#iI{>ĺs} {:{NR(2vo?Zao^xRG6Fi$sEU"C T_fj<<ĸ󋫸$(1EDL:Ox{tQ&5-K,Yr!|-~OnOܴ#ӽql!J3s}@g/}1ͧ]yFώ;hU$"Fމo?_ds&u;sM/%"g;%{N{Um~w\kM5~H\X=^J$m˲qLX^Æ #sY3~ݰYcs$,u3^y3ςr\*7; 5=؊.tdf+$i!lsOKejdzuk; \<6ށJDOg&]^uo3FPʵQͪQ]T=N}\Sd׎ >JS۽WK0_v:~>.M}!TYrQq[H\/,[Myڐ_[<굺kt߶."Rqz~ŹoWmՂkv5架,>~ts2W~=ﻝOein}pPM>X/9#+XY}C>`lXODD$ۗz|+bvKxK[MA-e_molӻȮ1JDO?gGM =Ūz?PskɨnُsV/mdqdX88\}wx0MrD#sc>]bi6fbeK&g9yx}bkRRDFFɏ;zMn&Q.YtS}+ȿn:arzҏ}r18M̭6S?;p%FV#} MkzĝW OCD]r5o$|twDl@W%,a5kTo[F>ⵇҋ=+Us;͋/{6?~3o-'y;~q#iڭf9?ٛ5WiԤeH{ŝ>wq-bOݶ̍o-Z={vΞ=E˗/w֨.g /k.,T1=Ok-qDgW?""JyEկlqMb>|_^w^79.bi<""mVmo;.3[g=e꽬̆Evj~>¢knӽWL5۵ Hh69Tu2]?h3Dۡqi޺'r2KJO'MWᶪNsj.s0aGڻ-ήv~#p1O4ێeK-re;D.~2UjX?@F-+ҋN;ZFS"aҮoWD#Ȃs2stS3Nd"檭EXDħm57%M;ռpn.uW"!V%6Oٜh׫_g)n ɴ_'rtZc Oުye $O?_PpӬY1cq^JgmڜQ8u޸oZ~Qi^;˯5:ޗ~jŅu(Ǹ@IZջ&>{M{3O_ivȆ2_|] kGŇ6YP:/o쫿 8>F-MVne&Vz [Qrj"-;N9[7)2DY#kᤔuk6nw-HtϼYT=x\_x1cĉ'Nxc6\ǻܙ{i[rr-X3uN\vxٓ ʷ$Tb[7ѷ_.<}~ Tٙe[Ta-r2׽@Z:|̂us;G6]s߹gu̹i)gMls[m˧O\5<4Se7)0:SV9"Ĭ2liL8ɉ=*_ey}o;Ul(ך}ַ(s))>u*&ZPwDz~r$o㌇og Sٿio]mXpHG6 n8{|522.{qKNڹCCCOm -?M&5};cv/ qdήR=M;}ymZS uU|o2=W;0h[;|{~ d&וkl~m'vy<P]}o">1>Ga{ύ[ߕOQO TpLI>O]vkT6ٲ|kTnzZEYc0D2Nۺ}[B_&OS,.UJ;|$9㊓ģHD4{Ӧ>ҷaLbj[]oT"&0y#8SG6,{jQ"ʳe&>T1Fbk]r~lO"=>}nrYR2--inuT6lwġ1Wy;wwTirU,5qr;'4GG2GuJWʔuplgh9ɉ#+`^~7g3n~wسoy-v{NNΐ!D ~.^U֊8ŇKϣ{^^!M9!&j Ed޼O|||/1h777u|7?~,Tӳ)M 8^縮m"51.zM[7n\ It>G5kw:\jvH.H nſ60F?z\?Zbd{Qn? C|Zai^nGk"/ )R%%W0|%%%_w[RJӴ&MPƸa'a4i@Ӵr;p)fswu R0 Tfs9e1ܢR...c=M4tpiҤSO>\= Z*-UfddDQ ]י0ƍtju,b[WimL&wwwݮ뺳G\ifbX,ܱ"u)L&i...a0U*Bp+*rY Y Y UwfrڔRv.a JYMsRJihU.}b}&wf/<( !!!!!!!!!!!!!1߮MJjfDT7;nEaw-6)bV&eV&OIJڵU3vU+Ǖly܂ʵ0oL*dN 'g3SpHpk+!=c+[{oLPG[3X )n,93G aI#dKRi] r" zeUpVp&j͵Ǿ?{Ydos\,Vn15;j奤p3*ŢDwPp2E3)E3G83mkY|gs;8Soa֌^gfj,ev p aNݦ~}*@_m3 pc8djΣ_\rQ(o^[z֜2Gm'~k1V)BGr4y&zg}d9v.zq's;j҃3+'t]Dd3CV)bȿgfS=&;nXy$Cy>R1:"|s}ml:([op p4gVK4Ȅ]18kw*n :e%>wG\;iSf,{|RIAwTfwɺO60tL瀓{vαNd*Cռ}nRuLh|%6v|Rc_7 (?׾n5z ?}αfUbնG\Ȟ#O<4*lDDGnL6†<gO6oT&""Iu_LMu9߅GV.\bK5V޵""Z/]ko;ZZv8we6`=kE cۋۼV/Gӫl̚r凉H i6]bq:[|?Vŵ0#~_R=O[TG>9yqmȳ4\j]]p[vDI ;B<ĸVnǖvU |Auf=G -ھi~S}@ZI+-/T; {yrm_|_nDW@E2uplsfcv~cwvQíVdҴ_M>2 Y Ӳdﴁwu9f.np2'eط_vw]WIY\"7lb83+)q8s95}#n'pjk!F<wqG[ى6w,^hj8mjF @pfgNk޽e oqy!&s91Ss +'dNZDQa^[mqD IDATQOy{8sy3 ,V@-1Y i9r3p1[ Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y ى6 ÉG E)ģ;' ÈK/Y'4p3 eLg,ZL6BB+9+0Ma;{dJf}zY 0[ҝ12 Y<ǝ;pݾՇؾqK;L?=[l{Jq-LfsRI'u]wʄ3Q:Pl.aIK˜Zd1@Bp O`tn#7g3̕}?Us󞘕}:QUZiHd?;zMQܰgZÆ']B#طog L_|a<Lf#+hfJ\^ U*: _!)l1İVo þri{ɺ{ߛ<Õcws^}Xc4]ON)jSκs6W*.\Vd;Ookoz>^+)cDD-'>8[-"rf)n;?ˏ{w3rO'l[}U]y?YrӱS6wH6]XKY6+jNCko122rKq>)>}ѨbTL"wZܭϦ w$`hM;Og69q<]xMx,8M= kdCf9lVsK{=~*$*`{AI\!zI]^Sַ]i**µmA52&Ttz "bs(S]-,"V""'_]x¦mw3}iZ.#Ϙcĺ8죧4+l7=ěPŨ(Tn/y2PX"΍‚xN]9P#ym6?|a, nd~mq> b}u4d.*]l~^0Ґɍ˞|amjͻUgsZs!ޱwwiW@6CD ׸}7-]W,Zc_J&|śNn^\WTRphmNܸ)m@T/Ε:LψRR3U,Fg+:@(YCb (hm~ZLzlDJR򗥕ފ8sD\^GTҔ9{D$:9&b1j_>^>WH+?3='3!aAҭŨbTƾu}iy5i5uaHTwhQmWҍZӕ"se9Gp-JeRhۿ봽V?Lڮ{-j{_]9SkKrU7izFn%qnh0!uCT`vΞg Q!&MneF`]7DED"?٤ڑXDDY Y2""k3)Ai9猀7REiRR<Ǽ[ɒs#F֞yf}so%kpw[0D"oW#'Kr צ;V]߸.ku~#[fNsZ`Afw[pܖa4z%[ @P ״fERAQ}Z[a<@U w{?^-+iZ{ӃVԽ?uΕX.2ʼHuaيF$Ι]5?aqVO?34?iR'>5jL&Mϊd1@BB,/(a'<'dRAn# vVY5Yqp,b,,,,,,,,,,,,,D\^;6rW>}JDDD)dRIߑ{z:*wlRfլegfn)(cM_o񍵜o>3zvܡG&1N|%&+6T۶1kz) n݌+~>UY|#bn%bS!_tӱ&ئ톌ӲEıLZ!a&]~_ͫjяz#HaҦ9VݓtxV֢]`_~0}I_@XLӡml8@s\=L"\C5M:]Eu ۗ~蓋Dq_%ۓ c[nP;5]6q L ^'5?e—^HDǟ{sL-(GA?k2~yqz雳<Qήxf*:ЦҥGB5 DĽFiEmhu;4R"1[WYx#XBq!5{*JĉQtH>r.~b~fcsl>?aTZQۮK kܳX 6mΨCK^po7_ V(ŭ^;Rͻ҂N}oŏ&$<3 y>ҵ -̂ժDD4MCJ?{tnѿe2+N,C4nuٸSRnO9}io u*rb·wf\s7w~GtlS\aXW=?q̪Arw#''J?wѮVV̶QQ&S@lS{]yZxmNEL^W_`^ҬFu-1nivJD´S#4LV #sxc!*""}|ֺu ֌l';{"~Gw6->t2;`֍?*-"&u*&ZPwDDW:[m|oDF֙Sv/ujΎiO߿.jڑp#nY,ʷa柲YԼB,^ȉw@]&Z`Mw'I=\p\kQ:jRzw1E~KE?fD~-]'qV:jc[m؍GO}"nЩ߽Ù*paVua $'.^8^F9GL4bz-&,&Z}-@El1@d1 d1 d1 d1 d1 d1 d1 d1 d1 d1 d1 d1 d1 WbuFOTJYnF9 NRfYi=RiVWBBSJ)MMSe|b[_xkp% ,b@b@b@b@b@b@b@b@b@b@b@b@b@b@b@b@b@b@b@b@b@b@b@b@b@b@b@Ds>^<7 j>2HfK{;R}ԍ v͏߶Dw;ƒQ^Gʳ*"Fqfzv};kq.~Zv=KMoe?'?~xWmvg#_.M Gƚ7:4WFY`] M~z5np]v-.ϹHC ߙSEx^>>̱cڠJ 9jӱ,6|S=s:^|Bj1rLhgT 7<74a]=s?XZއ6@y+b;(];g/Yo`w%"%Gw mw4雗Qb~@)GMޏre k~ ]#ՁkUpa fqDzI^Qա-,߸=nT#]7WԪ$XsfnzGS]jn;K[KvFҮ^7r.=GY5)S-˃o-~8aᱮW(צ=LW#''?mͦ{rk4{;zyj$x"BkG69|E#aΫ>K(U~oӂZ wI_)f~⶧j;WcXu.-!""*zV oپ撸C;6jm:[k_Iddbc/-"4~j_a$2v{n~U"ШS-}4@h.9 T"*rU-ȼ0]y)Cʯwm=hߗ] J h} S(_E;vګ66fTM(+W7MAFץ҅zu^rRDDͦKTcCf.N.ZVbkr޽ }]Gu8\]xگ;lUWG?j3SI'V}4+W۰gNI[SiѼ|}]KǓzeM;]dҳ.,U4*YS M;iszV5~^(?FQ7/?4"8|veW[Yvo㊓[~X۲-+%qn!׫rݨ6rpZ_ݫ}3ؾo\ϑdTcHjs^~xHݘ \]6Kܕ#Zm-jZ#Uc;?|}ۏڕwՅ,xGGjnE 4/7:/e=GROQ-:1fP&R)~OYSy$ OMw?/۷(SSDbnށU[t=xh*U6Ybqnw4]DDLU{ ՚BS**bϪ_سgM ߻1m ϸ K`Tcsﰦ]G|yRa\fH38'SRz|LJ,l`Fdvpb2.-t-T$@yK>٣@y` }GQ}^%Bz! *AEDizSz $B).Wv?P4@_lffo/!b1!b1!b1Cbƪ0t喙76mCO\K*&no!hv4Q+Gqc'ͮ?ضf^v0sQt2'bPTUTqE[r;DsZn//k&-Z%ݾz۸cJMzpa7)W~|7apIRDt|LB#i;lny;lwgSΑc_tKŊJ5(!^̣(nQyfw8}sƶiDC+_6M:=Z3+_z $~j9e^:YΑn?oq|C;OhȰK]6wڏF8e?Mt0?ci=݄Fkho'M왵 7zvc#k2`HNLUAvy@~cDž)Nm_xe5gVe\W/5%wJqDB""2 )pZ-f="N%a^J'\AԌb/ZXZrRWqBNͷ{ٲu"lîėZ;Xg Uk.:K5wkmZKofmw<9jX,2 W2IiuB_Ϛ!Es'W5r:Cc9ThyҦhEQڢ[V;W,gkeL֞obvu3 4U,f-zޟxRwFGv3եy / jR^NPyN O+wDnslM;ҠAp=YW:w7JHD+K~KmgJb"SZ4t S9hY3[ ݟ.mfG|4 "o1`(e^fNZ؁u;ީ5r14$ St*k r<4D\ˋP7颵4+c$nC3dX~Op0̍OkcB.|q E.y9WefTN+[OUWIl2satlv`0nte ޵iM ~OD C܈KwO&\gtnzԒjbXCWv҄sY[xĉ˷2K8U3ZIbz}Wbf$&. At2N$ 9bF{#bAHt:ZVTҶm{MixT*>2  X @X @X @X @X @X @D&^Rj YMTsS1sIX @X @X @X @X @D¦oB4I[qNU=w\Sֳ 2~t[h mٶ=4>Rln[:HC]+#/Y߬_4:twkPmw]N͵Q7u'?ic1_ug7av3&v2{!^q@xUou3y{*"uX#:4s?{x+G.)I5e,˯t7ək8{Y3?˪;2uMONjܴ[3m^4a,K.!o47xLQf&r*SqDe2S(p14\ޅ6t}oL#14ZK+Ny֍prOˍ\AI9G39e\X"###Prd* "e$1>]{* Zz@n:r+fZX0$t`#bV h/g[2O7{ N.jwo=o &Q02S{G;GfR;ZEDqe{:y}ׯ{j.%">I~pt;1X Mq޽i4v>Y{焹hoYg}bhZ ?8NVU*Մm[ĞA8%zF.; ԧycS&]Xs"bqa-jo g1b3ח k-cQfG9kIZWK'I-01='U/_=³)JcBX ߅XdXC@NOYFÉDeVEOў<+w lgM~%c{jCkW'ljbK'1~>:T냟&~1Wͤ;pnn>m\px7U1^BMa_~q_˛>YLP˓On=z>:%Z``8`҈nzs/qy5z<l7XkDvBB%iKbطԭ*گ۠IIJseI8r51vnN/헯{ y5ﶫun-.gؑ{ĤTZu3}PkK_xh׶7r+I3' Li 9_sDZ^y7~3cdxNS]UYPJuc\Y-F̘J/B"m_欚fV1ۿlgY~۲^]jT$w]zVN+ijT5 qjeGnr}::Ԗ櫋vjff ʍ=vx' ѺY]L"^ymg_ ;}wģ[~\#}.J K9JHD)V}eQQc?3oۏs+7cه~[7pYPIO9B{|k4c*}?R~ |JV'Qxx[/.8j=xo2=ᄉ3_k ;Ϝڿy]ˉ8~s'DD?2{xZs4}wDT0>nX?Xu5oBbֲ,K%Xܮ+%.<|{q_9Uq⥟;C@Dc|^3L+g/9x"%h@uҮz[C_KU-aw77Pd}bw'ʪ=j?t}a--^!7Uwf 2F6G:1ϸy޲G۴kep-&6CۥGA{z~KD*=2J*T:Z8#3^=~t}E&ǵpgdC~GKtѷch#_ƹe'U|ڵ=qaNޒK?ZXAn-:kڢ;s\z&H<䎕VO_z?DsUd`,hW`hdHLC+kW{5o)}:N%2n;[I{tɚj-OUUVXzMx]ԋ8j׳ ^YZ׌AaӒcyBYErÇ"Pn({rNyg.Rj1 ꃻپjk*x]V%ůb*ʇkӕ>PPlNV9kR}Vw) 'ú&yUe"o+'+O8{k W}'eT,=xmjUeYO>4c`(~0o##Hk {媊SnEشwbQ2i>vU!}ʨP3. Bd>ZK{oL{+mV'JƤI3l5vR֮o`WK0-&]ez]}WSEVvm=M""Oߧ ;_}1rnk-Qw޻.AhV}W|yB'64to*et {pqFdd4rѤH4tz;v|Pzab (t:ZVTҶm{PKne&ڴiT*eN[ >a NFS 5eY߫@iB ޟ=ߞ<7—݁#mo|~0#" M;ysPk 4 M5vhd.J|`p{hqx$ow6&dfpd, *..|M7ԙgvq7Zw1glKq-]~@_A ˸jHY'r ""Lq+n,RY lVkDƖ n$ag/=|-[mkQ'.+7&f;K OzJj󌆻1I #-"'mF"DDdhmgF(KK9eQ#PH ұI]Ƃ@ͨ1ٜ98^:#p4W3~<6&f&,<4[ l&.4Jv`ݎ#wEDD|ufvײgd8bLFrc!C<-E"~R00DD:~Y1~5Ɓm S9iY3ۆ4pyyrgrMLXyy#01b(S(VdAE%5aMgY 'j׾R1|M1 ;wٺMϠg~Z0EqK~gtnzԒjbXC}=4DD wgk:F۩~7BNI~dv):uRw; C>3FMѡiJ: sFg:Yp9.Eߨ"kP'UYw}gW[C Z6#o۹iey@ bx 49ֶ`9˕^{9L~kl;pL\_ƬN~m 9kF{^΃& |~05A6\J乫y6-~\7O߽ȵb#o%0l9˙;X(#NvLDЫ]M [T3΂BjE\xЙn- ق%bĞ;%,;qNS*jBڶS_bπ[6#ՠ᭥/7g/_Ubodog'U O+r_^`eo'e^vOQ/q>zvLwǘ1++Q@ڻ[>`em.ʼ  &Q  K ڵozCxe rXI[}G&x\iQG\uÓ$k9ȳϭMEyɅk{goz}nbvMf7͒:HҥfIw9gV͋Q1rw?˜$YL#[G~ooH8wpF]׷Vs"Dk@AVlxZرdeE;|IMDjMV ݘ#"e9ֱ[/?Iѯ>&4};P.mvhqG""^L=:wX᲼GնpYd콂4Ny$iG+ Cbv?lpߎ_uy~O`Qc k%0DD:3f[iu78&,W&YYe|[S3cCCCFw/3GK|EVF _M͌XŤg&ը557a`enn듎VY[v= y|Vnߞ'5QC;"$c8|ݬ~]&-\8`Cˮjx?́1y{? wE:^> CS"x'F 0)3TMbG;bc-횉)ֈ%V𬉙1C9bS39Q!i圶dߢoQ*;qĖ^62UK v>Amd5"J_*_Ѫ؉A Of:]˪ i5)J<ؑq*8'"3>."1KEuup 0qQni ¼ip<'U!&xֶ/?ڑ~.4Y}?*k`eъ}9%.lm,z&wаn#zYvZ;7! O[Eݚ\[^leMn◅j)f: $vslQۄFDlJUJyѳ-_:{cʭ7DDw׃q΃t'cZH?}2hȪ};>.W|Bk/;YW]񋽷sGݾ]7:r{N3o^䤢&/dV@zNB0:mG" ?H!t:ZR&m?%쟫W./ ?<#.`1+d 6mu/Jb@ `G1Z S@K㬃D&Obx _ǟnem:|խ]҆16^X OAsuQtW^fbA,Ȍ*+5ROB;lTOJ2C[ zbLDb=C!WU]JM sqD? _?y;8C@D\vi}~'ϙsmΜ]Z #82_+p\8}a-HXS=3`$vSL }gS?_|w#=ЯLڍeq ]GZn>eqq~m?|֝KL:mu>_٧SfhCt=;5`AfUi%p=~:Ͽ_ՠ /w&c˹iy`͚0b1JUӅ*Xy׎gXSUܝ/.&^{A+U߼"Ļ ėw:;4θصַP=2![_ >U_X}H)j"btmoa'R朎6D%^9gO.\՝5`l܂?eeD8]a0ݺMt1V 6[V1R7B:WC7ύoRbLvoc"T䟹vnU|Yﷴa*cn_\z%%!02'+8mE:N<ۋ)at?lv亄ڐΌ8vXT 6̕Ve335y% R48/XuMm5r7\ϙl$ ::ᄻ.kn:EdbLeFLE)nO+~NrUG(/j0-z-ne/Wf^S7ﱼ3זqu3! {SKuf}LKF4Gz)Nç1wU߹y's6֖?0VZx/=MdHWrVޘ1NVqTlBPzOa~iG'F(lٮlwyIzbbPQ3"S_369ݻm ڍ*Ӂ݂0v^Hq/ms>C`}[.ۗ /F!D},{$}f0OfBejo-DsDD?;8n%a:s?T+Me(yXp`dͿҭhİY#?U&aLr}[w$rqtC^]d$;kO%).\ؗ_3Vmz}`'; 'Zuy}]K9KD&?bIe~7O=./7᧳W(tDgQ*mqdžktv51SZMrZ&Q$0{.Y_1RQ 9)ߥ`<#9ت'5ֹy>Nf?|B=gл3iiV{ij\{+iwaĆn"uݒG?0֞ՉsR&0aSzbN Yg=/Kr[#"xفn"*11>'D~^c˘9j.=8-Υ^i𗗵&K#??huEM\y_c.wӏnrqMyѽ -# )2R$63p 1Ҫڪ pw2r?~rhJH7GyB g)Bam^X5WAĘOQųbI˷V~6CqzÚ7Zq V}{狗ג/Yq0YGĚ{ʒNUr`]dɆOU~<ߨkͿlZ2/ݼ{A) -LVn%s<1 ?W:}G8{۠?ܽyK2;B\`YZjuq10neho㈨*,~bQ} H>4# ^oљ-l,_tJ:4άKq<׈%}zz[V͓#wuWoa=^B"mjOg{y[>/x37c8ӡˮg(y!׼k}kKo.]x5[;WTNƌNI[_±N",,dLG*T꿳X4c޺JZ)[wաnJ#wq0+k1:֭G823HGNm٘ u>Vƞ}mK36&FB<]m=ˠD&~c^)*Dn~m9Eؚ?CRXy8rb_q2=43䷉c9wo-Hהz ҤW"cfO8ĭEO6yoRݗ) h -~w/KDD7>-e tcJ\}s}H߻=~O$3:黍tC%7<7COcfܼ؊ >D*$#Zbz%gB)Z~PK T$|%Rz֯ݐgN_i-THٺ/I%F"21w(/nt:b?Tn(a,CD"S3#V`1iIuŴ& 5Ƚ-?isurڢ8M@\˿Uc^VrՒgzBS*ry*ʲ|m,V0|LMīY* Ɍ~T񜖈پadZwI*Z,y\2g̮ۥ3 b*SQvi9G>mZLDJUTh\ԏ~eʧ媲eV+eUq*ψ"b2i5D|ul驱W]Z8{Ȍg8P'|M'NM~J _]Y1 K#^2=[$ ɧE'NfݻPvWs\R+iJG$8h괓C"=V7yVEPPc* ATyVy}OϚ, qw睩Zaqa>m_i-JK޾e3gĶVyC4dXC,GP KV(-%}s.TeqDD<<1Lk?ݙ=sNVٲAjZbR[{Tw櫞дK״|q\\ O^][®؝]ز\uP(yx\32 Ci1jW]Y3Fr1Hl+*#2}c~к{m :7tZ"PXwjjȺyeiFO1f2oWWiH gHZ ъ~5~iNVb&4e4 ɍM:)vVD8S4tr>㌙>_#7fnHin R+ukVrB#:o\O{³Ø9w]]pbι yW:X322-cl"a %Φ"(2{E*eaRx"oclP-g*t W8Vhl2BX4_^aM>V/li/Ir詊gO_>F9vVAúY|Jf-k܄<"eЫ),=t,<3ǺV@u@@[0jˑ63-S'kʼn/%7&ZZaw7&?P*6OAzsLQ|n6OKkR|EBM.:Ydeމ0hig,:yre7_vlVXVcxT]"攪*L-ҳhgeʐı"lf]kX.I{bxTSQtPigm_ԄngR'UZEizu(*Zy7յw UR'{ĄʡmzyzzO8[߈+<]K$M v~wH8Nz\y XZy5#?3&&UQsKE>|;KHqy79U,s#Z[gz >|Yf\9?I4ERg$uwTdFo̵_Ի3_rts"ҥ&\ٝ!FՌݖX tZN,tvfKb꣮g:Y_܍;{O9̿'r=e\k6=Dn-!;C[V[yoE6zn${;EmNHJ/yjX#NGOo|Cg0~±d</:)]ߞ[u=5sM6:yμzF4m: C_r_Եń>|٢-Y"x>x8nKTU ?tFg&Id%FB]BNQ׹˧N]TQPD^|[uGaV,Vú|6؀a |ߋ q q; K;o=58xF+Wi򮯈̬h1ٽg_Ӎ/O,q-Dy1FJTmF̪4rhǝ8@KwBg m%I?/'ѝħ߽k$S݌>u}nܩGRq9ko5È5mJdfB L1z%Ӎ)oq-aLRZFXTn(.:tԮg=2^;{oqorփٻCOPŗݻ("ԣ5}jEB!|b>kP$`:܄8NөjJ5!mV/g\Є:JsB2.A7$Oa.;ov ے ]ug.TYokձc ;֦NRX,,LxzT^G'3&" !УRZK=Ov!sX ꛱77/-ѝmig(_yzk7_X y -Aoâ",(zٝxW4iհo^$䫰jI{S q9?=X> ^y[ MהYi IDATU s?q6,_M4elo. yPsEXԕ/I񳳑Qu#}XTغg|LXu2?v5_z4kF o2`m^^RuD$iFذcV|\0j_3(xn|gV#>r۝7j2 ׊w9ˌE5DBcâ8F"HPl{{lh,$FMbS?5%j$ A{m3lTDO'+;s];3p؎}9qI\?,nzд6WZ2xB zyhMe翳8aƀa}uN(<駁{.{ r N]W{wc'OH2>˾}gU*۹29- zkW*nw B0w |2'j(TrAԳ2Un+.(6oMm/)H𰵉*O!B/ŝSWٰ5q K8t,]_X[z_1s卙BPԃS ;zs<jVGA.!BqbB!0#B!@yc"bbάjB!:'[i}eZw{cspN^!B`,(Cʒ!BO'Qtvc(8 A!0wnVWG/B!Z(:1C;fYSu{v-bB'ŝ:m }q<-@rB!:1ŝ2FA7Y kcJB'Y#JM3]B!Pg#J(%B!('FUk <(Ԛx;B'WcGkӵꎮ!BsXBPווut!BN@!Bc1B!B_n}Q1'Υ,zao!z1`,~IP9ÂMw̝OPSߙ+^[nm3/Y~ϔC! c1B!Bx% @iK]+ּ>˜'͎MjEК^Q?AMXykIM3'c~}aZvU/'0:4pvmw.LJ6T$~_Xgwz.4gwqD`Oo#]JU\zkGpعɎ B!^_>W_np9,åoxT[j"^))G=Ru¬%__gnK>ނ7,0]~kYܗX'sHYy%qM(n;Fphhޔ?fjT9y~dl89.c#%\UiJon;ro3oG)*_ rA;uBu_ʼypAc, GYOnWsD5~9}BP+}t%h!Wm NVRP^V&|.יe& ۸hA4YqF(+K(z)eMUhF8Ț W{55q;B!c1I}pь]!sP (.o7O<.Jucä?Q)!zi`,~qU'?|3uڮw__xa UMu-:akHec%ҋ篭X3EޣS1P.N&JU*RHh(44zPKnM.7ﳵEBqv P|:͑CQ@5B!Pr|8GgK|u)URPH10xpﮎ.c+t>^a|.?߱s[G<\6oq"*Npf{64}B6Jh 6GGZWlC!^ZeAU&ߚ)jq'3[T5" "yD*~jr/m˪5s_l#.j.L RwdXzՁln<:#'uC̳ѠؼݡeìY[<5*]+ n%3B! 8cYVT9z.:@]B{ A6{הּst]o܆m3S BϵsO줝{fD"@0 M7@scn6T)І^mj!zya,FqKor庎mqe*B!Ա0Ǣ4!B)w(౮h5q%;JS)=ގ5>#kj0co[5=Z O tWBOc1BCYtb{֝/,^o֩c'wfĊז7/}hoIՆP¹e!OMK˜'͎M8Ğ6ڜ,#6sKGyT5i7S kgq4*vot䦌qWNZMapq_6 5e-iMgPbٕmM =Y+)N~JH {zXeUtwѶ}p zByeKK )ȈܚiEXWhhȃbאzWP$K| 5PJItA%u./o@͜2{9@np޴4rO׭/\{*!M |ƹQl{>(%-Bа_(o="VXIDݪj)3lV 0L81?|Oj~sv3le?#o6v|A칣(-߾CVxX9i]S[39ׇ0uM(nԭ85o7\̥&[U %Y{GYy9wfM\G!GZgl.c-IUʃiZg\M]u~CI7k}UO} ` ]'x J# \baNs/# 0:4pvm;0;d\O3pN i(SIn] \C{B=M/'g(^橾\J>NJf0QfF䕐e׵b"xzR[;հL t9vU`'Pc~z\>}][M} ˨A֫ikk]F혓rvcjNؚYo"@ t|V7)j5b|N݅5u,`.N4l)kotqւ}GJ|^o1C_};&+eցc,N$`G lhJך_*o49gt6|LVSo|\(5y,=\>邴˨Lys7,o&=+0ԩXc_EܝMAp;7~+9$T%wA R62$߬>BAIS#Gfä^O8/0fSK3OCR9P'o ҍLhGy`ҰQ 5t9ΣN3]l;xYDLgo>x1fZԴ,mYv`+7+TJ'ɩϚFI8կ@ʪҷtIR:n}R.q݌ |hJeK]ŝ7+Y@]WpW ,9DۧM h9CtqiB'Ƕm)vp)4P~- ♣FαbrۀF mdg[Y)P}(QPť7nޔWw3oQI^O#2Zdo@U R6= C("^xguƸ&LEJw UtոVIU>|<\՜i϶^U!z6Um?w#ʥL\3LfM}MIfY2AS| ͽARz:LM͚ԔaMu56H%RKv+W}>ꉪh do ǯ4qnM5 <R<7U¢\C#k n_acQ7;WFyfS܍7>|ȃ]#yKM9 -V"uY'7~3.(K(z!]CcA(gҫ#Y%WBr@qь]!sQ (u¾ot]=n}^#u'X"Nvy׷[qbu12oMm/)H@\YA K90%j뻷jN &F"J[$I}D յhe@k [SS@[3Ҡ)a kҠGK4Ӿ S ǩh]DJg(ODzX&h< P42ɾ~ex hdp?wIo!ʓN|yK?*RƘM4u(R.FRF5;H;tZCKLQbgn'_ 2JK p5yX,~miA1=w,y{cF"ZHK<2׫HC}GY w͓m@j e)P2Upj(@djk6>G[u) :su{f jh / j ٮhz(S0j>.Cxv}Q"#Mb,WZGXS@AKu?5յȆ!o K8`^򝁽#!RYEяfرׄ>}#ч g34/61Imla&,bnaK]¶Z&0qrД*eLRL_J9+9JP MjB Z!Գ[!{X&&)9r}$JeMD6ߞR^G(#]]T]]A>AVC<ՍGu*q:j*8)x %}QN[A(OBW֢ R u 7H]R <%Gd QP :V:4Qɪ1o3yDٿ e#W9=*>|cA^=h+u# ã*ޭ)u(Luu;(JC(x^\Ns:i57+eOrBkKr@5}EU ,aK~?TVbXCR: cG3붟5ZlXKTLvB~ll={ܜȔٹ2.+Mf, E Yѵ7[gleI9 ( u$WӊxCK%2CSf9|-J.(/a< %jtLDB3](3cVRuMU1ӻ|PoA;QPr]fB]3S$xd+C*}{L U oG|4\#Sٓn DVW[HHZfR[.qbJSǸac U63Q < i` ϯTU+x<5Bxeu1>je9Y7xIpz?dVͨ7XpnLD_ZeA&u5J0sWa\(OKmc} IDAT!lmZIe -RQY_fK4DR_ZѦjYwM8nkʪZc ^/7~F._J%PTo*\Vv92VNu.#9RVbT!Ծ0jq'y筜@˳XAVY=@H@I*]-:G [q`.N\Ȝ[%,X2iv x-J@m⶞!R&zLo( 1,@IױwZ{*YT<{ZoPxO2J[2n5 0lkl^}婫3$?:)Xm(*;$Q^mrYe̻WP̢еM+H,#B+9W~4G]TװbEߊK~3ihWo1ׇuEIr]oe4@QXp훫jZQ/q݀I O$`}_|C>ңD\on[lvC}x;'dwyȥhMnDKv^CbS dت˱,?v{.NĔ>,Lؙe]t- gJՕlLwk]{-wUi@/m3 \꿸(9oIb@M ;T-A(Ys]c\/\)WPE]bԣoAfϡf{bԩ_wAn^r $}uL7gDM^|]ww:*.z:tzBmsMa}IanQqhLQ20O|Lv/;SR{..q0q~N畫M[>vClGC{`vES#@L9خ/r !ԶE>񺳓v%0Sq=#<_UhUro@]|ww+ԳS˞L @{MZYP8yXDa5hPc9Vg)evBy[B=#3IJ%c&/-TWe]m.sa|S3I]a?gpE{®cZǹzdNZ91+y_{zD&v'Z<ZVuy{KB9EL0ŭPHsE=Bw$(P{4D'O E5VqJZ-9סHjpd!PIHC}uvsLx%(*>l#Bs'Ƹ.=qĮؓ!BOT'fk?]6ynޭ !B3Se]O((-Gߔ !BcqDz #BGXّJmۣNjB!ΎHCﰳ;B /i<[#-&;B cqgG[[p\Hś"B=!D1 MRu Buf8ZQֶ4ɑ+ȣF!7tt26"P˖f%Jj*ۣΥ٣a,%> HbwWc,F!ڗoQmXi1516 ?ݘ۶a,جޝy()*;B晕fGg ~rx0wfʪ.!zY~ҁOC!z,4{ٗ|Q!Ba,~Q~9^OmB!гC>6 ]B!O{queyex5B38n{<$;A c1B!P[8~C`y:6On>nv1sgw ewDpmXB!.ffjr5bێlUc ±*eC]myQ~r|ռ: G!B5fm;6F~=ffFK_fff4St"C4!Bm,}}Z{4Kjridӵ^."ۀ၅e( y@Ow[C]().\(W-`5Uw\3QVt# A,(IیZ/m~xt2vԐd6<]c;K^j 5nHg Xg7 ݻc1B!PO}xP~2lynfM/s17i";\R>cѴor<2'_oabm*bD6PF8%,'_yruTҨߔLx6Ne+8#D%`\_CĚ_@ZZ"O:jp{VBI" :],.[B!ƃ9kBbjn;ֳ0P̛R@jҲ m0tVuAn۞onn)ְ%ݽMpEw%)y  #`  )?LLNr+L ކ:) R-~z1EЈ uRPu=W.h#0?(ߞOH_\ B98Z|1,bh tr8nHc,ncĶ>ʪG]׬!4s2eӤ^J} 4ZRG4n0rJ~gkp?[>Px²M6>W#GҦ9)Bvvom63+( 5lÔ u7r)*QccE5)`b)PC*+S>=8bcRqhQJhEKJA!Rb#ilEehNXEgbkɿscs-Ϊ"B!Z-n+5wMWΥ+~R:B 츈ɓ3lAq+Ur@ԥVU$_6[[5r C-oְTqN7waJI\ͨ ]E6'̳*j0vSU&!Bmޑƹ iNl(.H]2[ol`/w[(jJS] Oh_䖪 H J$і(!lICکUmki ʳs/GG*@&Fn=c. o@'EZڛ݈ x#ӆ(B&uXU*r|vξ ;+w@GB7jܖ t|4|画ZZ?Kh\d<;i^Y"H 0 Ch3k\;{ꞷ/ B! m} Ԏ0?kv™{ISvtA!jK%aGIiCEWgH"W,t`,F!^(~=%E?zFqh!Be!5Uk.!BcW/O]k Bu8h]~ҁcC >ɉ߱juBu2l;߹tAXf֖B3VIw!Pgcf%_˽GX CJī#B22tiI^WeKK"OKj*ۣڞZ7ꎮ!BOHRS}c3 iuyPu!Bτ:iڲzL!Bi(**B!PK;B!0#B!=Ž.!BԮ) cqݫK@!zkXfΟ!BmK Μ?X\o@? !BOO fN.@[prq; B!p!B!!Ba,F!B0#B!B!!BbB!c1B!B!B!XB!`,F!B0G#y69{Wõm!B^D|vb:b~ctte!By8Z~h.=)+Ïn)ttE2B!y%G>]#]_XƁ>Di_G %Dm3My[%2N-6x oAڝ;vp6DiuĒEieaC kT.݆Κ>G$_s;iTZ&]lmM^'I݅ ,c?tQۢΆ1xe<-]_Ŵ>U)UU~,RA脰³{SmC.ߕxnLF\:ݑ!8A72'Y3lh }#s';aB!C{ve'J3R+= E\Ue5Dۢ^I25HҡEk[ Y hPcl$@.J5j96F4Hꪪe1|H gjaf>9(YυxTԨ vN<kW{JG[@@-.}:8S儨$Ɯ[s!qf-Q%,hPN',2c&[P|nUͺ !B/'qAo 8`oXPwvb7QbZ`"f-$('ɗ/HC~amyr(=>Uqqt (]I4E;GXd?-k #>v< nE=Sӆ^2ۤt]ݢϿY-zg0G(=== 55JԒP,֠5:NiΊ +|]˞ç,գ@ ' fLƽzE/j8v=J`Yӧ iC+[^Jz5k6b1_dh̹.3x+S:ͭw}u^ҎcvNlc]on܍n#{Ih|FʆOD{ M8u45tcL!NĵK#iܥiB!(B^v8eJ\.o pMho;BNmvνvD"@ `; OG!Bc1B!B8F[]B!KGB!XB!bB!c1B!Bܵi'>&VPtt-!B@(k9N.m/f7c F!jJ"rx|L7ld(ޟLB!Զ şi1؎.!BԮ) cqábBЮ) c1B!BB!XB!`,F!B0#B!B!!Bbzkb>$h }8}7B31Q̱y^e.>JOÖ";>ݸD&yޅtiQ_01g¢"N|.⟘bz@!Ћ"᧲+Ƙwt-gSUhm}O‹:+E{҄ksbfNz*\:hB=kNV3o)?[6SC~׭R(ɼMOnx}{bu'~zňĬe؆OQnk\ *-t͇S@<Ǔ_RYcU#'sMEizFnpy|V,>:8(JC;Ɲ+ણl)FB!\I JoշL򠒏=p.Ͽ[sEVnY?L4Ŗg8ESxl֩?+؂7~g@w lYݴ= W[g Ϟ7ԐJ.n۸q{h)UEO>-fhm^_;ϔھaMi7Ӝ?6[8_xs7,o&=+0ԩYд~.K'.kMG~[?XpBߎ5ح5fmb^[oB偣 QQCM[jG 4Ms@aթMmPL}0_7 {?kf"&LxcVԋ'O4f<>#}ervz0M)@s|9f4IM =Ql5aS'Mc7|\/h~ 6?NC_$iz?xR q> mbϳ󷡪l]eOt1\Es6W[0ԓ/ܰ3?h+GE}LUq̕С__˧} 'n IDATR B/JP,eNF^S8xFb+/RiA6] j5t󣚴!xЅ~j56իjO?*"cGwp-.I hJkG}Ro$@V_jh=]++(@A~)Ə8RDR]R~V'_Ȓ5"-#>V\)M *g+YT|e`W,!uٜ3;=h -Baˇ6 \J׬_fڑLwA xeMʽ^xfI:'ےk`?w=%2 {~Ы-+B`eGSl).Ʊ&iSӻ|%ycuW^" !_|jԢULhQs+a\yqeFCU8?) V}szjS\-\:+WfuI6n08-P7J644.'MEb&Ms?Yc;Z'ܰ BAO-:ݪqb1B!8cYVT9z.:@]simš]3EC!# }ug'k7K$ aszN@tBJۡxBfbBu(ŨpR F UY~v[KB!Kc1\w*B!S%uwn!Bmc1z Ѷ펊 |'E?Jǘ[#)".1nr"N{^xDsB6sKg c omI1za߾ʗ|!1燦ۻ(4"MDX]T4*%VXwKlRTDEPze!1&;3/?;L|Nd76r ]VO_3L I^n[݉F{ӕId>NN1\nL @pNpSb8mw 39~=dW$/6~:$Y=)*Ò6Y矜|w1 HW]JR8;wt,Lա? OL>zZ!i`ddФ7_`*OZ!70=e؄[{j{\mRgsIOzf/^,t뵐8n|حC)@,-)9v[ wѓVV_M qyZ cjiKV&D1,{\;c|3u|SQխ'o O+X.RIPew UM~D8R}]Ӵ@|3[D 6YP-e£v2שwVT TAЩSXeFugvXU1?2z+o>e 7[BBZ`6ߐ=;ZK,ezVAd6ry?խe~A~rpS z5 INCǭKIŵ K( $5Ji'66#4*cy;zJt4c'HPAso L&HTWB|iu!B=;BF#7/QJ*y[Ep1]_[GH(Ov.lA))Z͇NuP"ۛ H TvuX8tY^fex$ (}OߗUѤbgArMi8$#͝vMۼqU/%)ql%#n0U^VN]S&deh5ZWk]mk,%R9$]YPPG4R$G3 5uuP( [Tk?cLU{->ݧ׺I O׾ ػnOXׅBa, >h2_*)RES M(} mC-ن)tMgOΫi'U,z1wG4,ʎx`ص%*#6|BIMME~USSAWTcnKhҒOPRg@AV $ =ikH^7s?^MlUCJB]C WPmB2zN/'4@TIݍmostOw_XB!3v)JFO\xLf3ɵ>DoDeLB&G̶xV](,09,bpۙ D*Ω m@WUTnV^,ʊ*g3:KxRB:"!}|ǖ{4{7!8Y3uoR)SVxIպ !;o);0Fŗ=E.XOPf{;:|!Bcijϕd ׳pJHU1MF(Q{7BN";9Zڙ2wyoۜBσ}%LÙ;ҮLЗ+z< !c6osX{fKB!p_DB!M}IB!B;R}oB!"N,_%']:md{Rx~}so'^xw.#Iחl0M7$xIwJߗ3j]iTNٺGYc~ܳ Fe~EqBن'/ȳۛė,I IehXlWIGHk`SOZ*lg;jl%-H6OQO 8c⇓?l;ϵm7>ٻDB<UjCGL2VsE7Wϼ".kvfNO5`ZZH5*?W{0kcv\|s"|An#zpȤu2EN a&1:\r*SP(2"wFcKн+CT`<0,:?͛"a&u%zw8RGKEUV:teISs] r5U.>XEv-ZX!7(:mKu~Hf>>_l+H9`[<2,^:L,-<081 !خDΝ'o\wL8zl{5 Ri1dޭ[߹3R=δ9 3/x߽RHenCv>$2A];g(d7pLu>TEU\8|"P43~M.8s7ݿw/֯:|~ MHJIgh5~R-QTN{|_7U銧~w#U)O6@ ;>!τ=mX7ϊM}wq˩jflo?tmVwF63ih(5"'*ne}m\LvDPUՠ<D9YOk_) #I!#'CRe t~^1]HEYh+I/FpaqI򂋑 9J t v_I 8J "]_($zq8F Hybo\ |ޖxL=sSyFI{'UeeД%DtMaAgD4BpYJGWWL @ N.w ny#cѻWⶌlSgEKKN 9h)֤qC I>=fgq쯮mՙgq،[K Zq2[2^`f݉5QWF;zLk9IYy9.3j8ޥE$UNx-9Ut=O8f!c^+&PAysgŌMrљZpŸ9e_aT&mfag}PT`0Y$A""!=x Su/\J( `%ۘ^^*N!@j3 {Ev6J;Eo J$P13a٩vQA9q54(seH9`&{_N,}Fb@fSiV؉c"|mTgcgN`g Xj ceui>Pf9΀ h?śwNn;-~氹";) 0e&8}v!̊z,骼}]Km[~"w! (J$F߮][5vа݄΁1IDATe)wmgpM\B$4߾!Ul6`Ќ+Qmj)[fbBo'QHw+B!b/yK!B}>D!Bc1`?0#BXB!-F ޭ@!jwLN߉&q>ҢПx'&{[=TM>VoMF0d*$!7Plc d ;yp12!7(=aw$71.WKҪvDp2Iy6g`σ^+0\xVL v rcm( 6v(.7uŧ8G$^ںr}cw{i{Vk!G0wRV7ՄRT!)$:s]7%_>~1aYZz6F=V4>1 I?IHmSl8h)R%l°ڰ3_I|8eR i%@jE" -x%cRvZ)>Wb K6Zv` ~ʭU=&o=B (v! /ć)n~C MR\B'Qt 22*|P=FN|,`檘b=L7@WG!#*?ngfno,/LOmG( S_ 1B}uG% @֝9_k ųL/b&<@eftPVſoLt2R^ۿWwI47f6wy֓VתLݓӅ|ܼ;1}7yʪ$w!0wqkeraE'p8ə6,V [ħ ԪNK*.J,m+jmi|b'*Ņ}z &+aW&`L4FJ<C:EQ"766:vز}& Bxo_v\III6`0Hkm!Ba,F!Bc1B!B!B!XB!`,F!B0#B!B![B{7!Bo0}-̿wB!) c7g<0F!svc,tz9>hcB-!1hz]-oW^|V Bυ!Ba,F!Bc1B!B!B!XB!`,F!BBA$QIENDB`litestar-2.16.0/docs/tutorials/todo-app/images/swagger-dict-vs-dataclass.png000066400000000000000000001673771500564371300270770ustar00rootroot00000000000000PNG  IHDR 3iCCPICC profile(}=H@_hJ;8dNDE EjVL.&$Qp-8XupqU?@\]]%1ݽ;߬1 ftJV+C !D$f곢wI}ѫM:Ԧs'tAG.q.;q## .U x8.+8:kߓ0ZԖNsi,`"Ȩ,$iH1U#<6Br[41&ES@Ŷ?.jmN3puM`FGKm⺣{0KHR x?o*@Ϫ[{@H<3~^rqDbKGD pHYs  tIME 2 IDATxwx7Bh W)"ҥIo*E{VD,EA䵀 M@@B =$ݙ"Mr+a3{3m6"""""""""^y^RLDDD 0+EKDDDX'mď eƉsEKDDDXÓeYXuw*)lA4y@u9(s2ןQ xpx!.9t7>MxI,K~^Bj`y,KK0C[0ս]Gaje.e.ːŭ{xޱCwymvw698l2>{XCcOk]`~ }UQ*""RT̥̥%""~3?ӭ4w.|M(S0n՗|?Oՙi9te99 xpu$r_Vz\( c臥-B{v2mlN? uc^/e^R%zҐR{ Ŏ9y[INp͝i_gi^սQc ̣n5)×2-PݝIŎݨ_G@YZȑ$Zډ(L5L5mٜf̎$xYf( DDBqֹ乫4sV0t VYab8}q`+ۂ@GP$UBp,vge.c?'bFVLވ-O,M1Jj\@Ov6cήRzRPc仟ѯU8P!5t)JX+uҝo|SS9p8uc '=C`L6%|/ÉDȉ;|\v߄K}F0QRR[+w^C۾XoM.vœqք! aI2¼dr2Ȱ}ֱ3OpJ)u汯nڕtYy)YLA~9'CƎ,\AZp,/ioGaAzcsߗNm3479'ADD6L>\\""".G*$$>Y< nic7 00wUst)h7纳6}"x+;uY\;1.'}xrՐYП#Ng5 -4yY8]`q|Kb " Y9rhS`H9-xޅtd<\˓(wx◅ړOFȂ‚)wU]b;Pc S^!Fҭܤui a*z t>0|(%/UCWFKKDD۶;o|7?? mu,<dgg30 &Թ/f%lOܹ68|q ,j{^mY`yrI ̐`\8gj3!k<ӱX_DD2xWi|0ŭwӹ{׳k0p`̥%""RT+syy,Gh*7LL?9EǷ״KЦkqX gx5DDDDDDDDī%"""""""r^<~?\4 nȕDDD MG;e3r@j(/gcӓwW@(ЮMtuI\fHd<ѢxgPDDSc YF` 7;o%:kYҸb-oJh00%"00%Wa8 a呕Bn#7pg!o{ ?v枡')YMW{_3{S>Q{[;Xc7y>+sGDt377)3WC=4MUg2;_9Aci&]{wh?tzvvfGדx>2QzyDiutՃ G, L&.}zvG9iԼ\Io]4]" "qDv>n>_q;{<σSc^y`gb3)|{<;SrpJԍkʸ8ݹd$.ekT7z6f}/Ch˚5l<~'RZ4;8bQĖGY} ?cQ4&q~e6xQCjt]0=NJ9:w Lg8U4#Xd UL.81ȷa:O zz(AxU[b*~JsҘn.ځ2-S#TdXF+ KMFF?32EܬI]G>u'K4oDDDah_JͰanrs=X6Βj] K'?ciie撓=O8%yuZn?/#v0tRҳ\[6~[Lr.O^|zOJ-y_#n4/q˻qd]Eoae tjddr#<~NIf}!`F!EM%8c{}c6n nEψ?mp;O1jv^6[&>EmхgJIۓCED!Ìz-,dl7Rar&zQ6m 7z?;džNps `shTM6KzM0 x` %򶓎yO””(L0UWACn0q\ڴ/}`;9%䓊F)=pҠfH}ztt&%aʼn<=) jAE?;*&[.iAY;,aF{,f-]~8N$!*IfӖ ,lo7=o'I8 \uҳJ~!pQvuJ8v7stLX(b֠VF뗈0KѪ{cJ97.j/X9n[h[r}qD fH-Z5|+^͵姰1Cr|"G3:,))))`?CeԳ[7j>,_[ﵑ.ċO a@p('٫$@Pն٣%]{9+~O˲NA@_zt4| rǓ⟿j;вϩ/$64vRwQԩpҊ_VF fUOîY5gǑ2M3""q XȩŌjI׆Sٰx/s~$#杯%qp&z6ZF.ab9deۜ4*1Vy+Шq0Xʯ#T!0 ^Bs//I}. =DD󏉡ygt?-&|׿1T 7Y͊/G$U*V- 3|9~VIJ:@vh<_8e|129:w!Dz+ω='PZ8lOAg ^>zȒ:je*%x=wlt bHVʴf:M˴%XBbd4>f+7d*4@U!f ),rxV"WL^}7f0wu/%6UTt{~Ҧ^w&(9f%bҢwnJ$"\ұ7&ܽySƈѿpjps;f~<|(Ӥ7O4}wS Fa1BSx)wuh}_߮`{J&Fɺ:VǣhCĥl;G}x螫ȚO^A?b#CPu` qc`[7ø/!Khs a͞|8^ʝdh)4DDDT4U.enUXɳ7i9gJLzg/j1}V,O>jl./=)imQI/I1lv,_E{Oӳkg$xH|rr0#$T}X:f~p|TG屒f1ieW)K7S_D` |+!OS:O -_ɫK'y(ڗ|:m'wշp|(Î񿛮x~;;ߧgall)>Z^6{v鐯BWRR)tw]f2}jYloǐ{Q54Ƴh& T 6 7ڵ`j۱UKRty@,^v.뎺(Mofu,AT~[cX',S>_Oeа3gGR z߆:1epMW72ٸz8vfCkr*QMzpWZɡ>?L*vK*8pop`:SR $fC:3"""22H%3JfP~ 8叹L<_TtTױ}J9PIsBy{P>~YȾl0)]9R*~&˜8w||gaa2r=X6Kj$b)wRw{@eRDRq;9ؕDу6N+ekr؆3MyLW%|*.juhM2?s_:4I2ꞕ"""22HsUH&<|^Ճ9`Q~._^!;2: pprlׇ/'!,A>X'2d.N40kY؎|<77?ipO)N2V%s8̹0bڢx$/ѐҔ 4ňP̥%""Ŝ޿~ۃfYI!+-C_7^+܀Toq; nHȹ_94dshZzml{{ D`h(uUIt3Rл C͏bo(ʄPJTiP (֘>}W1jftΜޜ;Q|7 ۟RUs37QOH̥%"" ۶ϫeYx<rssf`LsGz穃ô ԇ/"""g1L0???|||p8Ȯ%"""\\RDDDDDDDD X""""""""՜E(sHAX""""""""Tx5DDDDDDDDī%""""""""^M,j*`WSKDDDDDDDD X""""""""Tx5DDDDDDDDī%""""""""^M,j*`WSKDDDDDDDD X""""""""Tx5Ll۶ZPDDD2S Xe P Hg[:oe.).!m,K-+"""Ee+DDDD+oX""""E=s] S"""RdTw݅PDDDDDDDD X""""""""Tx5DDDDDDDDī9""rmۧa#""%R2 X""mXŦL4eW*LI SW7O߾V i%""\"(sB,6#>eA@Zy)6߈eYjQ)DK=DD8۶qL[lۦ]V7\.G 4Goy.<>W:qOe~.'Y' }48bÎxIPV};ODθ^9r;Ð)lי?WGⅼhӓw tR>tr%W"Li1bn(a*ڄ[!/c\:凩]V0ޛa8)w凶>_gl1 S U76#BOF#uj"|%fN||0N3x3>bKg`9-n=) V?ɹ+*a?;Ob~spW"ɽ|1q:L# ~60˸f͒dF4ڊ>Ԩ?p؍ɿk?)))QR+.7X<~ԭlI}$OEq(Ghj5ZJeьn5rԴɑ+N\ssrpTؑ6mRHwג纊aS`c7y>+sGDt377) ̵lݗ'RȃVd2Yo7]hʷoLfD|а Pc+-Oٙ#Uߠn\Su6i֍USrO/3p 2}a˝%bEw<>LXX?M1@7l##u!`>1nX\ea{`,qw<# FFv[a(ڀ ֝u+`l]4RCzs6[SHs,`JWM}pz][Z4~\Loitաy;B7W̄ Y<A%Ҡ<~}Y5w?5;rH&9a^޹ !fCwlҷ[ /承\Bh/vr0 D95Ġ\َG:B#(jny6:rYGMPa͇v aMɿval_‰O$܀Y.kx,Zu|ZvdQQz5`D Uھ};wg,'"{3ㆾ[r1CQQ8Y'ݜ{@ۿy>D~dME8e}(] ]{Yf1㟍gӯd0ϖ JFvd7ql=ՈXy T'Stvٛ I, R\0_4ά~%&mg0eٵYXzO|"?6-"< y{ZR6}t4vƤL;(}˝0[_>ðTENaXkVI0~΁Ccv_-ƮW?ϙK#`cTB:/e"n*UA5ԤM[/&c<*8Nه8k֢fVɢX?~޹Q\ `k6aG[zS3 ňP 5ϸKӏOdm8#.koV>aċ({QĄh`/ow#.oJX\gW|sks}(]YN8Dp7pD-KTf̝Uθ E=sy[))))BM/8p)0aC駟u%r{ΜtdaT[i[,2$_MYzq`nӘ#5œeûmcegs{3S/eP0F?P ##3wL5;yٶF}+Q&mO*Wu>O6D^Ǔ F|Zgʳ)Æ1juɚ zTM6KzM0 x` %r`6އ޺X3OcʓfP"9i7f/ TعpMgws{a5SuUc^ǜ{Rmp3V\/<b8}QJ`YZg F'򏙔2I7>#fgcļ%{S)k`İk%<ƫyگq.yg 8gv-4 3!/6Ũ/dfz,"dՂդuhC& eZ7N9}Y,3l̐<:!Gq81%%so%K?;&?K#]?vl^AuiRX}$ilrRp48g^%?3\HUDT*,))))>뮻N &p]w1``rCAp ԩ^qö͉`ggĀ>8eln݃YM $MUbIJ38IC+eOiO؝HBS±q3dJGSML2*hBY@$- I!7V|6!h x5ז񉤥hM PV+ʵ䝔ujS`}fnNelu8+7meVR+}Kwʗ*Ԕt<|w[]]Ň}7ݔc5˝.&.1磏zE&FX zkMb&>aKws/~٦i4A$}~4TCᱷVd=̦?>Uϗ{"+_.[`DDaJa3*Czy?domyY 1U*c&'CV7r{ccYGIZJ%p+O&Y$={PE 4 ^Bs//I:ex3"UJ^89gf,VgՃ-z艨ABpSMUrCB Ubbma̜(jx)3sVf=W|ܑC̣Y|o/SWi~aܝ"LNMOWeXBq[x^%9xn-Z.E5j˵wdKWhXJ-;%”-/S/V,б4|RFdvw<8H$G;PF9=6O]=lUmJE%gBx!\.\%U:Ԧ8̛ TPSr'kobi Y2v;|<:oQĸkG>. sVi7Wk[R%\GWAY%#]QqUG$Y'4O?\ڽ3VGn"LS]:r)Imz?IIZ0ezmMmnw& #33Q@bnuQe4mJ1OyD%۞CSeJJXV1jہ%2ܻZ#b!Ծ}G^ ջ'B_(YXs8GNZ2yT<"HL(ON\)_paZ/' Љ YʶkJ>;(M̗kCPeK2R]Қ9vRSϯUGVM`X^[hĦ+]5=y8F' +Lm=ֺɟ닃yd6.~kz NPb{XMuv7 ԹWM-pż IDAT]RJWzT EF{RIKН ޳Lz߅izTXmyL-|9Ts;UX6'Շ臼)Pեp7Z|*QK;~yj S| tkd8Ux9.{\7픹npSS-LQ\ٓ.œc>K߯o^U?iڽf_V͛=LIuBMݠi UOۢG+[)! {F͈Rxeia [POkTt}bٹ]+($,"P/CfjZ%CQ{h0Tu5]WȊ{ߦ/4y٣Wuy\3{&/9*"ֶ~XVx(Ny_TAu{1$9P1VmP얭rVm'{_yo>j۪~S RfJ I~nJlVly%հSW:N3 U5XwOHaUm]Ou)*}'6OKhOfaHSPx1U+&=} !FggƝp8}ԹAU⁽:N%] o7ĉxsbbjpOk~\a 6h>jSI!Ð%U^:v(84O;)ɼqyo*td.0Ha a0u)pVLRb;Z3$Rm SAxL3zN_e;Q8{[u:=Sd.2u_*ڷ7-ITV˔gk=:6U}J{晋e S'vU nTV~'WV+L2*twEn#4ϵRٖ5+NEolxb%lҕ>K La =9͘i<:MazjL-2a ))`c|F[H Ðad2pK<@nE(j*˲,=@[2H=VeZ2M" @n 0%ө֭iڍ1o,brpKǖe0 nLN @nX|}}Yjժ0)%UVEÇ VdR(st:r˪X"JO_^-ea4﯀@f pp( @n[^-;NGN2p;e. , ÐieYl -g 2$;azl F[]E¸C??irݲ,9_2 9N+\?XaH)??z}S SiJn*r:2CMuUifl 8 !l F[Q`(`kX5 ,l F[Q`(`kX5 ,l F[Q`(`kX5 ,\3 d.n8 ,\;V6ohaJҜ}z{~6ZΣzqZME{zMzoB?og[h tVuyI#q^U5d5 2hiZ1V /_K}PM];(ڔQ[k6Rnč35?> ԳmFMx7ݚ(zު/kvG7hʨ5ws}UCo ZV!F{@Լ) VI=^xLKJVOswpR|U}խF.K c E lVԄwܰ喜EՓ}+(05JߍœԽ5 s(.֣bU^ԼoOnOQ8Rb<%/F KNXRV.Ԧjլ|{c lТӧS7+!:nURv-^~`zI|˨ːծzc)9?cR/DJVmZ"I5Z喜%uO2O]ZˀP!˵W[_2Iq=u N%yup-N}Ti\@J gpvjZs܆CZ8$Z'3=JOT"_ C2 ,*LKJ'ӫ+G:gt%%$ͼ u+Ymj }=++64 ~>z,Z+-Tzdi/},]n:;Oi.]ֶ;˛- YJ>(%հ>=tL^].޾SUI.t>W%m) ʟW>Рqے%,Ej(8$}}}2|z)ǒ=h͊I4JMT/cFTd.2 @u}闯#bNQBSu5*K_QҹÔÑkYN祢u[Et\1啡аJ87ʭ\tا 먻Aܾ ӘOmGQfjtlUFՕ۔14vW]T[SS RYǫԓJ7'f};hyE"sl8ֳOI=2sX+SaMpr&|2Wܒ?ÐhWƵcȻh}4}MPFk>oj^3XkB[vֵ+5kךu3ۘ5Ԡ>C).5˲N&C\JضB{/X("RҢ5j7zzk4TyԯshZOMV$^EԤoj:QZvMm*0U*u/:4%wam}wwF+5qM$KI1[vgtZiϥB䐥 4˻F"su<3ʟU5y_*RL=W/~7uÛhz>Um[EiuՏ(AuҏKNȑ=_-o2-Z'K"+Qo \ݨ^Q閡uT#$+ҘE_=-3zvmexrzdQ_ ]c7GQgn\E}\_UkbrUjj+hQMȐcJ*T OcQߏdr*]*^'}T_9 Ga>z+{]/*4OBV"w025e]F+#˥t7Q*ͧ')[>uѭyw֫rջqzm =/___9ّE"s\g.Q`֜L* ԝĮE&(`kX5 ,l F[Q`(`kX5k}x]Z G~~VzȲe22 ]whcOqedh%ZjF|>ne~xPW22 VsUŖ@@`y8 !l F[ ,3 F-XU+&Ij2ZXs@]KTylx#=ЮFo#)M咽E\qm@ b7,WTt]d)5~vXR@P +5\7-U`Nz+9 ,땹n[2sX**hpZsQOɯ)MOu\W/n'S-sjQ˾u"JۮЀFwySQjQ`:Q5s]>W=%C53^څ2ztR^UWTO_Wt+,Rn~5xth_]EvF_-:侬Q9\`θSv3MTq|J= oLm<:v !Iy%W~o5]OJJu+TP_e,^!Sd%lv)Ir?ȡ3+aqaid-ԴkAإqTKN+v W:L_.y,yGz= 4eJptI]׵}ҳ9|my_:Z 8Tz:0gFFP֛'ԳDМoi=x]>{CA֘W~z7M[ ΐ$.[ya*ګ_`Lf;,|"RzFhKutCE[[B.XWǦ-м Jzz29UNi;Otl\ؖ݉9[kJA=-qk43n2?KՆ34=2rԽ-twdtj_x }ҡNce4m9}엯n*z?elt2_KVnNmީmӔ@UJߢx(?4uN}w?.6u/\`y*E-"U.WuuI(p{1Ô'̐A#tzӠr^nՑj*S~?*PϪOyҗOM1ݬ°r)==]MԄJ`ZiW 4V"z_r82M󺌝̅%+INkqBQ0Qit~g.4ҊCJ1UqmfnU/)7p)%-t-*NܡEQ|#"4CZ=pV3[(p΋sj|DQpyH PGW,GvMdDgRF}33˧ZKO7{bd[RcQs{]ocjweTU ~@^ Q׺%߿{T,M1?]]UO'PkY?oHV5(,N9ըV^嵒5ʻ[XaN}ݳkDˡjUlUe(a0uNaW:L_lvɛd(O!z2ٷH㫫}P|+ޯ՚2iJ׿?zJ5}YZ>*ӱ0%3{ jɟYGTzE^rn?o]z,WykPǪ>  ,dun[Y@'sl <^y-t8ŗP`V1%x*]-B`X8LKAoh`Yn*ls[Hy֎ѐ+*ä 0Q!?^Ke1%f&D颼'LТEKned` |Y|j韪cnSsp3 ,dqoҘGi7E ,dtt2f8l fϏ% OVV5Xy9sتѷ7[O=f"\6\*"˖шG= UBT{k#Y BYr1P[d.I6G[Q`(`kX5 ,l F[Q`(`kX5 ,l F[Q`(`kX5 ,l F[Q`(`kX5 ,l F[Q`(`kX5 ,l F[Q`(`kX5 ,l F[Q`(`kX5 ,l F[s^͝-b32嵔'=k&s;=sO_!,KzYpgzo^Pd.@?fnnXwz溊G,5 ,l F[Q`(`kX5 ,l F[Q`֜LJ֬{2O_a0ɭ媩}jU:P3l^a 2 |ed+=v,w)7N{`n@ kf~<\/͊7qzT'!IRvk4uϐ#*o7S`C= 4f -Ӊ S /RBM R\4Bm__ G 3fi$9S˾տa7ifA.۩ rS v]GGF>SIZ>s"ms)dmW)wY㒤4q aR6 퐤L\;i6OP\UDuu]usy;a0nrde,7 9}5qKfhU[/yGqvѷo} (R^uvXu1.umg|zBkDBfٿI?/mӘgh Vbs'k8i5YЇTd GTqdGy\hICʩz9/T4z] 84G;U\_nUҡڱf$w#spX)nzF93u,!MFH Grem L~؝!W㟽N3}ԺVzNb{eyg4vLI޴cO|TeCMMM;|PN画?闽.YfZ,S W .p}=]-@1#_Q-ڜAi\Z9'mL:铑*Rҟ[t`Ə{E=|Pq.KFPey_j!Rxl[`Sd.Oťf` JOQrYUOϹw9$oB-ZwT3^sPD*T)8 *yNPFUh ۧ8OM%'%!ըZ I~%jnmGq;*+s*a\7n#Y:uJR?N+K?ϝڹ#g銪:WW齮=42,,7WX[`S3sЇRzqϟ*IE8.8RZWPn{+B 3\_yCf{?.Nվ-k8ګo^m!+:G۶8b/_okf[[z'-^Kk9/O>gYW}C~[(6v>mWjKڇK\k-S)p[߿V~Gf4~Aiot˺\yeZ):x,XUTa0nNڣXO;\ރZ(Z_*-ܜ)K#"TPϗE ץF`$ .JQܥƥ*(s_ܮPxe=vXk))+5^Gq,IV=Fsifc`Sqxv'ZSwv*ܤ:OۦvGk!Z[9B93o$VOjX~~7D Oލ呡Tt,Ւ,V*HwRǩQ/ֆbʈ1d步C{IWQwߏVaNkXs6MEz_XU6orKS ugS0rTօxr\g5ZY=*8'M[]׬˪y󺺧)yTQuEV8mK7*5o5RS<{u5C*ezʫ(g~'GpUj@j:y81.ow> wf ~Rx:jTJ8$#g7(;b{nt;5Gf!7V$a0e Qw&yQ~h^EH4Lm]ѣϨe>}3\,*Q^T.Uysk?9t\"Kb7UYb˲˹RZZzF3j' s}}257յ8W#41r!___9lǸ\u~"9֜L7CC? ,Bl F[Q`(`kX5 ,l F[}GGQa HH(7QT@E] ͂] "**6D)/ @-B Rvg?! j|xfgf<;wDDDDD\ X""""""""T' XZODDD.%n=?HAfv;NSm,"""_2 l6+")s2,` n S"""rц)b$ We?9fEDDU,e.3ןJCFO%改DDDKSKDDDDDDDD\ X""""""""T4DDDDDDDDĥ%"""""""".M,qi*`KSKDDDDDDDD\ X""""""""T4DDDDDDDDĥ%"""""""".M,qi*`KSKDDDDDDDD\ X""""""""T4DDDDDDDDĥ%"""""""".M,qi*`KSKDDDDDDDD\ X""""""""T4DDDDDDDDĥ],;K/A ""r^f{+:B߱_[ǾevFSy RQ\r `i2IPJj܂N׷^2gu kǣ"Ft7RKH9Ox \S%JV깩Y.9\\\""\.W2aYV0 +6|2vr$=-ICIc',PMh/ P5<*ܓAf~)=7yyoxW v-B*㩃BDeq E{DKKKDDRZbfXVN۸wc"A.ki*|<'v%Ap4ҍ^j[zauLeބ\I4ғy!/`#;RHՄ\[wTfT>la[ZaùE?&Ӈf_8k͎ {d庼Fi_HǾe-I*v#{IHR~?[lN9 JT"7 &!+YKKKDDKK̬5,^g2Jwgp8]zXrEТ <:cRqgkl}kn1cy LÆg^xuoۂ_lY9W9 /Of#3o/{y,V`[hVyB|>k~شŞTk w]#Nﶳ?ǁ'D_Zp*Mg֒mgXF},.ػ7!tO0&<τG gw''yh^[tu ((I&''<=VbnsYd?1V=^Gpw(^˘`iq$RI9j%E?܎H}wMS&r$8>}1P s&g4yq y0#B^xy.[ OBItvObω*`)s)s)s2i.2ɴ; %׶ξo#c&m8 Vpbf,eΚX('¤ ?wR+&H}ܕ.bPpN0Y: 4 j8fn3HYHmvr89j IMʬca?jkE\ 7"_MidS3XM["2VeXw^Լ15N|IED.G޼(^?6~8OuS9L''I+&#[_pȉ@بGxnkL WURJKKKDDOZiK#5lhјp7_V%+9U]&͎,!4,Iu?-)8sN^'X,IY䙍 `58\yc!֨}o-=4 ֲ`q(|X&ի_A;yh5W`ZrQq/ vVN^gЕw#%8sXt-36$ <oc*goޟ5۝kz3t>/f^# *仟eVO,\}m~^UzJ gf7v|z""afƄ#5?S;}n'z آbK>x/5kJ&VRRe.,`YëPfp$}C2zT"̯ +WH 6ۯ,D|V]x]DCv8# 78v'w^6GdveGz5ͧ1aB0 <*5f3.(&^Ոr?f6+g&Qj>y~Wh~B1( X.NBBŪU"E IDAT:qfZ0|o zx^{-X,v=6v8KεXtRf<0ΓWi>Ϯرv-)ðDDpBSj4m+(/˽op* )˖q=׋fv\aa͏)tUZ,T#ܺݎ,RŌ0- ϪT׈uD]JAu$E^/ ur/[D,h\ӆ@lylL߶c Eקagi&qE,p i4.9ALy։eY#'222Pg`b쏁`sÃ`o,rdA L0ltySȥΉbg8d|=ڈwԍ7=|^6aVG8lCf"vD,e.e.e.e.e.,`a D}xefĉt-konOy;yBs?O:7Z[N2o@2g]-UKxaa6OKPYػ5D~H:y(HAr ^NѤ-U7i'ͪd?;6d59iDEPIơ|0 ||}Vh5-m> kⓛ}[n}OK3Uui<O` Y& vgߍQ۹/~ XBӾ."r?}ݿ++7[| 8?=!oJ^KI]Ϋ%'ff;6EUQJKKKDDKE X%16_/`-&t'82KCO Njxr6 AP4r;da\9t02 vnJ%?Qv/<]+\7M`h :֣mxvr@y6w|<{tmOk#,YϗZ@TBsXm+m&'ώտq;10,y¦3,=MP^vV3cY6ְ8= &7v%1}IV0o/wܘlX47"]Cπb}i+ ͸\r#MֵX#=g[LURRRe21t! ip8(**~iӘZo gq( NФ""R%Mbjd_<==qwwjb=qOKDDD뗙K""""""""TfSȿB>ZBDDDDKDD|W14DDDDDDDDĥ%"""""""".M,qi*`KSKDDDDDDDD\ X""""""""T4DDDDDDDDĥ%"""""""".Jsi5\"""re.ۥEDDD6EDDDt 4DDDDDDDDĥ%"""""""".M,qi*`KSKDDDDDDDD\ X""""""""T4DDDDDDDDĥ%"""""""".M,K]q.vCY/O#)xL9Kmf٤ITeYkկXԾeK4s\jkfIlۓEӛtI(Sm+ }td%^oKfJ?P!;ƌhaMn]+:aDDDR3d䱩Wcsx*jded\ +Əߤֽsis7Gqe.eK5s,Wfڢ <9G2d>&sgunj~m#~`;x"y5>^tw݈hr{OJDDW8ܲ\_?|Sؐr?$P{-FhtK|5c)RUb0&ZUL \"""\KrdWT@i/dc{ٶ$~ ?YBWd8t^[NR'-Mqd2MάKm}!Oa4F:Td㙵;؝ڱDQ2}r2X?m~4ޝ;XT)HV"?ǎoqkәup/dc_Q=π[4O RQ溘2k7sX5y.[+㩦%7:s8~B`_ 9N XunG#G[, H7#y)_]R03[;)l_2Bb͖̃$o%h| pfo'y_0q%C#I9I0SŒܹ={c㳓OMbي{_B++z(=5~UQ2r` yq缮U?AVC\G|nilܐ#k'[`gۜE@fg.V+6O^wLlix*ܶTFZO\wuuǡCTz{${6d\WJxxyX_YaA\"""\crw_=z.O˟ONpfInv..DDD}ݺ G&)ke{Ôg5 ٻ')n4I FDDD.Z;XEyNz):Ⱦy_U6~ΤO`Dq=y!d]_W k3HDD.q2v[93)X'P?xYs1pjּ8{q ›s\[Kw^JSmڲs1D7R&lgkAc=y.kܭpvv675/PiCi8w˺nw%!2Mr McU{2222GuZܼ"[̪q\_UaI> >=y2=?1E%V*D߾w(frfWw-U~a>iO{KLNJ98Cjrqe)Xͮ,ڭ7m$16͜EI:Wvݸ_3"Ln$~Q~gwyೊdn>j䉷Fl~ڜbBrͭ75!ЊY|6g{s,īB5귿[{#aL&pS 1DlX4>DFj7ǀZQjTfu9:!/cEy8//}8>`_wX$[n%?+i&Xyx]vPS?+`{P}k<|,ټcLDL+ny:7_DDKKKKKD,XYh, Tvpy66%yޗLy{Z`;fc8PrnxaSQN9ͼ ~wý5g:yvBqCQ*'}jg",y=˫hvS\)o3ɘa- هغa1o ~T{z[0hB11'nAkv( \k3n4Q(5gɁWr#y c9zvƿa{w %]k]79gST;:N,>kM=>,T2!B,m5K3|bHt?6d˻#/yN>#c?ql'T2 0Y7 8'C ~d+i=A>ސKn1}CBgh(S0vPkwA dZ/w-$}~XOGYL]]0ɓ_㭐vURTDDKKKKKb%F;&~Xm+l^M\4ɏ$; ,LxMe[/ V&% WL9\s-6l'E嘥\k!1ͻqTvGeE㎷fnBÆ"煯VuܳؼpNNnNf훈4V,;Y{<ʊ0k=|ߡ'WaοcL+uc/U9eBw;簭eo;<jƁ5>ōzհab:eVuwXڙF>ۛ/4e.e.e.e.e.F`] SdnڃÙՠCnëRkwZ,zަˈ9A|˟î ooˏq M$w6 ƻfRec>kB5:bhnup39wumB%yd5x'D\@VjY׌0+>R<}K9W/!\qG7~xl&O=u6FœcdF HV3%Ԗj,}vZKD߯I aDd-ݻiԝ(?GI{8^«#G~~CU~-D&kyk+jOP< ٱd![ ;'u{^Ko'pZo~fbQ3kXqSan-Qo 5 9]ii#kB>s*]nm)\ j\V\[جQ7[,DMpG3|q;ܺ>C m)cI+AQʑMCBmUG7}&;sObzZ#ú*ThE xb\iB?'#,}g& IoaX>3TcKq""\\\\\"=tki8(((_48]4ܸ,Hȥ_$FwwwVkɼCG DKDDDKs`(dۦTz-DDDDDD!PD.'MIS52?H#DDDDDDDDĥ%"""""""".M,qi*`KSKDDDDDDDD\ X""""""""T4DDDDDDDDĥ%"""""""".M,qi*`KSKDDDDDDDD\ X""""""""T4DDDDDDDDĥ%"""""""".M,qi*`KSKDDDDDDDD\ X""""""""T4DDDDDDDDĥ%"""""""".M,qi*`KSKDDDDDDDD\ X""""""""T4DDDDDDDDĥ%"""""""".M,qi*`KSbf{}ӽ/8ur{V Z^f_0hd"~LSGDDDDDDE5m6'3uS.j;?܆Jt|Vf8oDDDDKKDD\lj'fmkawW4ŜS,`ςv;[hO8~X JBq=2+l6+^/Z UncK>g6OU@ymoގOcih O_Ž" (:>uG)vxDP7ne.e.u3pۚ~4^GZ z&LlזGS3rjՍiȿ|H@N y{'농V_+'X|8 3n{ݿ[Ǧkn%g'9خ<[G5éf2q }"N╧ykn-ʞ&2f}'_K{ x}P"""̥%"" .\m N.ձ.E8vpGo*YNg:Df MOɕWgÑ>`6+$wSlp%m?xE;/d% ԢýX\u_7~~'LWu6PAD?G,QRk92ޒs) :yU%""u].0wz_ S0dI]QDDDD%V9yw`XC'l?ݞOk᝘H=479'"" X00%"""|6|6SS&n!D&h](~+)qtBVR+vK9G!Z^6%t:EhD ]yc^HRz')4< R1aǚaW?5[-yt{K}c PD9L YtzQVv55(p6m۟lㅛIjkyNg>]:VQ8 .yC>!nl~1oa|T$^5ʵ]cyPFA^qkt}^DD4DaJaJDDDIb'o%Q(#cvR}ų{ ye|_q#\тӾ?{=Sibxޙ:3|IZz&L{ ˠ7sߐARÙφiĽ/C%jϸq0~̝\zeOד4ۇ̻0 ,v'g%0xg;&6pzS +WeߘF>y&YS4axr0&|0_?]YoNx7_+| p`sRvJ9sЬY3NצNJf͘3g-i"Lu(7" ve߲j~ٛx/BK]*`ĜC%!oez{ S- ʆ~ZNs$O|34Lu{_ Żt,Wf0T+xvIvIh:v熩"(y#0CuK0չ$L9/Lu"b-v]qnzEkyq|{4L̙ó>+B~~ʈ#u~ d,W#wxnz!@ {邈RD{ GJы tBBٝy%( Ur -3Ofw}fiXq87)s9RbΌ#%R‹'RI:1wm\tWn<4y%3 E2.w2˟KÌi r\G9=eNթAA/lP%+IJ9Ȯ({J_(jR;Avr:ThS d.ʗ&ǷLG)dFx2iњ^r5{q%$ 9ɓÄII}Oi&p!w7>}еkW-QKDDDnp2[>nؙNu`̨eq֟ж}67+ukd\9Y0b"0#M(dLܒ uυ2qmg\:v{%[=W+_ 6q>ظ| ?dz_rzļu5jur׮]/'"r7SKDDDA)r 8iӣ@Š5 (+ࡢ`tl | & wDu%@ڄ:J80CNqH*6뾊xshҬeMp1C)dq7VEprijzNRl0 Su4WP+ǕRmfܐzNg_Fk\nsRD ,QRR)Lz"{q$'jj^N}Kr$ÙU&2y]Y Z3rb00ݜ:G_v<(y8e,n`~҃ifN L"qH5k{0Βtx Xbyrx"CS/kmdNݪQ&aEXxB0b"ِhc8rRpƵ.W?5`>D$PKDDDAmyX>a{7-aV|es4v^noGA:Д~e~|eiXOzWƓoi_a:9AT?9 \ ?>41ucKgRIK3ȟ]"޽/FeO7~^ȣp^m;Xˢ4f,ufz ѵFbOxUTLaClL$#Wj4x!+1Z!e-Ɋ)))/E[aX>ڹ#W|_tfWΫ^f̸J}1&4Qmq0C*O<ҥynϛ03r7<ȫuʙ6|q_1v" VU gJ\>cV zTi'7S-LC-(L)LDK2~ӫ0 jB$"r{” 206|)i@zUUDD@,0%"""gtY4QӻJ))-""""""""M000 TW!wTW*ab`"RQeIXE[Dz-74M CKD<qnGő;2L5 o0%""N'=ay2[cٖ#w 0l t:D#0%0%""rۋ*y}~,8 #w۶iZ9~*y%"I ,))\jIHeYض ACPj`)L(L 4p²tֻY۰ˉ 0"RQAap80M/oolCDD<X S" S"""h%"""X S""""""""Mx45DDDDDn =ν#R2Vse]Aj%,d̦U,,ֶ+I_ݔ|VᝣaD'"*+J;396Y?U$"SKaJaJaJDDDn*d6p2ov~;7Kd3יٴWG0j'n_K]aF#"N" ~;ID Y LܛFR+ u覚ߍ3T*&ĸ|Mǭ^S߻WUw6sa*G{ oM_DDrǎ=yOjdh g5+ &Ͳ$bnMңRg^,m8xQg}h| × sȇm۱zrm~ޙ {nͫ),)R 4ŽK3oLN>Ue%Ys{O g~%ʜ(%fog R|Q>.̋K'퍗3nm;ԉ\>.N'cHjCR*ELI$&9^y|ٳImξ^f:Z{o_cLfJ4\[^?lb ٚeq[>~LjR6&,XқW7(jsF4Rgn48pb&Vq8DDn!5DDD&EodOTQ"=/oWa1z~qXp}bqX')E¾~4Z2N]ѯIG&׏ 'YY"F!F 7;޷\I2`c%T+)Hcow H\e6ڜ7Q`') |;=% >ɮxpIi2&J vfJt>+xs)C2'LFIbd3]DQZqzW9,NϘԳ4_i(7)8g-:-edv_\ 3x|OfIIt-™4>ܻ_aJ|wj0oV~22xy>XqG {`:֋BDD ,QRR4WW/7jUHl=H})d?g/;jlqQЪEkwEWkH̜0OanBhvՈeT*= M b(E)xLa X?؞;?8Hf@ ]6fLH*Uκ!0+P`.NΦt(swԠ}RdIUIJHɂĝz)33nu#:I<XMSrѸ}:TG2Os(o% S S S""";wJY$r2Yۍ?Mc8.~00l*c[yc9Z@ݳVR~ݘ3q팬r~n) }( ͩi͑}wn-dgyꁉ<;sk`q[ㅷy~ML²-̐n|ѹT#TΣױ7i2xbNy׹+WgGC56SkYt(!am*{EƴX?}~!L-mNy}"[}q-n,LƤcÿ3*+}!Qe)nc:Sk1o"hFz9U igqlI|xmTxmSoOjD#(WCW{sֿ tVXBS3hnBc?3*x8Ɲ Ps˄SOԡizƯDbFٓv ٚ&ί8N%0[Sw w@ĜAة$쳬1Ejq,r,TvgVzDn'N2j`)L)L)L0rso|UCNR2Ip^rNo30%C[<@Nɒ%i:& GUjgv"%ϟބӈfy62{_4*}urnzFqwpчZWa7>M|KGojpdm:/c,gh5?8fY:T+Sz콼'(1W4YޅI<#I~r `\r0ENOgc#?oDDnֶkz[, MZZ)))t;0q{u+8pT,/5|>X?olMi8s>[_ɹA..ĔP+K)uη`'"&g]_uc'`m|ϛOaE6teb>|s"prk-=Z̚ZOsNgn28Z.3|ΒDyq1hTxcsnOfx.;k.|3>q8һHvή =Ϛ}SΏ]"痹gH'_Z{>_<$p^[?֧xN~uaF_$R${9Toq/Q f"'UG3(oWg‚|Lվf6f\4o2d 9ruzO?*.""QS2(s2_3q%CTDD䚩%0%""""""4 x45DDDDDDDDģ%""""""""M ,hj`GSKDDDDDDDD<X""""""""x45DDDDDDDDģ%""""""""M ,hj`GSKDDDDDDDD<X""""""""ܵ6ߠ"Qhj`d5|ھ;ygxғ[)wZYxmj,&S% S4O|Nzu_Z/~+aaifwڅC-""Opa?2Zq׭rچQ?\fϰ:_cXxy؜2Q}I "Pqћ0ux 5DDD0VS ;P't!Nlf$5,ݔ^>fibdssdlm""F ,)))EѺLr+j6o<ۍ669#?}KB$~47#L n JW}4 |a(˭]},bwrUŶ1z,6tR<߿>,&}:qc'-GIZCڗ?KN:Ģm.(=ݻХ^~h*NÆ̜t6rQ=61+W-^aďfV'bH}^4tzcnȫƐ ]G"-덪 i hGOeѦĥZa |։(6 8qf2?Tx/˛qo&ƽ2y*R'^N<uEodIrP*SK8c~Z(s&w%B[PcGC$T+OroQoϲa(teEqvb6nJfۘ5k;~4hq%}g ,]-l_?J]OpPQm QW&.ͲǰznpPq N]ƒps7GUJ)-zy8sOxSAKq33D|KK]|)8)Cc7/Gt1aZG;QN<Ξͮhֻ-̪fFP_W3-2䵲Ƭg̛#]o%i fڛ頝y)z6? | 7/Q$wD}WdԳo3ٷ/lwPsYf[ƙ0, SkкYQOf9'.sv0bCkįwR/C2”&y*`] SEυԣд Hf3&LMYq-Ъ[3oYwgMemL:%sҲ S> b/gL%a*1 maԸdt-3TxL/$xF.={ ?>&3tz zU_09~X<'$'""Y٣޹7$$MOz $i;O=Y`poRtx ݟ@u_pSsDu[eAJ#{gAi N`L8*?ggHe'$dʘ ا#,TDOn g:? 8d˶G|Π0?Τ@v4 F8"מe,C#;oU: cӲ^xl[OHUsmSA%rFEq AmX!>iEHvt%'"YBas ϥa*MZ=!І SN/p2saxȥa꿵O0ɷFw6>wg'd&EBc=ψae+2t0M^7ѥRM/1!2#L5r>L8 ?!uX4jx%lSW>xa%drxVg:msajL6'Q ?uԋoAa H4LG&! S}-G*Ae;ImX0EFa0EFZaqMDD$ iԾ&'rPE !F|eLH:߳rC"FAJ‰/pwE斗_d D\{?^bc8m>O1&s9y8 f07 gR>.`ۙqW 5NdF"O uJMNV͙SqlpEKya>v ?;2WbHn~xgb77} ({lhx9rp`}w^JY4?AP vbtn ǹya'oDOBTӴH%DD ,QRRRerۏi7[#ލU.}^hDa=6`bpdm#ct^-a$:)2au( !04>I{KPBlpyr٧1۠6%c|u8Tm\&XQ5f%pZ#XP~VX$%`sb-W0N(ҌgT'G0sf9sm2Lvcn\9Hud$AD#{09r]))үkf1liZ~.>W% S S S S"""10 b'O3j㾘GZp(a`et+or>Vtc~휘4=֠Eٹr/^Q=7F)m%>(g0CkаdI0аNѱs kX{ʣ-Z_K&1/3˖pҺ>50X|U؋pgSU: \TlX"yaì۰$A}%l9CWn83X#w:7UK^2) ؜ٰ-'"j`””””t _H 'GC)0]WSrۮ3oO+WTm[rX|Kذ=mk4-{ʯ_4oaZhKہyᷱ9/J-|-/wW /_=vzA7j2%S$ Soy3Ѝoxݑ^G.3Kk%~4Y=CƟ:OvsAD$1l۾k,vFJJ g\^\C?sĿ@ WƦT6 ;@ͣW+x{{p80c\̥%"e<""""""""BIGGU @g_%))GSKDDDDDDDD<X""""""""x45DDDDDDDDģ%""""""""M ,hj`GSKDDDDDDDD<X""""""""x45DDDDDDDDģ9Ug.&˺5kIKMUADD<j֠K(]J0%"" S"ryO>%"RSYt֬eȈa]w0}0 K)Pxhϰg.D6aXe-; {M3V0%"" S"r֭Y"Zn35_MC_^""kx45DDDDDDDDģ%""""r8(Ϭ "b%cP*L)Lnika^zKnN@ajh{XaJDDDITA'"""7B ,))h֘x45DDDDDDDDģ%""""""""M ,hj`GSKDDDDDDDD<X"""""WZяWN@aC*""""r 允M["""7@ ,))(56XADDf0%"""""""|hj`x(rb<^Ê㣒Hܲ0(Vai|%"""rx?]x天(""E ,LsfF?*tHlJ%"""r !qk`t"""YXrTZG#la,#@5DDDDn&;&ݸU {E[n ppR""""7R%cOk""%8Lٜ!,L՚IX"""" uK|6,{G[l,hQY2jyr""""8+ qoSj,NBaʿzGǮ^WQlvr""""#=;VNbƦt OC3H0e\ A>LYhs9""""7EzDD$SKnyr8L ;tUJDDD٤A`> r""%6L,N%%U^8ۗ,858uK?h"""YȍV1p e<wL'Ŝ6kgi5DDDDnE@/wɪ[ȉ 2#O!MiQ!DD$KSKn]JPd #""""""" |#/yy⏏Z=JOg0{sTL+ "";{7*:'B-,[R &g&#hDDDDWN]ͼ |h "Vei~}qH$6ߛȖe<ϑMSg }>Ƒ5.S`yzhQ/1 :Q%ߥED.N%Ț\k:""""`>uơ539L>D,^>@ۑmVVˑG#e""""r'm1 ޿o悵i1ּM\E+W̪tQ'GbH6U }t=#՞ Pgp r6œ-:nC/<um鲈H&rsX{Br`ߎf5zu_l&sJ!٩L_vsƺy=K| ÑJ wܜ=tCPm:""Hac66mcN __| 8li4Ĉ5gu gq |KI?3,ůH>JiF6(V/'WmݲETܘ`ppGED.K ,,&t<4\NSI]))L6;-Wq0Ĺ # +"I!r,۞'::Kw\nLKІ IDAT֝i# AƷٸQG]XW_]m>˙1XXxm5*d?KDLj,Tqa-AٶI?ޫgK*FniKa8Yn+x Ҹ"rSKDDDD$HgQH Ӟ[Ȧ"禫JתB>X6V|xa4W[Wk`YQLj,_L~ʽ,cϾ|8}{ DN_JbR6J/..MYkѤčDZuTsӈO4ki0]eҔGmƵǁib.\qqm_[eYnHIIہK`-4f8= qɜT3˜Umh銯/8LӼ-ϥuf.+02>x QofԿx@s>9w&2]tV,B;B~y>cr7‘~Ү^eP㖫y%"w=5(׮ϠޭLGrM?t흋%O='I`Њ'ŒM @a;9bThw/#]Mj(hq<ʐ7jPirZ=LV-‹Cg,̠PuB2Nf-|<Ӵt:a2}̗pG^|)E\;<{~fnZ 5snySP9p:o]zu?S/ىq0r;Wi\cV˙>i~w|:X4s5: &v6t V d - 3_ß4X(Tȉ,[Xl7^lruJy1ޛKG{ѪN6Rgﷅ>U&<ܼX2a S#Q񱦴 CEV-)hg}ϑ>éNbx˗ؽu?Np|?5pet/6_ llh]‹tah6J)9+nm^""&5DDDDDz1>#QGps&m -̐0,I$&Z 0M əӑΧDžs+d֡q<^{mXdĸBAk1b )~ڂDDmj`l$` \!hk̟c>ȟ:ij҉;}˔7>c~}1B)Q:)ص)׼Je\e<2n3M?o Slīb.e[)W"":5DDDDDz6ֱ;5>CG:qMcǭ`Ԫ4XZ28-E8t6ϛϱN-Y'*|٩IH"Exze+`w?"+Rx83;?6!qxbNᙾ8jqxo#޶Iaw/ڤ=*y0=y׭%%}{!/>lPM^iF{$?90z .\e't <׮<Jysp"Ba5Y,vFJJ g\^kY "rrm͸"]Áiޞ\\""̥̥3DDDDDDDDģ%""""""""M ,hj`GSKDDDD"zGFrJǛPhU "kX:̢RܔJ""iWxV "kg%tah&""uҳꋈx8oB_n X""ũu+QCF Njdxo4ϐ(Q ""JlLLb ""9U[ҥx*'uaY;~O,|撤ˆHFw~Q;&YE,WMByd~.UDDD.}^DDDD#9q`n""%""""| R8́BX""+"""""$|UHbUT(ѳަZ')'8NXib&^^^03糪d 3)?u(gT4x45DDDDDDDDģ%""""r8+5DDDN*}BEDDDDn׮ϭb5DDDDDnX8J\~GPDDf%""""""""M ,hj`GSKDDDDDDDD<X""""""""x4J """"rvgM 8  SUõk IDATxw|TgΖBJB !"iR X~*رWz/X@Q;B B{GC$d=;3gk(\Ms7A܄G\pa[;pjs2PRE/\OPR s u\,P5Gaai %LaF!L:s8\.t\g:9Τjȅc,0p8sL۲90 (IJoTbZt7Ga)t8t;Zw "[}\EZ)?OZJu|Bm6j=U9.hrΰ֝BD?m`ZW!"Z9Cj`.cHY/mh1Ǚ;EXJE.z Us\m%ayۦ'\?'yZlmUn]\ B1LO\Į+Or&Iey\W}׬P]IkU:Yʴ+9AMs7A1=,b-D|$R@p922OʷaH[5살{ݺ!Lhrh>^vf;ĺOROqߓ";vd,Gf>%I^F j+~&X/$נЦSDDkW3?ǥp:jCQ_Ԡ׫5n`K[=*(+u P""*$՛Uڥ-rXDN}ޞxjr%,iMNo V3msw9D;vl޷V9DD\6+?%(N[о WR*(y1.\p~Pbކ8C|Z΅ٿuu5GIN䩏mrӴw'"":kc섾lǗ>L Zj {ޡB0CDt2oT;0/aQ;4gu-?ugǏ|k[uc/?{L7j("?/wVQ:wZQb άԭ|aKk}0Ί&qCL-? bV *+uWm-9iK>nZڕ]wSssLǁKN6ԷqUl̫- 7Z;]P7U}U0b@ 2» ic_fCDD:EZQZUmt[j~VkP$u]G=𷄸[:wtN50fSDfxyX솘L1,aޘShZzM"bԖ%2Mވaj֌L-!"":?#=OW(e%OeF`pj];:wJ:zy 'اZHC:;*D#񫝫wZeޣ|͞UDkS{gi)u+xkX?ylϽ;Իy^mَAw9+4)8` PDģM?h 6,}C:ۮ%%HeOUu_}7QSÕrhgQE9w.Gt\S1({ۦت_hT=?:;eO6XV1wmZnTr<}oni+FD$wրލ/$y,J}{!gF榳XLJg>Ŀ_TfUv;DD_O%Og(\~S%o^wzhP%So mί?w.O*Z?=i.GッpQ0xp}/<{})4MUPP74 >ա[^AO""]'Z,rFJh+p;E(aloH46M1E,ڬv r3 Dg 9>yGpmÍI(CNQ}nX,QDC Ws(KEV(4n/ҏˣ~aZ>-8(M֢25la= PjY\]Ms7AĕU5_uiWs fN2Jn(;rkY?㹆xZlm O\$a[3 n b&9-HnR.zcm9B6L1YӼJR!ߛCi]J@q)[sRwub2 b&9 nZ˦5w,ÔRW^.}O#Ph%62gu0- N1+Y[`_}% f6mo +E+ >xƖ-=\9uEZkUAqSJ߷o'&&&w= 4msfСbpy#FiW3o|ژPNmݼe~b)P6hM4n8ʴ+=9(ڭ{w}M,H524Sqqej-L7Q|+bb&9 rH,+K{(@I+-Hu7nRw@X͆~u]\fp~5:Ha,*OI]٬_#Cq~#s 7Tb.ZMдR-c>6N[秽X(SJN+JY=ӑy6қ0]NXBvb0k3٘hfx4k4,)ŕc} \e^ A_6Wyŋwj^{ی-MHL8cB"0_v|2Ǫ+?+&.)5#+w hl9+ĺ1M|$W}ʍo |#yϟZa.ϐ 1xp?Aaɉ9t#bWT6rW}1}dzŷRTӿ^EČ~7;xۑc҇7g~3}mf>!#w{xt[1όE 5)j:+"rEyJfMQDرF)-pUi,۩rC]qkSRm[UkM8/r*yJ]9yi1Yʧru1;՝ǟ@g-XצO}yںkPt,ɱcKyp;CjA"]҅zMdī'zJZ:Øl/,~a:O2RԆH׀qɢW~.SUVldhQG, jusHcW"ʇw*lEE̵#NkPT&){7ڶZ+DzRau\̡S>g.SVhߡUyn9g?3vARҚ%{4<]إGgl!}sgLsl2%LQϾ"}8o5*dRZkw%Θî.TP_icO>xAm~+羧?5یxjo~IF{罁5:c;_[uhw n7TڈiLx^UfM}thCZR[EUM\1b*akHQ-RԜ-vxawfv{>9UD{?U'-ri_7^)P9⦍zjm_|O6;MR1v乔2^F7|->3I-0-4ȩHoڧ[e1l6ѩ3ձ?v? ZFz*]9,ؐLְc*6^%PmIֹY9bs#?F?OjxmMݟ;sɩCuDͳZ:- [>kV`궾GЫa(KN7f|k<*" e>(n)\" KQ\|+k_q*M:: %vV~,)]DD֭SO\x*ji{Yd.-:skw/y7K1T,*J.0=MkϏM^vpga̎#cY%uԢp/J:D$-q;TNs :t9ؖ+^mufɗbD=oJrrw o>Ev{G/9Wү]ruD`CD{"4|G^r@k>ߒ8icX۾~SjEgk>>9!F`_r`SV|cNwqٖ~!.h%m2H][d:~p_wVF dYulR VQfFge'֗ 9qT%Hfy<ʿ^Ֆ̈w}nO]h"-: y1NOCbsjNh)Źç%зwճ]|hn,QBt(L Uho6|OߤI1uRtQ+Lt\CM{c%?28:??E'=\ 눑"5}.G/[o^|XƇO_皷9n[,(ti n b&9 n@(%Giep:WvEPJ43H:t:oֺFTTqbm"0rtݶXXJ.t6?=\ZDk0ނjeR0 Qc6,]%W]a;Qבjea61.˥(kRVJ7 Gq:\E)eXl6f+jbeFdpl&s2$"^{-')؛T8-Qx[dvZ\z\Չ9zoz華.!PVQ1pM\9QK`.P\C*1Z_1\jU?9RbvFJRJRI:.j:[x\Pq%7A1pMs7A1pMs7A1pMs7AD  o_G6avq5F<13 kdJW?:f c13wWmq8= ѷն[RN+oDhszH׻F}b|K}|[!RzδVHUj.JLs=߷%d{Vm󑇺==sm\{Գٰ^>7ombsmn?x,eѰT6=ԅOiQ~6!Rq; iX]"JC_/({HXa*1/æyA[ԫ,Gѧ[ "|nڃG_t{+!хzR7훹k_4hV^s)Wϗ_Ś#=0S9%S}|/ ~{b'̙K7ioV |4p矚oMu) M{b3jsN:n_UC83[l!Ƽ\po}"{sG5I'o={[_ڨqc+#g:fvC Oǣ΀ߺ5H٧'m:ݜw^{U?~EK,U:vԪ~/uu=SCffO];N_3}l{v?uQ_ |Ssȅ> n();8H:nپA ꟙxq\Fζw7hү7w%ZqhB~J61O˳%/Y~9'f̮_翈3n6=gWҴǤz[2եi{ieGo:1O7C_^xMGJ2 3yD[|_+͔cyZYkw r;~g;{k k'Onz٪tΜ9z3f35jT)H%srRrDD+_a 6=ZtގCL8'Ğ0E,j Ԙn˹["9ԃ{"rWz| ,m)oIH^+UU=z^ YjmU&"am[G|y#P 7@_}3?%y վݵߕ76#Qx:[EzHhk|iLZMbGoelө浼*@Z:mҬu{ 3s{y"%rzaԸa!"MjK?⺾Zc]ZmoiY'"=n>3i03L)wS1Sc"ZTHu7\:'/~OySSfwAJħbO%:7'WG66O_cKX-}9*FԬӪK^.cJ^uV\!'X?jԨ-[{"Rt̘1{2d5qHIFp7VuO' N s_̷/_& tH~d kٵ]'-ts=r8!>fe[R'yOpg!ke=ߨܮWwN^bq.eױM(s-ek}vmB7|:eEKkNWC_s> 9*nA3xֶ|1+\Ko(n!"Z"߸RW!^:wіqɇڼ4nmioKQyz)ЭL2ED  2kP~^qo)kYZ,JtR{}woD֪QZZd!bƮZP 1_ ݽZmU+eM /UTFPDT!:fav<-3E%6֮= M[+"p-pÏWGUyt߁,OztSL2dȽ{gRY"zn;K22Zt tkwpcnlT=Б~8.Pz74P>~xW/o!*=5ոUK^ 9\:t/Zlhy+[3k>>9!F`_r`SVJЪ* UѯJGu -zMsW -H8G]ڔC?y/>9< 1ʷWj%z'WNy詍, 74ܹYD4kc{|=^Gw2eʔ@?~|iBJ!yR ^٢t_#ǿD97lڸ/\jXo}aE\ލ:nƏg" '4MUPP;dߴ?V#+Rq}Z/@I_Xou;9ΌAysf]q;&u߇/vnpWՇ8ド}# ñc"8h7+5{"WSYyYzyývb)r5ı~v0&&Ml5{woNW. ikê'L16mp)!-(W;΍d1*\߷:+a)}b pPvûS; g'w(A~_W,mAY^Xpb&9 2L)r8=BGw;"2I)eFf֓&O%5pLZnޙCrh`(#Hl@-f!B2PmTC8:85;YL@uKd?s#쵯I6{OJJsż2=Hl@tkd!dtr,@uKܴ)sX7]f#SjSPSW=t(]K^JgQ2 2[e?mJ>=]`̱y 3L5V@脽3GYC;a3+\W{gC;kDȥC߭;fd@mf!9zEDH29BH,Kfq n%ϩT@uKd9a%/m Bh]:cRFaj["s3 ݾdZꖸiE6&F#eBP h#7T{orUoǎMv 9 Qޛ5Dv v\@f |CC}< 7脡(U@7@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@' Bhf)E${OޙCӴ ֵ#)J= PDthaϛV4MKPʢ4tLӄ{Ib {dž=Ws&iZUxi 6uǦN;q`T^3jMTU=Ra >\b0tS$UUî7*@nhvY{sqtpþ=3MD:AN|Ġ s~,[YHL'F\Vߐ$h7fy=}sց7:[t;G'Fp#ꥃ=rMIR Ҝ2UӠͦ*RMg_d7V,²_iV!fh(-}wɥ%R:88|!9:u%z*xH~u]Gqi`4hŅ>Qo~f7 Qeu̹~NB!uP-̏SznqiZEփ$Ykx ;oKr&441œujTE񶂓kfӑ00G!?"%'"<"b_,ї&n==dәwtP5'gV}Y%_UYudvw$d42R+,Z'$Owٱ'GC$ʮJe 󥫚EQz5%! 䢟rFJ  2HjᓹC}H{);@xn ĵV+oGwwklQM*T!p xQQIyr=DZ܎}[xΦ\(o8ą 5!z&ZR~H47vmq*p)Rsmff()u%Q&""LWJd$iBh%bC8UX\H";hA M%fUܾ|Ȉ6RY^~Ll濸|e]{9yGzr@¼ )?'X,vx/٫ctKrə=J6(jU5a6pUCM?}yʮFu]rS2?v&Ÿ$~7tu]*h_yIa;5~ IT/X=pYB\?_1o=T@!.}jMԊY GO-jkѹ=W,>^ 1S7$I !ZFncK$rL_ckX iu7^:,qA=WsȲl0VM ?@w$I8j,@$IRh4Fٮ9zӊ$)"Ib0hJ@$I3^?% YRa!. ߴrK|ɭFڎe@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'  MӪ{gM<50_;7h&T&R߽#Z݄S{nN@'@'@'@'@'@'@'@'@'@'@'@'@'@' >VoB!Ibr۸DH><d08 9™f8_i Ws\&y1cr{icx*!RBhE ZzKGfLг$V/t*3\6yF1-+vr%rmܝP4=qҤuB!ԂZfٴr- *q1 a;῞\#zzq;eZԤ#o0,eϢIX"\{ؤ{(BgORrK5G7/QSGua 5P]!+ !~eCIff5 |ߞaK\槫zGآIcCl_ݘ4ر8oq,e'~au\J3~mfݛ*_q{RhMY/ޙP &&M1ŚP3 ԮQ[i#5VshfL6Ω'fވ柗~{H H`cܓnCȴhk9s9ܜx^ѪC׽8qOpxx֝)QJP¹aəg-ݛuk& ѶclgiUC* #DKJpΈԸUhjŠI}V2͊6CTh{֯ߪCzT ~QsCk֢qV;{•ϺE{hd?^:z.%33dl]IWUVk! onj95n 1LBȲ|Mȁ3:k7}U#dɳ3sq@?(5gHSsC!$>>4vDuK|KpHy-ЉQB'|8e丵>!-g\p8>1֥q Ռ;F4t MH E w OxvnsB(AQn|+Vym #]Bc\%!V3?TJ/zսB-w3}6&ԜB79; v|ml!=ܻONmY3|nީ>7}m2;bs_⁒Ց}sJ5g`kW!v 琥VUӴ1'?A$ ;sH$dI$ɲeIVs @ 9N{dddddddddFg@ IDATddddddddddddddddddd|myeThBuk9[{w[ÓmBHa҉S%G?2V7۶tR5yap"PSzEO/=:)K6-Ν&?6'- Iv 7g֥_y5.-<}D;$ !*{k#cf]NOt<1T8<܋wzh}pٙ\W!j듻g?0>39*\-V!\\|{w$l_纚 Z :zܔSǏX`\KJU^X?{J.؄BH~MT 9}FO}'[5Fv(GrrU~WF+m*3g_ʯ31.Gڲpf!$OAϾ?=| B[rU!yJBHuLҙ҂ܪwP$IjP%9}űo;dא;׻.adV39`~zM:k^zΕ Goo'Yz}W`Un&\P/upS{-Zu/ [%1РjXשe:otݙ(OӃϧ$ll7ղ4r2gلX9Rv1Y,.V^\uЕYIc70+-3yOܱj o|]=sh3{;(DZ$tU2F4tzw ݺ.m=GZ2vU8өcN ɩyBn [{6^H;bNrכIȕEy.eBrvuv?ov!ޮ)睍tcaX%:KUJ~b-A\nd7'ͺ.Y=sHQ=wSbe&/V?ah,C{_~_+97&:wH8,]U%tY֥4'wߐULdCݺn{8\82tĽaBԿ{tԕR\ú3eBcB?l#GeJzWz5}Cl?~l39trII?ioUUfY,<&yj~f/r# 5a̱y 3L\j:A:Q_(;ƘifY@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'@'jWzAws^n9'}2xδ>Ԟ#ƣ|Z;ǫno5>$*2~=o펣&&w}qk'UR5}­ߜGP“WVU%KXIw606}bOK8SְG&޲!{tg(W4;طu߻G#|y|_}da߶gQVNvЇڂ:UCD`#KCSRvR3lB>1]\$!|ZwnǪĤ&3=sK6 s\&)Y[Q~d/ؔzw>]r>=#_?eSy}F>P7k`TRY~m'm\ok*O`P}n(T̡ 5!Wq\R-6e7)}Ýnjl&$7Ow0-ߪ kҚgG||*߫9Gb"Kb3$9}űo;dא;.XLT{l*n_wp}dDpw,/O?& ^W>eK9ORp`rMHNa Fʁ$[(<ę_>;Dԫ&zF 0! >%jqʆ/$Zݒ\rfGO ZUMc d7ه/}j@c^zV*96ܿ$~w-\tzNE?y(SR0#{8sSO_Q]̲ݩ-:Θ|1# ^$Iuw|ojf,ypZl^ i}~įwTM!IZr~3FOlE7u2-rˮpf9dLYiO'JU: v$ )~Gϧ"ZB玧~p՞lJn1S^|-l9sвwt67Zo&|r3Iֽ5EBֿ<\_R5o:HmCN],n86<4v\ ~ܿӔF=5;4~ػ@ftsdIvl6b6$/Z@ 1- d2988("Xp{@'@'@'@'@'@'@'@'@'@'@'@'@'@' =@b #U=sTTddddddddddd˚g>;]%/szМ7_,w&ܴF󫚓5-TFC@ޭc 25vjCUi%Osm>{yJo3 鉗?>G>ziɡf jZE *4/=f֔ͽ+'E5ƟNt@TCqqGJ dO/^ @ @{z]yC*/(g9jj֏{:_:&(FVԤS'/%z啿4M|24äfr0PxyVB-/A?D?Pgw?V-(;ɌL!q%!K'e;7lCXdO + =qNx0V7Hޡa!a$'4(tZfZ+{lnB8G+O=bp=վ$׶_|yKN%MA!C1S ~R_NڅKҴklfX,fyLslL&(|;Tj ej}PV3tůݻdR}dKZ䣫mB!$hrߴi[^a;h,IR Nm:ߧ.hyGmI:u j!2Gn4/%v0itgIhmXzr'4sƱq{Vs6.57FE $s@D~sַu ZEj4ج?ref1=$!hݡsN,jmꋗ]ӎ [ܚ :Îlq_=;'9̧c/9b.nX,wo-_ݡ3YF& !{!;;;_UF 8]T7?~ .r~׷_|Ӊr: wLJ2֋ 1hB(Ba=r7[%dJnz ?]h ϖۗ'ykwy5TԴyh}K>ѾV'?cΛE*Uܳn7&=[u6Bhqk-HbbSŐ /WmBȁg|91@gꧩe%Wj-Zwޗ-N~j\]ci". aMX?ߟi1K}ein!ϴ Xˉ%3Pyw(~M;jjE\0#uG>V\vչRz>1ɔޒZ _yk't-[~*?^^SeB{cOvGqhtA|Ykw]>ࡩm~Buѿ?z<ڵ_/p9}UUqs'-d Ng̕#sef-5Ҳlp EPe_8`D\(}ޯ8ys1>su]xmU'}շm}Q wʕZ-0##V-v|GۺЩ_#t{#F6wD͌^ xߤvFϷRB| bT.VV{=<$o?j]+W^wYcN/qQfd^^z%@1R LAJud +pg(o@@*8ûf RjMΦGu%lhmRfha Rѕ.4<WYQeL>൑ *M 9lj6;5GK9U71iȾH^^ZIr{"\_pɗq>}Z WO>zfHǪczIl^9O2*W3ϧuܦ](㽌*Vz{VQ0bnC/ID*^Z)?\{r灁[zrҲʻ̲,qM~Ҩ=}㻋G9S"u0aـ \eaJlā_!Jk6U<.+! uQ_].u{bO\ajJI d FISP_VpqtaҮ&M]\=m֋w6[u/megck, DD/W+"CZb ҺckomJB6>z &lc&7`e&.]L`OyYJ"b"!R ^H*\~O;wt|w0nj:៛><ςˈڿ-L6zL]wVJӮG]1D ftR"{c 12߇WHY>t? Y[xht%a-z:Ua^vXʴŹ93g jJ  JYqOJK٭tg_j;D^4vw6K bb:̚[yQo[ #M^‰#nɈ5Q&=zM+7u84#O~!D1뷥CrIKV_kU8ڞ'j96O"bV(5q5nR""9o)~ݱj޳Vןݑwɻ:[;7̡)<I oChJN\O[f m}myhc kuϟa綷(0t;qv_%E`5h]ܶ@Vob.Yk$97paZd`7nkHA[wvY։xq0<_0qZVR)'!' IDATi;x9fknx7 ؟;LJbX l=q`nh!s@ !l3y.c-bh!s@ ZB 19@-bh!s@ ZB 19@-bh!sCKN->b/N+uϓN4V@D$lZ4M/v0#"|zƈf Bog5ti@Ut|sW73 p3YIξOmINP@|em,u[y2qbw !҃wnq3[pB[qΏ9*̚Ř%ReFйLZlܦߤW_h,KN}3fE IR/o) m_yFVVk`nե3ꬨ?:PK=;=|4a:{Lǥw&QCeqW&T g:Zh&HY[DtuGkJ3JH[YD\q׫Vd7r\7뒘_|kL5_mR W^3ݰ,@]/ ?kiP?'q^[N|y̖?;tg^u#|犵R_;|ȥcG;ƙͿGWظ/6W;ba}<̔y<_~m@m ۉ&zT֞Vlq_MRepl%_me]xϨ6ĭhϟKiSFqBݝqCog@zDwy] "N\*cɐzH3% sU"]]KkO{&E Q͌%So?]I<V]~6$O yox72r;_f1ݎ6#,̇_Yq"5=OJRt m_3@O 4Q'.(5$9BŅ/mHt1ugS ~8;&kϧ&د=!nNq_:!vǀ`ͼ܄crK TM\Qa1G&YoRWb{}{M תo4@D!"}3S1CʒBcmm%$")K%Eŕ|MZ[:R)RI%S$XvrwBÓ&-;?zbK;+[2b#J҂PH,Ddno%ek^Zws jTHYD&'pD13 pԗ`up3ĈۘGli=)K~4'=+-ƝqgG1IWr B9Y1rc#Yݤ,;džq'keɌoW`(d-Ѡv; kbS.Ner##V^@  ̨n3Q>ۢB2I]Z!ǂYa& h}~wЩİ.1͘;q׋:.1웷>.:Ԣ*bX}]DD wce][ZFգnfW3Ny(72& Q_޾dGg+3}>=FWOOSfсiEY7~|<,-J% 9u-/Clbgo\=Uvdb%hxNy]b06:!ǃ'g=_r^%Kޛ7[{"SV԰3ptugw5닡sPk `0mg|H?k.%̥KW/!~P&ٗO߽Hx)o距jnf'w8%t޺ As(Hyt,5#ۚCyջ_Wߞf,#vExL 6 ZJR*vl+{p/EmD]egflZӰ޷؟;LJbX l=_c 4UfѨ+"KHkp'ds@oW4k6zT?sX 5xˏl*EB 19OWR?~v9"RU]ܵ4DS\ݞs/SUWp9"6<V?~نTi1St9"Kx^Q,q+^wDTT7.8vԞYb-m 8nG#.4**y}Pĥ?^wfIwUhTLz_ 9gl?C=voАc?#g5!Nno%?}:4,:7:yv14zXQ|%weK6ܹ%ve]h\ e_|A/8EccʣR^)Cڔ_pRirbD<I%f3M`fԈϯFs]&/dyϯ$˓ X a-ZH46ZʙR9ճ(쯝yG,\+לƷս}M!ZJD$i;gc lWz9k>{e|̓k>?ijjօ)&Mڒ 6=s/L*YHDBm=+YR=mx x~7IDDw:IPx1"̑wjszUH")tezNL2">"?NYp77hKXX>%EGWuhnވr^ɡ݉D>2۾NRm@cKr.=Ts M!O2ӷx+V 橂- Y[?ix xp)o+c|3nIUƙo-UJ,Yz?/ !"*xc1z&R%q$425f,_Qw661gҳ4ėg)kbcV`6uk5ƦF 44pۧuffTqB=b4rg9!!_9>vIqTJI 1Nj+ 8fw˖ =/x;1 xs0ҋ.^DTM<1atteqˋU$wj%8Rכ+.(XVVbJַ1`Ʌ%e.7C jʋjm;l&iY#hG5vz(# 6mW46Z\g_?UyûIgO,V4iggkb_xRf%E 9 _67:7;f{էىUN#,IiX.}cOMi^6$|b[K ي^/;{ cϴ[L?Y!)R# 5 OueR7#O>yk{~?|ƍ c#lz9ts)z!3+ӊ0)#(;>O.b+sgۙieO:V'd_p6q;ۘPZ#> JUC踙əkE>[ŹRn;x۴}CaO4ʮAw#L& 7U /jBD^c񻣣kϾs}WտKmP}XOFF/YujHOli)**w7VRx+K6c jΐblޘ1'+9^[P`ʽo+0tiwk2pvblD߀WˆLج2MG"7vd rAi<=c1=Wvv0P c۵Cm{/%q2gCBonxk%,M ߣלǧ#?2Tkw3x2jS|WJxׂLiS~j]DbS'W+YunRr^ucs5c]DtGknj>T̔+2\ 1={LH[1ڤCd #..&4;\DD qN^`% ⋮^tg1@,pWWKy]6hƕneXᓇ?&26MY@D4v|]9z@&єLl ei3mkz06=wpvӥ˭iN>lJ^~kwV#Uzmc[9KDv/Y:crIkI'7.';Z"F9hm(ogAqܱgaWf f_>ܢSRI]qStkQ 43{o=oSH7Q~'9k=}mt0ᓇznHmGnkx1'"b.z"U͢&ͧ*qa}ٛkuj"}&8(/wH.?fg[ /cbVpȧ7t&'uv=z_^ֲ/XX=s`{o'?.+mb#vp|~$5v nkXKHq'58)J9@:Ve{W#.'w tr'$| B1qzs<Э_}M-17Mr:^59휵mڼ+ns|0ȌU]يkHs+V9%bM=dI'~u,{Y:]@enXrsHC <dqwjZ h?aUUg^@ ܍Vn.og(xb$fz\\}6~R~ sY7/|~!Fw`ϗ n$z֭Ym\LЧ i~v-*C#?;x 2hkvkѧJ}ߵn ]'tQ W 1#> 9V6Lj' X"mAWA;2{c䎽aY)-o*!ee-32<l|&|;NSsDFmm,n v` |mpw"Dv`:VQ.r)28WX2n /Úqv`F[S&|) >sX..wn]΋ 9ŷ?7S}u zs<$" bώme͟|%*f"O" `ˤnR8Ic]:uMcX'eJK$2un.LB"ћv^h]\壇j.4SrLeaT.戨.WPThUT ^wX~mNc;X+NKm&9zKY9\>Y'ԠNMK](MnF.-E gUSpw'%qŧ.ɡG""$Wg3v;HF.u~E$bfB*Qg&Xu` +sw168TN8N=Y4ieTp|u`75EG }qY IDAT*y"tw1r;g){k?'VF6>+/^K`dWTH# />|SJ?ޥzon}+DD|ɭbR4y/`k"t}'麌s%?kcFl͸+ܑDJT#b: R5]i j"Hi.'&)zwQ]3.,THڸv*I"Ja:\p2JoTrJ-ψ"b2D|յ.:[ԧ㈾#n#˿%JZ5.%j"cl$HKɾ0vouN<|+o3>t]\ga`7cR\>xZC$PvN5Mnl.`F/׭PKe3+ZtwkZK$J}L32CUqͧrՏ(3%6RT7#lv'U7o n8H*Jks~=z)v_Z\QqL@4WVI oP)ki/{Qv)֜"M -ZT͘ʊ$74qUI8s3%jL/ TPͭ3&Lnڑ%"HH,;D`5ڪ๬Z"-aL{N8LJ='/%$EDjgad(oDghKG# /q4Z^7XM.R'kW*ҫNv ۰|fB[f&,.`H L+oS#G]&Beh29 :F]xi !xbu7UE#Fۿ-µKrWBAxIHO\vZ1 ?d&+iҵBϑoN /L 9TCk1`)&Mڒ)k9 >c 3y"xxUznNj`3*tܭ:#Ft^;V6aȬ3Wrf,@{9mk_Gn$4s܆REI:uu,SgNO 6~x" {[>y \|FtZJWkDԄHy:p;=b^9l iOh/tUӪGA-٢Ĥ­O=,Rフ9}tuM[ryp7KˬFrK YVdž*YMan2%!͊lZ{k/:֕b֯ pRt"n<5p Xnx=mhvO'0sSJ ڄ/τ]8U(9|A{4.* v>gK>tcX=Ǚqg*N^8wPܫM嚱hVWx-}?k0 Cw{ѵi, `YVr*u؎tۧ@ÊG?͍^QjmYʜq> 7a Q@A q2 \ gM5?uU~9DW.`rvAA˗{.1CubVG#EŅuk;(CUP67od)F{].ܩR!yQVwʒaD# e%2!ÈLGy{n"b!ez|PÙb ƒƥOkxMt؉Ie&NOjxVf=HCXgX*8EgA8ˑ} /V#W%? 8#Q鑳 Qqt()ԇ{VǧҷqbGW]yE$mQMؾ"hKAOx S[c)H@OoB"w5w1Т!x~S] zLP6$CB& }>aW݅Gi&.ɺ^ : zsp$а;WOhkv)8M=4"d'Ш_ǧ#"w#]hTwf784uτ^ =sc9{v}^"Ozs<Э_U]Sq"㱆6+l=rҜ5佲Uٯ**_^?S4ZV)jeS:u⏟*1tU m=-;2g~Fج\ 'rڅڽom{9#\HxN x~7IDDwD>2۾NRm@c3EʸyKMDYQDz{,đåkol_R?2^Tbn[vRzDDtwๅ"261`fSO]165b >5K'oo5iڇo`(f""T#yx*sXԊ2NSo53y7iN_pz y')4xB LnwH&VBf9ȶ }c>cm޶קv7V3|U־mOhQ?\WB>yx:?l2nyWA񂩊ܰhd.;y*9PFY{d_cHd{’;¥PBD.ڱٶ9x+{.qVUTJrrڎ>3}e-oNZ᷇|@Ku2#;9RX,,[O OYeȚI`זݳ4!性+I)o"?ss@ Z-cyC#"IoH[!4;5шa?O-:[ɘ|6r!&eNfEM~ͭ|\3\9/ۢ]-ZUE1羼Ynu~,Ѥq'66dU}ښuv-A!'G6C_WDKuW15?c-|ޅ.6㱆6+l=rҜ5佲xP`y{u9{AٿJ", ǭ:2s+Cp^~!q D}]ʲB9XR]wKQrbbWq]ٱ$W3Р('FlhP}gż<ݔ.m}hb WplR]xW 9;a݅12(WWD>2۾NRm@cGOƿkvhAm 2'(>V".}*ͫ&{M^ioVjH,ʴíLŔPmuG᭴#ٵEyrE֭]2T•]Is@F$c*"7.Vdp$261`fSOBcljpwN>wq^/I;)/7G4ֿ*% _]Q_^“Rf"!"ҊBZ`.k ٖ+Jx :\ё&ߺd?^Vq}˿. 23.xYSbd[UT]Z3bjfh c?4fqVbM0D<Sr-|SSe*dihÛV.Ue?M=wNW9 f߶{^XMD\SM ".*ry[~iqoæ 2&3_-ؽF_RP¹XiJKz]}ڍf"fy"Mٍ"~{, '0 "BU ,24iUE+&p\UI<_UAKI91g7YztmWU3aKoܞWz{iT'_ٕQn}itőI7^@$bZM_ y8NժT*R99mvϾ`@^]"/]]>O1xg6j'dعGwr&IRX,X5ݣ~3G/5mg\62bhӰw>mXzϗ7w5OwN]<24ՃV7wP. 2"L"}1޼Ȥ2cÝX $28&hovM { x>]߃0^X0=_3vۈVЮǤӇlMA7f~Z]G&u~>e䱯Q:;8sG.qa^(M 8l^!O_cmzN [0|°ia>]/\p6# aF yny {<wBZ?2{bp=[!yZvN"Z9o_OD~:V}nZ |xɟ`S1R^{K }f==ngܸi`}6'p/6 k ^oU̫ "&[З]R}4ss2F̮,֣qUڔ"c7g&c ]]=g2sJ'+Wn[m-]O?nq\<90ǻm\bn>c&iMQXͣ7cÈLج2sK'n57&܅Ǿj87Xo^'hP."2Q!V۽cL 1_YtޕS ym7A ޝ9JXտGqĚv[:qy*8#a>xsTȾqh:Xή6F!^/r.$7"?VĦNVܤꦝ-_I/92k!ODĈݼ{-d'tbBK]o%/;Iz3ik Q^r٥<-V.EsD$ g.""\U[ôqv5єgtvCb!zȉ^WK]9FS.9_y?` D|Ã降16~,Gra!Cý "mm*>Uc^h[8hڽG} gS9=⛖sL#IzqkD27Mug'GN JFICBzT\<Ŏ}v=ӉғS-iSGe1'HCg8 p1 "]__*ڳmC1qY;hW{--sW|mОZŢ6N3m#,l$\x5m"tpG,+rm+,(n|q\Cr&HnqK IDATn}?뮼r1胑}DduQw|I]c#F<&xdHy5ujRҖ^]+7M +-9*x儡#874UfZx2/8\(n~yPFDžxz]ΛW^)CcteW? ۞wU]GȪ'YA\Y3>#3j:F䇡yĹwr;aQ 5R""cw9Q8rDLD-9,tur] à.qT+m~ڴj_ue=:6rLg!>!LM.#VYFو2o:yMb^*n~/rz-ǹwj;KmIzn_-P}%fo(nM F( "ULS+gy~aܾ t8ڶ]q/~aﱞ֬i}"4G{mXMU m$liI|T7Uag\!1GUUF8Sk#lL"Hd*2ҕ)8c{PVutGq>2AY--8UvQeu.ܫ s1+[\7+q݀ ^fѡM|(}S=.FFC,%̅]Pz%B|Lyj V?>(K:`kS\,L9N2mJiXqREb[)SUVXyR/ξ3(3rqpؿxƍ%J[{7o0ފ#H?uM\ۊ<-Eg7ZNdzvޮKXM^UG%[xqҎ6|~Ùvvqͽ$G}| C^8_PJD}ŵN|;ʚ7obgwK3)}uOq<-OQUSTbk+N_(wlMs!`ʚ{0dQQS#S*WWV5ce#S'jݦ)rW'g9\|m`n^ zԕuIFwh^Lʸ27Eait8Ds_#o=e21bz瑶<lV-WCeyzNj1FN-*3娮C^ײk |ʯZ;MIEulV>!Ԅɥ$K_fUOTUW#^-7cՌH+c MN\fyʶmQeUf>&I4&!&5`ְYUN.cĪ˔cg.biSdEnb_@)ʵm'2@[~3ll׺>o}̫7tJD˛WlX:sKQnLX}=D:+❳] #*I_}$,ۈU>~|`ߧHTQg?d8hgmh9tO 1e fa"ya{1 Jr(d@vy}hCX;/r&uΩ=r.]giH!!f7*]ǴEM۟sYmnOR1HOV4tAÎy0ԓio,L+\2 oG#cϻ|-__40o'KAQ%MFmz˶W%BZd}}~ҥwRZVEf9tw∈\|;kLr"=~:|DܿרN}BD yj=q&hxmI),&""u~.6O9F<\Og+Yw{HJM/'VtfC /rhb}#?Hu)ԦM'74F!&R\M='=zGkRqK5#9Os̮Gl( NĜǟqme2D"D06q+Z%_VBo'<՟Ro(v%|AOcTXsw!J|i9մ1Uf:ԙ:k9a 猒j+2|eKxu?&PU E[#oiƾPOf S⯷q Vc TV=&c*߀Y>bl칍9. qJ3~]H2{{ٓG?hpVA -dhx Ÿ^$++ R'+ٟ_%S´ 3nZ?`uK/( 'ǎ=Oڼ5T&n t7w1 ;NL߳9Z #921OMs 1G D%@`޽sws 1G%!_"/a#WDn1GoFKA2~~{ J9W҅<9<ݝ:s3=ݱK>ʄZ.}v_tӃY֮gt2d%7/[bxҽd{ˊʈH_俍/mش[u2k\;1&4꺚҂ܫ._̩9Iאqlش!hD` lMNj%2S+gsGw-b"W^ eWs.Gdd־ߐn>2BwHW3"w~ܭ$T_UrԹR Vەݶh63#]6F3˾ u8^6|sL"`C8Jݤѳ_H[Wu!l@[akfb(851>f7.UqmEDJ "EA\p lj)ʫvDE%%6"s/۳_cH[\RL] 9:ʦRteeB;GՔ/Pb;wg>Rs|%ik['B^~Ƿ}K9.}9ޚזMFp?!x7͛K.),Ep_!xX]ivFiKW*a Rh%s@+H-]!\&#xt%<*e2bh왘@DtgշxbI^}|+/(`yxɪJ V19@bh%s@+Z J=(# IDAT V19ocf?2S۰&g:#B?RUNw%ܮ8uFڏ=AFOߚ!yƥkg'_'0d򴑃9SIY5fx<0!М_5D"peΑPwnDD$rmYLQ%jε/KyVsbsK8u6o8po=~&7qlڔ#"S[ G*EEB-2✜D$qvIQSQd 1d@1ɈT:HyΟpqԡ/ɵޛ|v٥es\}=eDSCzVKjliodFIFh"W(8e\X}yR gj+/*֑1gaW .o./]}IZ"F9Gjy-UU7$2Ig;hKC{ ^b%"bѭ~E.}¼xĕ+ 5™vSC\.݆u?p=&=X-WSsJE{|F.*8ؐ# '|Qy3Чq~CN|^yeq|t3p4[LuM^~.,XIglbbҭ?ЬBe= 7s=__o훱tc iDDDuPWS٪%m|xù',zz+MlNu7uqyA@%SI{lMxٽ ֽ#]+1th<\ta1*saeQ7Z/Rd7B8~C5vݞ>_ &c]rdV+2>rh{ cٸ?YzvId=8 }[vMcb~XytPxa ^*^ĕfKlxG岭;&34sCO!i31dd۶3@1i t:ZRfdm0Ww(;-;t5ߌo.$H$y=)@x,SW2xa69@bh%郕v=uǖ-] @ H]w6{ 1v=uK"'>\䅸6~ܴزwl{A]o21=dJ V19@bh%s@+1b#V <$WEG9ւ{c 1Gkdcq?t+j?o^7+ܴrC7uf]צ5sL|XDlLѭqD⎯pKxD 1V{3Htk[CgZo"痿1Xwsq7}S UO\!12{d5f 7ɻڥəȾϼ,U<5;/W1q~8úZͥ ,,4PktAnxC0SܴLx?w]'n{qĻO^%)pnc@@)v_0`hК"b=8iL[[]K={$e }7m,wt""JpwMM"?d%(hAI%"Ac. ego!vU[slg2}@""]՟ng`'}3 r} &!/xo,ӣEe)m ڠtKp 1#3:+=1DĖ6VP[&bUyy ʺ#LYSHuXpo9FKj+k4Z V#2{̶]1 yIFMnYXeG.kxXTKDZ-s/嫈(/X ۿ8ޟ" 8{hʬb$dzѶ]lj3F{Չfnϧ,YxJXe+r/O$d4|ۮ!t^M^կ)um_l̮&˒6D*}'OȻY+6F bv9*S8uB.ރO?˘M0UtO1rMe~[o訶R3p)dǾH)x pAөjJ5#kV W. D$ڞt?͚5 Z9?;P"OB|O0A8~{=o$&:!ckVi劵n>O(EY'$c7#dXI67N|y_L{OmX:q!봵z AF^c~[xTtĉ=_nȾ+_ 8mմ3ĝ:g͢/;}z缾n"2x\7myk%$D6҄8˶:}'pDՄ-z2ܱ^jƑK??v"uƖ؄ӜȆ}` Iz/;4@i x~kl\~F>Nn]_OGA='#cB~j?۹gDxo~ȩȘGw||gsHo_ϣ[7#hb{Y͚,:!!!!!!zpfo=v::6O1&GAeV8~.:^efظ_,YsxTMaO_{tOd#,~}l<`M_9qrw/bG g<:qCˌ#}򪏟.Iu]4 b_v&h܇8)P8nʭU./˗+IEY׽317~I&+o:oiƬ!֜PtzÊ BE˗% +{>\jo7||OaȡO1}xyTXBsb>3wl(_Zqdͻr,;Y1""]w*|p/;prkrw_;}P@Ddc@@)v_0`hК"b=8iL[[]K={$e }7|뒜wt""JpwN(H8Z3J/?|2j詜3kدH~"1 !Ȇ \6(0ӡ] ݉q]/ sk@6u(U$J4] Θ/'>Hwm͵uk@]yU2M^Ѽdm߷ni?>R%R~#l߾X8o6YLG>ev""Ac. ego^όH~cܸ &y^X,&^lmmxv}*FY{_JJE1A /XU^h5tZ- 'ƞSo46nh@i:";Dd:HK2jfK]ɰZQ, OPItLxSSSkK+qO9:rnp<}6ԮHS_&  erLM8sKooLa!i@-4]ZX[L_ˈTyoemS5V jDB& ?6ʵ?~ؙ{?7䍩а u99@bͳՕLn$'̭%/Jnrwx_ەFDNnN"Jqs1XMUëZH,9&0Y~hEhqAӪB\$ٖ+LQq3[LDDOa4|_/{ q G*ةcf5s'ۙ}:4eΫ#Y -,9yNZUxd|AF<;.*_gKuM5rjXuOM=s?eeV1vh[#ֿDy3s1t7xjS ,]<%PF2|O|q phn2PmWB͐W_xv/f$h.>,]<ծ:_eB߷^964Ȥ0U[i7~|E]kd\/~mDͿ58Wcq" 3þXkи~Mk3negv5[l٢}$hٲ%/ .e׺ U8ϵNoȇl<+=c i:lL;T:.S:`X} ccL :NVTY۷}!t -%o63 "RAD آ5ذXb7D#(6@Dei!XP ҄ewI 4|~sμg.s9HqIFu'?n׮vemc1jxqщ{ԔH$U?%9jml[4۴Ľ8 IDAT4voec3Q$>>>>>AYP4q߽trǼ]+9~{\4zc5pW[*Du+KGw{+7{6DYOGy/:踏y][1fe'[jԲپ]þV,Tb4[5S_9sO`JcV.b2:]gyo-+qb^׆3:o_c;%. Tѩ> Dg^>#>j3bO9tuM+ ţZ<{B߬ŀ?jceRO͈n? } bI׬|nuke,ʽu"lF-Q=w+ϸfҺ={GRر殝];mյpറ^pxڗUqjĘeD;zqietGN^rIvu*5Fjv]--ltzD6j<˚*  ]meeڽ8^Q6:w~SMD , 9 VDFቍq {[u⮍|hf^նKځqtoLC:hRv au;ߐy;{ĆFuTdA$7/a✈3UL`]Srb.6_p ۵;'I~*o mߖ[=QW1R~#2ke+II#S(faVJi54gI*P}3 ɵHq uogK/*@2%<۴NmS kdڠq=˩+ZUXص2en\gjR_Kjkg.b$ VzY~'Zlʕ~!X4CKc'2 7L|vµ" ^XN ؼ [~\ϋ%qM\ѳ!'\W-Zbr.9۹n;y3=# ٶλ.Pg&&f[;4pP "YeΦ]p֥ F=8 >z>Y1Bg֧LAGI4r$OH-^~qƣǶ ='g;>wk_R%]i+nq{xh$A5;a\Wce11ʗq~X:8c_#!ʹ_{6FSUm~L_qHIVZ-ƮVر N #0"x`hhhTrY^3^ݱ94$-֮srBcAᕄ{V-2\ xhzauY>35DDE3ו RSRqb.{~\'*~= $lvueM-OF1Rz:>5CM$D!)F _B$S: aoC6IN<irj_˺MYpÂG>t:뢧Jg; ޻FK1G5Sr}~1lhdW="R_?sؘkwj7Y?}$".0neO=p«0+[^kD#$5c *Y9CDĉ,èU/ /maec#3]uSQUA흭qITDDEInEDħͰ쪟x/ѕmY!pn e𮐇'W-5_2s^4.͗2iĴDD@Tk4wm-*~z+xۖ nW-%CGUzX]}au{ &" kGo=}K4d|OMQZ+? aB~gں{Wē|,N'pAyZT*F7~e5@v?v S;\o6*ʖ=V{@up.*(MMMDqV0oZ3wzjq/,ZA>vAer>UX*NKW*|$B&jgwk?PC`o7ofǮj(vD=<&5٥vFM, 23|Jd7Dḻ ie!C Y"Fφub,w\~{0km|\!1gyyԥۋ2jjҦ*s7 qV=9NĚ908bb|&7爳qQ( EV˽\/n&Q^',`UzӲ '|,JMJoEx-`]tz71GuSldtVKq&*B#ݝhBwSyK}b_W; "b42?LZy^ b9%VyPrUlh_iȗ*.h= be]}EdNΆ +j#S?4V <-]cdӽKe_(ey}GX( "b]LD sKUA ѹmZiv_l9{iДa/^!)۰O]l>_sT7g*+V=m[S[(}\%ʬW#gTy$LdH>fGn6<8&E(8+iZ ߊBooZ/S顭=DC/UtGwC~|v93Մ8|X _-5\V1Zc^_@D<ϫjRYTT4+zKFE㶣2)H8c @ jPC 19@5b!s|@ ]@u/<&#Z8114rL " c>Ys|@V͛yzmmש#dI44uժy}.ч>'ΪykW}*> 5b!s@ jPC 1}Z4ʹIENDB`litestar-2.16.0/docs/tutorials/todo-app/images/swagger-get-example-request.png000066400000000000000000002006411500564371300274450ustar00rootroot00000000000000PNG  IHDR rsBITOtEXtSoftwaregnome-screenshot> IDATxw|TUހ߹S )B"EQ"EAiWײ֮ {C H%!=SyME!B2ax?dνge9Wi@G? *x4w޹tZWPj9:N@MRJ'GCjJePH#ȉiY "aiT%P9?^zS?:;>,j*EQ?nwJ+hc65(w-D"xn{Kyb}@/fK֥f,AQut%⹏_;;3+'\υ}"N2Q}i+=vArwŹ[޻ɢ>v}z煅WUjQQ6NE{=|+#t,#i|wG[z-"z%ã7EiX-Aцhiv-o/tO}u7?۹IL.ޟvf3[[&~3Zg7?{;p6[5PӸz 8TֈȀ>=/Ԣkv u 1JDR xS+vZCCDDy8e7B*'Dbo Ͽ墺'ty3c{[W A Z6EDeXm6]tYr#Y7\׳CDtyzwx .4fL+Ķ=cj Q`<>yneqiq}>[K_pFii .ϿjeAjQ$6ލ!M K  '}AʠC?]_McIy9A't_6%U탏I7}}WƝ^kKO=nkw[Ӄs.^f\'C ~|ҭm+>x5=kѻ/}DDy/?9;_H[c/?m}ūoK鲉N'EX܅{RsC,a߻}ۓE7*goVDϞ߿Ajѽ`yp†DD@+_]sŸ7|֞56nZVS-emDk6 (g|43wưڍҬB7k֨!"mN=?--u@hHv%ei-2뗂M(0e}y.pp7ew.%D; S""֖W=]_<].6zK*4+Yc{D0ذm%ޒ|ot胏И@%"bKTl˨ؘ?'7ݗ=O^ޢ%y wRe w-+q -zLm r; ;^kԦY i3wξ_"n VDl܀aD &t!?kGW7/jYRH} d h:ZDp6%""xJxo5߭YoݔgiJ7njwc&TRVX?_XBĢUWoPk?uЅzVV]yAxA r>螯{hAzGu[/{ū~~yG&nysD$ˍL si朰y""bD_x׸w}E3^w><ɡ, zS?h?N|kBVl!khS .􃻆Mp&{󵗵*#ȿG_k/?c'iJ4Mr]9M;T/r0+<%4RŰ:-ͪDR_b = d (j @5r^0Q@GhMTO)Up h%5ŘnARfr$6*NZX~Xc팵kO?SRjx s@8YiZ/Z󺙣#n㓏E{nbؕIZi_'J?f3G]#JיY@2M*?+4kdq ?4  >cn^b>9T[N- F?',zM=|ذ1to71լJjOjc)[6z-v_9 JYv.+ݵv{7xꥻ?z:,^Y78lfLJ*f䩴bվ9 >>* ts;7ww=<3͓5w۷\?~I.(*5m!Q݆8xwk6[w̞`CVgjWfnAqkrv߫ޮEa5غz_-,ƴ|ۿF~l)ǟ3hDD{翬ܑUuF'%y]b?7n.bmvO7OiY%\IKܺEL| h=1}0sCFX o^RG^sxv#t UőG^ovN[RmbS"ew=*PʼuCiys]*%W“7|o94)1%8Jd$Jdz*X}N/}yT GB2omجMRmڼiWT/:V]k&ڴtxR7_7-N<<³vK+Mqy}KV$uٻEzonYǵlܶwݺ%oG{蜰-nK?P!&uR]yҦ?xׇUP|-@ܥ+ 3Gت>D%J26Tk1%.ZJ-=;^Ti. ϊj*мo |S@t/gh5(kL [ZC s J];yKo|g۟zrPtŢ:"V^[X*]}oO$sdb#jLEW_?U3VבH|}'DD"!2xf=Ɓ""bq".ZbMG]\ j}U3/M'x~6.jyW/{Z!"!aa{VwlA7=X۾ߘ$WL<*0ЩDj*DDGX;]ܣQ;Z5iʞepG-d5=j7GX]ι~ 2zᚼWƏn<^]Qsd>iSg-mWbp1eZ,ʮJDĨwOƺ{ 9MCN(e{X8_])Ze7sjQCvay`FdRhc^1DD\Ym Ht+bK\&AƮB+;+e~[]HQGm>ϕƳ.}ۺH7-e65*ϫfe_gvn,ʝPL1E!tȪN$nXbu Q$]tb[2r^Nl KW:2ED~/秏ZT@%&XWx}ʇIEKf-):M" Y֞?v [/Y u=k=v(pyFjGi 7ụ;fĵ?>tRd[{<ӳ]FZq*gǪb,Q?lxc*٣>eE)Kyo-k7dnP$ =:%ɪx=R/JHcWܭ*=M=Ѽ )vu֤Zt3Kya(k3Vx5"67>@=ȖjW'^/tf̽#tq65})SשyT_|E$w8&4^*++{^CjfdA*"?pN o:lԳ$ n[,8~W( Q@GSA)QJn_s~V5RJmڈ'Am|hmij6Xkb%0 Z^-^dYXj+DkQO_Q2IQJ%&émܭ%Q@2 f7.F׫Rfl6fP@8Y_RV4M _/S}@8TFpQ)8>ƻS9Q@wa PTuu\E(؛Sz/)2U}a RJi3Ma04*QX?DNUM,Zצ0? ( Q@G? ( Q@G? ( Q@G? ( Q@G9Y oUu [>p*Ykj-7%a೯_.mi{Cz?6vKmt1RݠU>C]$J5~%*>o`f~u>p?MDT`Lf P52N- ioƜ^QzB (ڳg/.x~#6M_OuK-<2~)D)C| nju/Lf}fNa׼FJt;[\ڍ>fW̄nW2o376D9pҜU)9e&]8_qՈG`S,˘sNqg.샯MĶ<ѣ.Jt*=iB50N ~7\*f_Sd;ҽAصa֫\\ݫa.ycgj1sї^]3 }_~)eI?"]a#k^ڲgV([B~/{߅%)=1yQ! IDAT؎PҗM{+ч2NE|\KP| ƈ3Oӿ殠lu}MuʧUcWجweO<ygG(eվYs*TLoi\9~{Di۫mQ[Eޒ?$FYtח/(}%b IhصCKf,7- >5M"x-RTyڈ |qL`%gzo) VOMf}MD<^?t1> gծ[{mdؕH|0gWSfsdFywb%nrKBbA!AJ 71D"W䔘bk8 7+s1ey0UIޑ_z8#t R]F?:Wiv)*QFC-V,۴ AΨ%"Q%eZh][b[CDq qq%AC  P"77 Q=^2m+}'f3[cgOL^9jɄjQ"JDDYuhHQtg|k4<̒#a#J%cnv`q7{w·KW|sE" ,ߵ-ݹMDLijA_ew0דiLQaJ<SyrFFRgK9={3uB]Bgr: uB}p3rR]bٶ -?Mߋϝ{º5 HY&&U諕g7vjyVffA{> mTc+gZO. l]x޿Ӝf-DTPH%k._o^ոwwiT0Nc6ۗm UaUlZӪG{a~qyk1n};,jHWO发=Gݸϖ-|BJ[(-Fb7hh]5KHY}gǘ%f 6n[XBOp9PH+X|8ѷ_\WurEom]8}kHE]G#&jT[XBW0cw|:Jcaz].Wyy #_̿Wfp?m٬Pk_t:vb1KxPD4U N5QP ? ( Q@G? ֚؎;krj$&/W@%VG? ( Q@G?_?a?>Y}=./))+eN 6j2}UƵ9h3N\Fg_8]bm""-¶wY_S< ]a'M~ѺgT5ui4tgMvY{ӣAPDxo l]wM'+>m,wy2qx~y򀂅~=~Se/>΋o}JĵG7cm,ITO]^+)ȉ @Ԅʿً rB@b_r|ZEe1c|ڻ/ 3DurCu~:kp뎛$YE$gʢu'~-ӏ_VHFr/X>F?VVZag]#R}c[⹝ټiѼkCXlޔp^lԉxr Mͫ6"е,q;&nN;Y1KKFxdؑ9FX?钢Bd#4,TInq:""fy2 k.3뇯vcW\o j!A]PZ]VRr@taA)Q#YXP%(8X8k噃FKOu g]$kC5Y&<4:Ί5.Y#E S%elٮ-coGplON0^4&2-̓ 6~7ۏ KҀ]zu/F\4p?Y{樋nbS=;#9֮V=Vo z5DDv{&s7MpF'{aI6t3ynGFC5qgؓ?R/?>Jt@xT\s75D4Mr]9bVTm͚5(Ԩm۶4o|j/tu:vbw}@|rz1}@8P@N/yo@Q@"ɢ1M;wk}J)em7펯FFqc?b_Կf'ZJ# >Z._w`h2 n}[umtu ߣ/^XbGmZQ^qFhdN憎o./+uՁ]KDEDxniTlp]c<7Qv^, Z)Y?F[PY -47cb VO~iȳ<=zq{n߶$ks~T~\_f;fB?ĵO^~~N_9QM؟nnӬu=^{Qm#3L1~[c]~m]؂WJ~iu\.1K.Yۚ5kQQ۶mi@Ռ_t:vb1`- S^TRq[^TR |X@+cWoOZE0sĹ{>6a>+ t˼y<;uc-SL\,C̛M=KxR)"Y){, ;Йy𳺴7bkql 1DemuÎ%NNϊs(3kBl޺Ԭc쥫4i1)P=(ub"tANK✾@MMXlsY];*0 }ŬVR+3{_WNmOƺ_VoIڡcwmYÎ{'9M+W,˳b  3Jr TܺTš'=n#A]nّ'6zv1- Rje=.#qBS:Јp8g^UMؾR!L^0x퇧cp ۖ;w]8$XtIVNOAΥz,qQV!fav8vKK+t:Y* ~4T`xM=z7=Gtَ[ CZvjSH@`P~~GzDقr=`i*e^!F|úVQΨ Z˲ )%F2z[ܭa@-""b؜!Q!~'wBQϯǵD="kYN[kVUROsa=?8Ԫ{Z"w|JΤ㒻ŝSӟvBa( Q@G? ( Q@G? ( Q@G? ( Q@G? ( Q@G? ( Q@G? ( Q@G? i}=pRRբޙUͪTf|= Pvz~l\| ezG9ZL׃H)s矼/3V-s@LfuQ-`@`v$:xf--}ar)s7/a%$G@-lYm'P Gjk BUy7Dt-?Q'aIwS.I1$&\>]%^YEy&\u.B5Rqx(xpruKb8Fw dߺ;敗j۳; Ŭ37cwGXӦ`mW];9el}[M!(+/*s-}ה[o'Hq#jdFpE//qZDT69n-G7f!"wn*Z]*""^oJNOl 7 eQOD$$8 ZtnXܑ SwQ5%#o~p9ݿIQ6p( NپI~B_9qN%eG(+\632ͿqHC(r*Z}eL&pJY%Գ{1M$Քfݾ! ]tg CiBKi&?g5/#˨ ɥwicZ`S"pUl"٥3U=0رf/~>^nxv8|YgDOݔ ޑeMkw_; Twӛg*NZ}r C!.1-ʤڳsVtD4\L/}yq鞚Cڹ!L8:kݴ=&S3<&6Gr?Y}2iɇ/a6+>k;!^{ϡ03dvu  3+ʶ!=> +sGzs]mK=^"{wvǘ..&hh:c6šYnMER/ؠ(d2 g~YUUMF?\EUwB'GO'56k~tNh4| j@#֏X?`H@#:J@8əkt$ oAl6oZBUՀ`q= ), ם|W IDAT0=4>=pI"2,_7I $I,Խ8)WD઻-\u0wkY0Q &xM(6uq>*T= pH5666ZzI@$Ih4$iZEQ VX%IO~ @!u+p]euu `H@#֏X?`H@#֏X?`H@#֏X?`H@#֏X?`H@#֏X?`H@#֏X?`@Uk1kq *z5 PUKc/i,UUU\]bi$ƭ*J hJ,] ֏X?`H@#֏X?`H@#֏X?`H@#֏X?`MڛZV!$[gwQcO 5PMyj@$Ik`gU yIW|we д5 InCޙ7\_^&ͲnB+~\dgbzQӢU-#wh{'I:e_/ߖ]bmZx|"k}8FM;]eF7~lB(eǖ`鮤bƩeHw=W+!,>}gSm6`ɩm{fHB~X)>-J8z55Bm}1jU}SSS@iu@4zG;Bys|ǾpwٗOIKj^#;ї&0gZ:ge[HG}eV!B$|-XoP~>Xץjκ,qtw?riyےK ͧwRJ]oT/.\4O Z k$砑;UB-޴pAQ؆?kw(we?^y*{ zY=d!Ew=[ע.=ϚGdK\ny+vKJJJ}f'wbaK$ B!<#&<91v)?-qK !Ry2fqVd:tBˉ{LK[OL n߳p*$[{hneЖ=L{v+Bh|B.>7EȪG#4A>Yn6>eZ$Pkrm\mgv P>8t 4_"QcoY沲Ϳm{9ďO?9Х$+--}'tuK_e 7[Ƀi!+Wd69ժ9q#Ox[23 MB5@P.6:KC=څMӞּ4xDZYny[zxqooKGsCf!i|4 \[͛ɥ>~4%\x`جV-ۙodv>ѭd!{t=eJzRvBEעU硃1Cumi'xk j|GBa_oÂ2NfkZuz{\ju҆{/?2nOm W> ɥ]2$W5-C m(IzZEX,&Ma4#|>&o=Ň+w5tC^ ~ͼUauO7(`H@#֏X?`H@#֏X?`H@#֏X?`H@#֏X& 5@#4:6yY)0[̪6'I$F+^؈D$ImmmE'I,K,I+id $k,S#!!\mBA~$ G ~$ G ~$ G ~$ G ~$ G ~$ G ~$ G _[glVq"Fh!4C#3I-{^/ރ￧}eҶvkv`I.qEQ,d2 ns=(9Z]lkk4,_£k@ ~$ G ~$ icI d46qy3#$4nXR“ d2wn6qҤ`}?h&IؘM>5}DФ  @4GD^0G H.c쌍nRs\=G;ܤ+j3?1is!3~gkKݘ1?1iQ\st-khw7&e:F~~w.3YځFw<+>~ԝyG+OfձOk@)#f+&/7WOxv_mˑZ?}l- y۳Zz$uq|wZ,ipfKR-8]܈ ;}c}hT6v5G|0!~>࿓!MVYc?pL?+ ?>ab\v}RlYҘ$ZْQ G' 鯓?ߚ(;vq?2gk&/2ڒM <ޤ!_ZVШZ' >x{IܟWxt 7_ɭߨ%7y-W!P}xw>H[.J6G)==@oO[QxV*ŧkkޝ ZrEF.ۼaKqþEK2~4&h7)̄25w >Y41ph5kN17>,_ީ}7O}y$7$v=~T}/ks øG)(JܺϸǦlou&~w)Zz"~uys]Rg : BX޶"6%yw3NXf"03;z:d(B-{ <4??[(>1G_zy]229Cj{>_]mXKشGy6汨􏗮5ڵMOБN&#ٸ >><^^pTX,d06{su<ڰG~*j?7j^ ^VGt:FS5{ 9in/w$!)1 mApcX?`nD]2MQnBf>|Kk ̳\D߁ӌ}g y}3֍Zv\bo3SgՎ}߆ra`u$9b Br;t_O;Muy[>K(6>x迶U?}OaH_!AL{o}l\M!齧 4=Ͷ&uKL i{>#[{Uw橣 7*$7/U_[aϦKc3[q"O-HaOb{f"x{{J֮D?~ɲo~k]_p 7| H;nO:\rڹ)U.⢳/;ykSjDτ!񒄥x ɩVTKaU⭑u).߼%1j#{ sqZbsࡓ.l:%K 6 %e/سm̠QR}I-{g@Ҕur> _$ʎ,ʬBxm߿P׭7\b7)i9vRO R4U),_ABx{W_a:fޣz+X#C=>T9:u{ΟE$Jr l>zh;NB!霂mLeNJt'yvgJ|"pvbq8uҭF!Z͂C:9ÒT"Vݗ if#ؚ'ӄVT7DH"7fTm.bSzdj޷VJ~@PbP*>^4lvFfT!oZ/JS7/hvh Ai\OV}vwNH-h}&9 !Ԝk0 9־6,qi衮ʑĊу m%U%yoۑdеfTɤ647,0R[nDGv3Wݽ"TJUں̚B^k6V%d6ĩڴJk+!.v kpĭrkǫ6 B{fn.$$T;q!cѯvvH9w|+S7eB?sD9GE'b;2O1A]& Y] ? S\}?3_X~ٲX뿹!%ðoђ& ZM 3ap Z?GWliq=uj֡c 53'8z?;31;We@UkNB,%|#kr/y9 缻JکV&_5~>KڻΖ۷o/WQV#К?B5 JZPf:LF{ U+%&ĺ>Gސ~G}imB->'n1 Ioc+I܂2_E1g/X{e[~ "ƫ[kKIu._Bn94U.}cեGBºl+J‘EG&s,Uﮯݱm)r.rR^_ŽqYH-P*!VU_dn| enY4Kla촙nL}K p HTcAI!I_D-B*V$^"9g9?Wr5"ؘ|93`]ߗi> n7~eW=;aM둡ntߊjBhL'׌ i/TsMQj6Ք[C_Kqa+}{7v~Y[}6oϮ/,|xۏ(-S5TUit;uye

ysƤT>jfUwDޭV$JS'Y.6oeE5ڱ缺g*¯ g*D?$!PsRHI6U{aKRhi7>jG7M@2Uoebu^yEֺ2BԔ3 IDATN"PxNB[=MZQTh<:y: ýwYC!yt䪿JEwh( wf}Njvpߠ!, ;~ˮmmݕy%ʭw(jE?`nӷψxʼnO-Co3;*Sp%~銅r]]B ~xeou@EyJQO{s6'Kڅ=|hɧsyK ipq5KX,YIeEM0'^bL&6ï|9!}kE۪]jοkW}o琂zL&(>y˖Vfz$7`)X|& {䗯OS:3a4.lŲkʋ ^fVߏ.tF/ր\}sMm]Cr)ssZ⏿Ac'mޒJq椅|jh(׷1\> BlCjZyMphľ=b5=pܓq5p$+OnJÂo{n:>?v/ V!4j.r' oy GfNnQ߹yB+:[׿^xO@pѻ5ƚ ՜das*5FS7V|uGϿUS=mpBr3:|t!eo8T}v(avV\OӥO:|^+tm|hwy`JsJ f܅zr;Y\]S<._zU nz{MIzC?}[zׯLg5/eXt+Nh5stSw]jc 9gtЍQ,{Ku~O֗[}w{_]r~YYx&0tܭfS  д΋9.\?H@$AуoײRS}YeS'#CDiq/V3 !yxoxO7[sU=8ZHN6A矘eˢ:"Ć-#ٶ:h7Q[X5KJ+ p F]"e0On\ ]Ͻwp.yrQZ̻>3Arhm[;=gCj3~>M+ !F=gǪ--I7z9kaeGy(!de_ûk"gT$oNT*jo]{Ŀ'M[׋ށ w,߲6% C[_uG^!q_%zg[C/GOKlTj,G_~^ٹ0,"5G{˵Ȟ5evݺAajVğB_J<lww,ɩ\Q_JL)oIp#yx屁{%"lu}=׃P5 sgB;?yuo>u\j(,twLPT>%%+UB% ) }G~ܨ !K"lP?RlQk;B-K$9eĈnrZքœ]ԦLe(^u_km:QB,ዝi:zw~FeYzssyrsA!I*qzKY_֪ϝδ>Kd6uW] ړT&PM:1͡U.ЩsO?hdI}mz㇫U!}6K1ǙsC% B!<\'P+ӝԯ(Z MJ0%F¢jsSی*e#Uz~ث$jjT=^:P !ɒPTZ\uRX\cQ-߭ؽ2jyQuBH.-:fxbw'`HKN:wF:ncO=vmAs{_u'BA mp nujA_ǵPжE-KP vmsܘKU(N}v~_Bs[|'rv{jS~/F{xPp]E"-~%\9{\k3׈Q-I; U5U}voңa} S+.NHFi͎}cLE4T91G /MzUW##d Yf2 1}.υBRK+bXL&`xvW9o_{Ѷ;bڽUxYM_WmX;ݠ3?$cYll:mխ1~:W!4wɚSk>2swKWWb󧋜~{jjb[[[Nhd{5 vN97wvG+*J\5p5d3e^xmFtH@p2ڛAI-j md\phǂF( hxOO3e IXѐ `ŴpqDX J.~&`uhߞ=Uӳ)hI;p}jIi̠ Nvό&Iа?'?97!^'ǟ k⮛z%Ԑа7;7&h$ G ~$ G ~$ GI{3'Ν\E!IwΜiaKbE'!y*yt1Wo%G_\.w}ׇ9yOj$kl[nk{K\OԀ.A]E-8c{_l+QcWdתCT>6W-="p9Eg^hG&32juڦy?-ޚU84dڽSR[uD{RI5}ۓ?4Q9/,-j=Q*WIW#͜j١@zQwh1ӌ_<0YIw|,{W"{yթj}r߿וeDõytJ4: j..ϯ,R?^zī Zݷ=hJ?=.>үto+oJ`Woz87 _3&|ĝwFW2",u1cvtgMBҴ 𳽢,5 ߟvBHηz$EJU{JU]'r2.qÊS[n=TjCW&H_'3iF2K ¡ݏLngҔU&'Zx <^O;Yw]} OwϽr9,?uBַOMneM%=;iM=SrXRRX|x=}WRZS IKBKU`D)),RZÇ,/(9[}Z!$/|{BX1)EBh3a-ʫ$~JYN[lYIvw6[U}?E96&>7I$E%Eܬ|EHEY9ATM(Uժ/,?.jsl[ci3?T~l_?l1!OՐJ=c]<QvTS U!9yzϜ#GԀ_KҖ|'piD!s?%[a{xE/.E*ӏEޔNK'ä㻏[ӋHZ7NIUBMaM_kPG7KYn8Mϒ[h !989J^|4pvi1uf˜q<(l qtSzs]օ&u]wrr[/na!^MGs*ϐ;͋6 Oܝ^l|~]ekؓO)L:2zr/3k>t{ck8}3߱Ͻ0?w-'A oZhޯ[2 aqNi,sof?qQAN5iGk]CN|_S>0%XzZXRұcI2K' z3EX,&`0yHm茿o/]7Kx.Z!V_9vSKCo?ױB|/p`baϼ5ӹۃ.Yo:[ 9}xqz\]"مOt=?KrI_Y\<ٻ( &{@.%, G" "(ҤWK!Bz-3I(ꛐ]~fgyfv/ks#6;-"**|D}XTE6z=cXh3xO:dަ;(`mYqĕ'FhZk dDWdeIJuppu mb.ʊO)QW-c\jGG3Vy;:k^OYSG7˚9;鳬M&CaT9JM5YYŊvrJqFJ` "V #.KbUurvh5qy7ʌΩ*61bش~Y cllt6f,S"*$߅ԯ/95Y1냦yD'yΏ^-"$e 8Qyx"\RbǼPZw),X_}`9Su~NFiya.ϣa^<0CHN^>mq2lcŒf.*ʻmgU[Y"bzX6uKuҤߒ9"Rƅ\Jü {e=O Z귷,1HOD[!%\ҪSOȐRG$" ;4ۏYȻ,jsr v˴]Gwa!wfZ^Pzbn`'qW.fRݜ1 F6C|[I'V@J"ETBd헳9rl97btߠ%Sd.]Zgo\xY0_&)煮SPjyƍ wB|EWOH@HokhI=ge>j|+ ٵȀ9RŬO$"R8~UR[6ojб ߹bGq 2>oTg2na F5:?'H~ȠlONi>Pc_-ߗX|o]8`(zU'":b`{=8#xzOz$6tSFDyGZdްAT9Z%NՃ]_^^^ Vn''";05iݵ[KqVoX1cȜ|Wxrf7t4+r95d-O0m(-ڼ«y E-Rc Hޙ, 1kɈ${}a @<9ǩT*\.\3^ }ug]sϋ4;r;}!𧪯\ij?Zp`R] .Fsb @_IeϱF5'؇_nT x wS{OobKM©m ]ߨSWDsWϓh鐀C ?$ 4lA& ]{5ewjjIISckNCSsMi& mںy_o ! MMoҦ[w-lڴu[wM).PH@@!PH@@!PH@@!ԟͼsV4t Mm;xoRO3JӎQ$]!VVsvN暕)7s/zA }x5$~ƹ^b"kKu)αN-[{ {g`W=Fug'KM-p2]Ƭ[?E@;>Xt8b$ܵ}Afy -X%5i*ܸLtfi-mhj܏٪"W_}U>wWʽPk, r^Av3/V=E4: ݧ&LlԹQs{gg2惧~H\|Kt1v"2}M Oy˙B#9ak?J}&t||0Sp UC^e`fپ߈ifZ,EYM)ߩkc$֐e_7s3Eu<{rvZjJ"]K[w'c bHr jRiM*ks7=Ɨfm&Gwe#9ko?kU\_z+/([x_F*{qIqj[wXc3! ĽY*Ֆp1D&(-+ammt"-m-xNP1=njǝO c糽$]|,jD~uN~N휤1w++e$66f ",""F(d=>; }7ktcohbRNisFR=Si?~(';3o^>7gYR֖ee櫈52441a),'wrskyb M Y* ~ң=˶-N;q̥aA^Ͼ.xM'tWT*-!CɈZy5`D]3+6z9zwthħ綘eS߁mOl{yvƪ;un56VgdN¢r=G1DlVxf9~>nb'mNlu{ND%c3ǜO@V}mgUmLkKİzZ^kM0ZnOMW4v3,˖hr(F[ܮ]L[awщVE:cv#>].ؽTԅx׀6:/k#"{GKתkI̥w[c HIDDa3˔9 pJ{11vo|ٹ;+!æNc֟BRːq3 t]3gqJ2l5uEp]gl^9w)?ï+RπkQOgPH@@!PH@@!PH@@!PH@@!PH@@Dy뇩TUs6a3)˽q"{9UKg!#z \=~U+Gkےi_S' \I/~8.F Ug%e({uUue–s ð"{Giq8\H=Djѵvqi3 PMp~lgt]G;M/G=Ir_00DD?wq5&ۺy 0u]4W?8i*M&ul\U{|߁ss"#Aܤ|0;;Jf\xDz&Z5n[P+MpoUsĈ;N\8nj%"u: _p~7$ĈMlmԛ%,_5W|qWWpP__pdmN+nm|<عCg{GƦW[oϜO..wmOگ,.Q死6c.,{g]ܱiXb~5gl?IZu͢IUNV'b:y<ݳ2!x_g4۽6nI/c=Hxe] Yvmݸ9\=3[qN2Ws<4!.BJLUћ=y3tBͿKk715f]1q.?-4~o7bj9VBmWR {zوȰP,A&U"bM0D&<ׯJΕ&x+B};ۿڬHK/:XR@b,L\U}iڰR##NMY1zTZؕemNYrE eeKϕݏRutie\Ikbisw D @4XZ ˸st~=PV(||5л_{YSgWy+/T09%O &~9[WđRGqߥ yV*m|Pcs̘Y9İ:~>@4išyr0>J-pceCClǍpneR#"sݸv'FŞi; STqE%[${ApJ:Ln%$"R%G_N;dXNݜFBqG{dmT2D*kӱ5&='.V7䷿W<'FscmshXp+D :iY0qY8^xB)Onnj7%{碕4+c4]F˰C1KwM//V.(`Ol~OgH3skZ@q{DV[.Iz{c= -YԹG/4j3`gd$dx靃?r}B'"HY{#~s*6vڵ%RbX=Wv\ ҥK}<ϫT*Ry>Sk`hvJGUS]U]S+g,=xRmYP^TR\_T-4r7yܑ"F۹NqzZv~^̨ 鵌aFMמd")c޳^N/&T2I\d`-`Ҽc=Y"FúS7;8+͛KU&>}}[h1q)) Fc^ 11Zn.Ft6뇯gp]u~l[WsvttTTt|Ƴ;34&P(,2 38J%e2قk1~3RXQ< ?$ C ?$ C ?$ C ?$ MYrrJSv֭[5ewM4,PH@@!PH@@!PH@@!ԟI{bO>zNBzacѦɓ7|2Cv?4Pe1`;zק,صuׅ{5ޝo٤@Kִc@;=G}Ŋoclۖn'?jվ$9wZ}A7u׃ց;H@ˤ͌=qo^/笩qJ2l5u"yPnªKՏ(pvD,®Bum=M>y@EA-I5ƿ7^_vw7&;kQ?MYR4~J"hqb @lک$n:qf|0-zN)\|4#"h8t3afPW3AsW&:w@QtM$x ?,'?Y/c[k 92_qi?Tp:Mg 65cHT!iYiYB䉵KeURޱkkB[KL$S)DMJy0^3((yR^0𣲋8j [.p% QS^Vimo% "s{1$WWbodqV~>@? Z ~aEw󿹘tӾkGZ'rtlqaJDg3yya6t4,g(Ҭ"5edI9b )s/fqk=\e"OC 3l~hHZOG#!C8= Sƶ MJ2ZX-502b *.C@fP\RRƓU>j5舰C;M8a[_+ _Bㇶ ݕttpBD?BLea%]ڏϪ<ؙjצHat$"bK"Uvfzn>jQqGȮ@w4V֏q73 jattu6:~FMQvbtӜuh ^yNFԭ9{8u,.Ccolktec<=LU}{E[hteSxlMh_+|r>a9|1PϳW#VdhL ?dc5]̝gp_0ܘd.3W|4-~12OU7>UYe;so޷ψvgl%Wz8oO¶]\Tihk#hysGy&XY(ZY"Fý ~bxw|8J%e2قk|)*4m:1)n~[2h3RX!XI,xȳ"NE޺QN}  4?^}Y#6!iqXko*Snߥ6wΰ$?$ C+6;D݈6w-@CSkwISi]#i"I>^gڰkWnDyK Dv؂䵵wliN4]4 xC ?$ K>HdTTTTI2 0}չ| PWH@^̌@}a?$ C ?$ C -x}|O]kN7O\sW4.@a?$ $ZZ|h䍰ok6w1-iADFL)Gț H jSu͛2] @Y0-+R H!4ڭ/-.嚻`Zmҏ}G $i/pc~^[EYV|wB[kAsW iA GB҂29ZۙbQ޿z%-M?h\^>2'KsMihA0,PH@鈚5$23/p(#6Pu@˃ǚ<#a o 8EHwɷ_~6-0b=zM ;7ri;=|]f{m۰?y\7ۡ~V'_hĆ-7^g&眙6] tֻ'\Kݠѳ#|3{/pg1zkȹsoZ7M"Fϲ7߹4{Cj1Dv܈?cNsЭ~ޛui:4z{.UȏQ";is{[muP_{\=? y֍SYRc6IQOcL\Wtsϓ::Wlj^z^|C6Fw]e-(c⤪Riҫ":=y`.{*"":J>naf6suGr==N(lS&^!y-u_ ""%o.ʂ Zм""| yoDDEL&ؓ50>OPi╝k_:'1% TkOEXx:VDDl7$s9뿰]kmMeUR՗cD՜Ao{r 17RtDϻ+[ ?'?pSZ5#YwnS욭'5;ocv眨0]I'3`T9<'^}q&h% KcTc2mZ'{;WgvNmwg-zkK~cވOq^z՛$ߪqKx"jç:=zuq|9/) ::3ux;ۭ\-5i8mPnԩ{ AϜ8pņ'S++Lγ0O31c2[e[}wz<3<8m%_ODD6$G>sN<ѷXȉ5*eź>eQCnpQ\+Y@3[ϵuX`݉;+K|thU{6~kY)CC[G>/)|x<3쳋ŏN;_Xn=n/EZz|F' 4ul&YVQns&4NtjZ"CPH4DO,HR#Y mDם"վpg| Xb&48^R|rfgvGX}SywɍMtjtT o{w/LwŹG}s3LHCN]}a( \|uEt()g4UJNlAv9?j%І(<)X!FPL%}CCa>HܡS[C{! u>Vhi~7GU2Yy;;?um>TgB32.u|] GTw34^[xBF==sR—N ްޒN]мgCڹ:ؘqey.$!ӼC9 _-.56))_Q&'Kf%YwU ]ԷW.R%F9gZu )9U+Fw>ݽX^ ׇ" 3}',Vwn㠏gQI;77#3x(d׾V#|>H>:<'2YUI&>H Y g~KIQGx%?=[ ~ V?B}iglԄ;1]dUyG"xz9ǖO+|]0D\yim]Yp@IRht]́COUj:e]ay 3/Fe6nm {j멝rڭݮ5m.6Ҳ._PS+co/-L5*lW2]oS}?7|#ʈs};Jܓ6t]YR֣` *!ʖ@in^2ܵg F  RoCW 8NRrL@ S'|6w yN_=lwr_e4D]90q؏a@u!4; ƯT,khhe{ ƀNSD^%k srs;fq$ -'ug/)z5zM]4H@ZeUDϟ'!yh.,*lqst{m?vkC6^ ǞnY ?5:3,*#w_k kyqҕfE^jKx:s{cYUG{j4}AVŠ=Y"0uvҮ-x\X1O߱b_'Eڝq?&{.tw墏{|ݗ<]3>pNαzGu r30PIco[}<=M6Pvs?~D0f~늾#ϋa-^38ԞcZWA{_XzOMKڔ]BW>Ȍ%R&[զKܣ*XA_lݾeg˧"W)E~}>OSF_+NܫjF{~׈VOoh[c@W{>8n0F)~[ptDԗ.YT=km&"ʮեÔ~C- YM@;&m?I4%N{:^dp]6^/OMKh{FO!^[gt k"-ivmNĪ^PsH@^.WdK8Rd樨.11fjH]ı&F,UTWsDJe[Cַ465boK.k4]liNG0zbC#C=L˘w:B:*8FX*cS㺉)|YI)0w5mX.D5PI*9e/ ) =gbj-KL*=:J-'XaW9"a< /{+Iѳ9c HID|Yع]l8Pr6G 97btߠ%Sd.]Zgo(>.Y6uKuҤߒ_][Q7%kXHI8w_Q\oߙ]ҋXTDl!vcآ5j{1+"((eAX6D:;,[f $1w39g.3:,|5q:ydhjJ#nZL\" "p'lilBϩ HDĥy/kӬ!F6*NoJxq#~5Xy+yϾ%Qcvk۩c#>-*8Uڱ+IzY\K*P1DD|CA:wkNeeU0oשԚRZnnNǨy)jT Ͽ98ZP(r _tqw.@C6KͫQ\mX,De[ 8u7+ԩy5$ 15e?RKUH@&h.O)U.|Xw^s gK^8l̊=tFiԲ5uԬjU--?Vv_Q|(0"FZW#$r˛ a10~H' ŵcUN1#jiw|}ϩ9bfġ&~e%K~_ˌgUcsj ?=9{ P1$ jܼ'U8V[w+>Jmy,,LLjaagV(IzZ ]:r#4 H@>#[w=q?w[TL1m hkץy#]nݓVD۷khg/p IDATQAJb%MMnSKq~34$ %^PV+?#M}Aak`e!bTD|ΓZlLvbKs- KKk>zaaSgf% qq_/L/r x2#5k"03qO)$9*̜<KwO7ZFbOauttX*156aQ)r"ʏl(W@װ  GV:$ 0䘼_Aֿ<ϩŅ)Q"e/KuNs@QvM$&&T0*2 YF:ub\i-y/syR^ KO/|K)~}PFvvF8E쭠7JxW|4ڶeE#~]wGWȏ<Qܽ{cZB2ݺ;iWccxI qjZP9A {~.(wLE_-]e[\R| NtnQJFEsY>ڶYbX$ ey ?3"ԐT&a'waIw'Ұ)@e{Oy' |@H@*ɁYaPeJmyŻGT,6! TH@éGyj |qӓ>jُTR.D?g(pjuͫQE /NnNfhԗ$ԗrs2?F?F!Tܜ[7?𾐀T&a^;P!L'f _Uvo/$ $3>j-*TH@C45+ UŧLF򉸶hU]*>c2OdĘ@D"McF|";lںۭm{ ijmiz>qxSҕ+_" TH@C@?$ P! TH@C `H/!?laa=+&|1TwO\p%GD #50l WSAe O5o@4ĚmA|eǩ1]s@>1Af/(N9='% i / 8GIyM.#w EmǤO^rzshplԃ/[.˒K<'MҘ%R${s᣿jf,s|Nr f;(:_\&:=tCTBmC3M(o{2n|Jܡ #;3G{ G95_$*t.?O+V-0~dZb"2nܸlM;A7ʫ3wvLfN&x6cTL;ӪHZ?N̖>Y%M~gvAyUnRr9Keظdӹlʹ}j|~?<ȵh7hny/375hٹfSk| 6 Zuhn.rh#_ {޾YuZ5!}OY6OG'3yjZF\DhK+l^-$*;Gblt9jVx:kcR"}>LMĥ_ٴԝTUƅca{뗥oİ=n$վ/_MpmJ#x ÆjCiioOLvDDYS2ĥI/ENdDg{|oK*"]c v=3`Ja!?~]B] S&o00,]z7[~5JkόCo-QniP(4lr:x&\ HU jy~75[l6kͰn=%6~ט^?zeHallixEa e7qQQ|m6"^/=ClZ+a<~u= }5~uTڸ(20co۹XS-d{&$ k@\v/% c1z{ݯRs p񍷏vm%ML"75d8'+'W≵)K9YY|Ҧ5Zb"Z"l{X7xxFӢՄ3efxRŜ3nsD`۠HETVϭty* %B\M+[KZr"噙q~}U%&#%MM:DDiF xmwߞ5D Pw >{H@>1Am?vP[pťqj@z}sSc'pV:M/""ҹfVPc(1472|"} ErB:GDjaC\?z&'Dڹ/4k =foiPvؔ&c`\7li4]z$REohP";3'S*,)$HY78aJԜ^]!%1B!]M6=)1];Fyا+~ +rhz>~zS]Z{4?89u7uޚA~nTvIlf11CDD /žm[vioxq g\^F 5(opf=[S}>>FGOWy2ؐWŭDEO"jMߍM۔N`^"g7>xKS-sCazR.Gn; H`|~~xva1w J ]U&5UWCzְ,!z ^!F&|a0lⲙ{O_TӸv믇@D| +ޏ-R i)жb3/ rrofYr0;](;r-GÂӬW3'pyi'̺Ϙq9⴬D ^Xkk/{m}Z,#mM0Y6~ШmK|ǩjB!I6xO땋+}N;kTvo>Y>ڶYbX$ }0 @}@>;l^|V0?$ P! T yѡea @%ijh5bz>qH@>O͘2|%%ҠM[w`>Cw# "EIɡ?qH@>YxXewH@>Lx?&# TH@C@?$ Avd2,"pEGGnMqE`tsC#7NOZPoұg7(>#xײ.7;Gع :.+a4y|Q|\ߦDsR^ K 3ٹYb[Q Vbĕs:6faf,lv4a)!$U|XwNw1bĮ (m>H0Q[N:̱5vT]}~3z vBuV8fT/'}{ԍaqu˜W22QAs jpтϾj/m<'Μ9V9Oc6ӁKQYI?/ZMkMP=8b~3,^0UN-5aaDE*Y]aahw{!/Ŀ?e:n]v?~[@;sV \.$c]]+ U˲4=# 0|,~m,X,AE{a `6u8]6uN,@@U8*+[C*KPm 91gTO _`Dzc? (ڍ>&dERa+wm~+#[HhlԛtfŊ)ͮ2o?'o>#xײ.<w坦1 > 1nzYb߹nRk3A)\'WfU_ѸӮgގ3BW6Oja}ʦ~!KX!" a^o>s]pbE[im)+tv:\&n%"F{/\J_ܷdpC] "ܾxA;'3$7L#`k&n],deg,_]!j:\xYDs0,oi;mbKfm<ro81D$h&/|1z6 ھcj k?yJo\:fCTn[~Rt.\;z d2LCaҀ lMޅ˲F.W _?)KD7/nZ oa[x n_x+8'b ld뾾iTvXɛ|ägٔm|Te̍ 81ySq vCWBuȕdjaMCپ#2]׉1.;ǩ~_dw跛~nO˄۶66{L7ݲI2h:rlc{u7R9.Kgْ%V4@k'Tzzn͒%KלV+_O$ ZesBJ_r+lV IG6Gu:{FX>ԮSɸ-Ś88j?ٷ׍-:L7mUY7{wd:s~w-_GwZ;9sVWs~lwik(t- Q-֬y}nGy#?ɓDD|ѝC˗,?~_}6VlyZ[?*Lܳ9t왻D0 IDATIJ,uIٹm=]&"R]|lz1n-D{~8};iΣ9""=:"[\:]W9աjiOY=”=b3lB[!zZgz/ZUMqi>;e+ sB^C?a#۶5{CAΕkeqI[ymWW״=Dm8vG)5k_6Gson\DJ׾M֯ c9w];m.g*Jy'2ܴ DDnKWsLt?'ٶm50tdpQ$0Y98O%H; vS6D?֌n(z"G5+v}ö\xJ!AQ˾6xRK)U*y>w *CZy6Jڪk/8s;XէC~:ؾreMZx$l0` moΔwkQG5yI5^ܲ|o5y *N7.;xWWEGZ򗱍 l:>m|ǩjB!I6x`[kew0.3a%DD">=7+c?tk/-w+U_3f_?%n}w~e ߸,׶"H T曰 >&֢iVM;n}S06yCӶU&f\|j\BKޟ:}wN]Q 7Q{*T/V|"_*ī`*TH@>հݡ>ZhTvO>GH@?]?UFE{Hg5ʤR)VEێGٸt3FF5o}. ]f_@eZOZ_TzҟiQ6XUY;HCbkG.\ JYШo:m[j*j=*5rO~ úӷ~fk_vx`uXӆMS~$B^ko)&"E)m:N\ƴk:&Ŧ>J.# ////WUx X#A'ODyZ2Q@Aw3HX2bɏ=M+hM]1Ufz,ZqC:ͧm^9Z(8nE;70'C5]]L'".cGzǫK ?sS7.1plшf"_ɜ-Zi? *}{*khPCOiraٌ~z"xED$cD$n4|ɬMk SX'^mDDDmG,rT,2|} uok.Ұ.5w771y3E$l4kp?G{+Ók;jGhwlʿ88u^:䠊'"=XЭsS⠕s\1a}bj΢[W4ZX^†%ǝX "d|VCl{5R]:)`I_?e-ڛj&=y""R;k1B}ڮ㌄uicq£EJyU!ͱKGK>ny[cZ16Pl:S#C`N0rmD!Ofz*'s.͛f43O}icF|ʍ=ni?w6zTaNLhmwﶮG/WiɁ۵zڞff1.;ǩ~_dwᣌҔ@8͛7NL{ BI,:Aj]s:9E͢k7лi{*Fe[۰(.Ӱ莗7FF #Vq@+.!AT#"w/ӥWfhʴeqcՍ|(GUnjlidzTFl3iҰg/2]WB|Σ.\HRi4^yq?N]8a_H^gňΙOoH! -9m>'ԳpɲK.NKxݴfTR"z=}рk&6x Q5rD%i JKsWO\c2vm^ӻ"|ܕN>}Go]4|C?nXKLBP7c:Z*JkfXN?22꽑ad߿r|=׽Ϝ>PUkg}Rn]8{1Y{F #3$WZwZl _uXK*qak(VpyydVL&_^zˊG!}L*`.FW_2ĸ.Тe  +0ژOT#ry.t/:Q̀V.7ǔyBG|a>)z,CD\>\_3şDW3ݽ+ z8PYjF_%RIuصD/_rDXNH͢ˋKJ5j9cӒ߯漪%X@S=?c eiDDĞ{6maJ"R<7io}Fyy2`O_F^ ˋ 0 8HH$+cX]u䉳=~D-Zip3~>i/D 1e>bm :wTwvˉJ6R|z Ͽ㑘8ZP(r Z]4?d.mʽ#m[o@,׶"H c5] 1*v~{_6$ P]X"*Dm@[#GH@u1G?Ex*e*Mhu30 S;)Ԣw|CCNlβ46~&eS ^uK¥[Dk36xR n1ʤ[gOp44'7 ""b-95r%yk\H.u'~vfc!k;@XfD$hɛ74q2L&5*F.H/^;QLD$>gY?]z?Oȕ`=$diD ]9Uw>cH@2-I;[x~dV[ Gu:{FX>mdxX=8iֳwdBG&DFĊ!n=-oz*ڸu3t_7ګ}H()HP{vhށÛ4mVK@5&.Kwqo̚XaG'4ɐi_5TD?[맵~}бGQv_yVX`WtvOAӑc#RXS1BJ$9Wvyg+HP(¶ (f7x""󮮛!U`|G޹kCQ@q3gՃ^s&>NU-fu:$XIDD'e /uLphaMMw5u'ȸXED9؎k@)[3˲kH@=/L:iiIG{hD°~|&dܛmֱ-lf?9α&ecrJ$-P<8ԑ}\MR\rC+*812|9#JL521YԓH*-NMJVPIR4XL2H@賋G_eZy &|ɛdzrxN~isqxEZt7 8 ROsQ\bK9򳲕޹Ëˎʋ}_ 1V i$'3clkI;iòqYټR@ٜ/*zC(LŔ\ح V,`W)U~~/z}Lj97->:~Bg&$yQaopv=t(IRY947\^j\kleNW8EiF#!XJ.Bq/8<_!vY!kj-0Lݶ={V% }- B~;jC-+\8g|nW4BH@2.7SU{. 4Tϯnr)#9?imaS=DS*K XW|!䙚Ͼj<'ԥԧ?T ŭ;oNm'j׻ zvU 剈 [MtHۉ%~?^3n7y-IqܥZ&y .ztqwǪQDQgw`-l6 Yba H!Hge#*Jf&e $E ,O@;˙s3i]l6N'x<9+A+z|vg/? TUUEwݺ OS@ >OS@ >$,^Ed$ӺWy5YId}c1Ȳ(C IDATDheXEY vG^?y-S@ >OS@ >OS@ >o!?磭fYZyW[/k S':w~8:UUU;_&hIENDB`litestar-2.16.0/docs/tutorials/todo-app/images/swagger-get.png000066400000000000000000000667451500564371300243450ustar00rootroot00000000000000PNG  IHDRx^ٜsBITOtEXtSoftwaregnome-screenshot> IDATxwxV<+{ $! =؊B QإhV+Z'REw&$@B;!Iq(&! ?y}sni@d5vZuF٢N.zV{5dZk,f3 ?aP,fsMuj{$6";h3[:>fՉ ֮r6R:@'$9o^{}8ȑ+V+0(_lxſWVV1e.}rɯ !W|]EsAWxui˟}eZ9;;/Yhɯgee:4'lXO׭B$'Z}Z-XMl\\:tl Dl{/gI<9}4ۥmmȈb 1rdl{m\VOXW\_ Ҍj5 !ІLӦf4 {޿u,D SM܄2<%/tNurv0fXnVlJhwGIB:^`1cx;P( UyzMRIeI Y8SɪVW*BH!ى9&oȴ#Bm$R[zjù8<&*FNm8% !ߡ1%zOZ"826*F SI*1 s]i0߾5I?|('ݛΕB{۸7{R[sWbIB{H=dkԌIC\B6&lĕM5mpo ouN8F߻$3'p0'v8clhOgk:d&z'.Cnd5q҈0G<Ԟ50DylxՑ\.v?ؖSc%%Kܰ~CNnek Q!ΛdɢvIz]]u4!!aض:926!c!t}D;]ڞ4" ;甹낱3̑;4 AQO kSUS"TEx]I?pE$tC/ >֒W^ˋ|<$QJEHCg$cS>qs!t'{SzOMCԸoyû_\Ϛ?}Lz o윙}2:J=kcWΑlCݶ|޷un̿だk޼bSƆ]:^H撳?RT#sϝG}/_Br u:X`L"]VRUcPk~V M~{P(7מKeS$cqMࠁvTaVG.H}6YN( ẸI%2BEB>,+ꌄNNBr qJ˳aZu^z!Kj!9YEAv@ +{yk4~~Ɯ Y(&Y'I!{8VermVLǝsSFYX+(T/O) !Keժ_[Ɗzj铭Y+7VP몰mg3rTvm\Fs|/_=^$8M{q%X[+fU::}3?ch:]st{L~l2+$Zޠ,fEPޯ,涎 C`h@q."ĸc\8ZrG3koTuEHb Nvw/htnېZsZX-VV+l|ƏiUdEg.KBޡf!Dq#\<ۮ.?ľ}JmN֕`!SݽtڜŲB1f0'e5I'd`g>Z^qQ3GߢWgH,bPV}+/8텗Λ7ۏSWC[={bC;W:#=k}sƽ}boe E.LMn'.W !Ã..>/TvE g.yHTyyqV!$;/oNJRDI+иz{Kϖ*Bn]I?X!<ִB3kάq쨩6;DIG]K1W۶gݤo.'ׯPX' C=JmP EK|~w}=3_ eF[2w9F9) qr>ݜ[cUo{6!Pjj:/';I(BTKя??S7 X@}i;Z؟yV6[G+++W}^j`PϬÆSVgk$Nocck%!gVSyzN{YFm63u|LkssȈCm$!+fh$ u*.6)J}ً"bTF:zqRr"Ry19y=rtK*ڀ^UUBri^VPIIưq%!\ZDS B !il}xs2 ʲ+uz蠆z^ʪW>nIkLҐ¬}xL?cvQdxSS! 6֪kdtB2B_P'c4kxՅ3S5Bhta< FƎPr]]&Xb~/hԜ~ϯ\zׯY_pBrqKw\ycʢw7ЂqY 37җ$7_R2y/o(DW봗(Nң̘vh3.ufxpN*Swl9R !wux9kk9u2Y!o79Θrc +'6mOVZ= mLBBIg)za}jہ Pro&O{c /󯴶˒wpö׳$k}Uq}3w1m*2 K (MݽӦX~crU5Wd!L9Y~9UB.<'q_,1gW4\ۜnY3R*J_hOx^1Ֆu] t |v3B^{r+lO.D\wPEŕ8;;n(ß ! $i}tb9[Sۿ[~cΛ7g䨑Niьwʶ :~WAc?D ?&Gb֮9R*8 L1mgǍtO-}rGN!ĴSW|8_jQ]QmB=+/+_}yɒE G-Cn}_x]+''鞹MF҉r;dc)8ғL'P(5i]BxaE`kM&7'700..W=>c+-3;d1/mc0xx ѷVN)ސSqZu#fur27B&)sd#i=!{Q>\TxKn1s\~KM' 7Y47iуTfۅ%?|Z33|@߅;6?M= !6nq6&vaIRLm~ Kzfekq’_*N K{p`3|9^Ndn 9ֵsV^FO47F#6.OnDQ{cENJIٵqߑ;Y?pyys>6@;>g'n46$y{(rBI:|bj'>qXW*H_eV;;mهڃІ?>oitH &|G(E0K1q(֝]Ċ"[LƊcEIųǿ]!x},Z0ҧk ϧ%-Uϔiy4^q kξ/V,(!&9Utay6 m)}zo$Jt wD<˶ !d;oP* z/8iB} Bɻ2a[AE rPgQB9}>K)?M&(7 'tkS;* wf~e)rMfªg~rUREwBkպv}ɩwF,BEKj!!. nu9rpJ]iXBR򗝹)4F!к,Hy dE2=V!wu(F1!5oU[PoQB(%_VBhTݡγ_hmB:Y!yKBi(V\\e|f !k>:^'K_bdoǤ,VgF5O vS_'>uMfhB%߮7$ cMyr[˚g/wg+\<]kXU^Uo)'?(nܟnMNt_<`)a6_jԀ!*B:Fg2G⩽B6dTVl~Ѷ)Ko6Bmjݾo'p "&!W&$5{|X}ui҂7Y:>:k[.^^|ESFsJqVڅ㇄BH߽,߾(’m-hr]uy]uya֥|CsΡ1 IDATܦ^pR&T8VݒsL;y2aWǕ+FLt 1oېdb/(KM o=izMwwb.*WNn[_蜂zhRVB.>t.`dӻV+BH< tChlz{VEhh|߲zPROFjxtd#wt\q@? ȲY$!.Bc;{SE3u_$W5U~pa3K/_v$5lMi;W?}U._\ʹ Z 5{?l%]xaѡMt!)Ƿ'T7] " \ !99صuKݾq힔r^WW~jG&d[s 3_t|ʕ4j?/ɞ6~5kxHZ_S]QvTbRVQ;s2xhd>nvl)/JMͮCYRs/'/n+-N6`M;ǘClFmj8|n {u)5ǷnډNJh)Nq}^lں۸>/vAhÏA7CreV6d= [*Jő՟_lz{oyw 7hPߧ~Vc)G8j4Qko' !b,p`/ :ІZx9ٚ'k*+Mc 8'F" !mİH?MQj3>LB!I:;?2^#0<ָ^=뎕CfLTi`㩽{v4)?ޣ'~h\?.B!e/?L> ~>;+'?|Zfv^iiyɤ`p? "5wЦ܊)r*^4Yn< Z'w.c{eBz0JEw)vB M9}/fYZ,Xb-||C؟-Y:?ڻJi^bm6;Udh|u9fWqGOmO?iRتrS6չ$a>cڷ@c:zd/YГɖQvb17jBѿX+R*6"_~z{ \W]YϦ6iړ?x٧W40J*$I? _xȑ8@nAhC&I;u~7fơqO_iVZ=b͋'>># 7kB&[rȨ^v:$P*]/0|I:A)9Xyw gn#G-Uz= =[#{;q5nqs+܂œ씳IIM6耾#A.8y9~r[~p];)]Y-}#wm&"B***"YiOh%hr|tRuWQrQJsٕmMrtLE4yF\'\scRQVRaio1zX/<֥&^޽|}৆mh,OC4Γ8,FeG1yߟ>F۰C׈cKB}msƽF痗?]w/kuaFⱄݻH,Q*rɮa]m GoK<4In6M䵞~^QָY/ Ƕ˲Mx4su6sݹoydݿ{+{|MnޡIQqܤmqCrjou㶤~կu&g/BwoZirU_`/\֧ow;XYYQS^~-=sOmTH:; /gG;[4bJfIҬ kҡk|G /%qƒˎȯUʽx6J]1SlQ}am-uKxtߎdž km#syĀ[s Ls^c25 \Rb٥NMBK2/d1D sfvhڰ}MwMU,O|2: w^_l[TCnmR.9FOjw`Ҧw*fn]< ?6jEF=V,hH:;{Cvsv]cpl˗{ϿyvRybf Gi_5$5Xro:n hÏN51G6o;x)"_JuxoV$[bF 0fPKm05>-4''&][Q'r9I;jw =ױK>ɖuGIZqK.'±f{FQ;Z|= m78#=dM[݉EonUҵ;#=b*ϻx1+h5z;{'/_o וfVTMV3;x "kUIyeu'$ڣk6yck#XZ }|wr8VغxF~i[Ahi\}%{:\hq ?iw3wbܪ_~񙜳oz1$(tڤ `tIY_o?{KO.5gϛ~)ʯ`鰢f?ѧgq;'d8=qgoO/gTy3-lxZ!9履<$DOȓu |p>)g{(Td4}~Nf7B-N3f+ɸxz~k,.l~T[hB!yG4AcG|'yP6_#(*R矏z‚:!pM gzuǞw_-s凢]؏Tm,Gy 2 61KUu^#|>lwE*;Ӊ,!t:85ݤ{>X ?GF8~艟wm $;'3mۯl=7GG}vKS6`_/K2->z;8mҜ'~ItH ƍy'P!ُ6S#3cf o?Vi<yY3_w˅nve!99:oP&هqTݱssr!{ihW–ϏH>AQSF4eGV !G9IB=}֞U\oHvwj_Q _?~nIt޵t Ph)t\2 h֥(,ϧ|nCK177"F!3­j&'gE$~z$IҀZm+nŊ[(6*2m{Pw2[ع6? nQLNJDez=(Ic'M&kXvjX֐ll=^oa]m{0Y mL6@6@6@6@6%靧n݄@DhCX2/:4ʶg#.֌ 1##{&77/BÚp*&rt4clf͚׬Ye˖t= \Y1RwO&㏟9sB,\Pf͚~x?xwІO Dg{Oc O ץ;0g:[4nmT6~mJB%o~{=WWW!Ċ+{. :~ xsFאi:*OEQ{Rh$IHti:@$WnMLs.|}gM$I:##]]]  vZuuu555uuu,5kkkhkkj{R[(,flXdYeFz^3m: Z&5s"*tBBBBBBBBBBBBBBBޝYU׏{,Î(9ᮩY˾ejYlM4ʬ4o * *l0v1l|9?s  @6H  B$@hm  @2o`Դ Ջl[[(㶞z  B$@hm  @6H  B$@hm  @6H  B$@hm  @6H  B$@hm iZO?o!w_9_n~wymCK.C~?Wo\>qEhC"4L߿mF}փ_\Cw0B!}ˇEm<EUcs+[)RVBsq^B\CB>S>GLB6lQ]}=\}č ݱlu 6A1vǘ3E%=vC:mMf6{W^ye̘1g.//l빶tV! }I,Ju}7xN}ݭSkI[\4nQ6!`n^>+ sqUӃ:xMCSBȟ\z.BW-B.uȧ>vvw-By]v=S/5!+Əyc:vY;4ݡKz;e+s;?Xr+ޘvvA~G_іWܭgq.*Nzl9Yl!ÇkmBhcrߘYu;S{nL{|zK[v7Wm4Bͤ_2e_W7z1{oc>3y U6fsګrKk}lެ6媖UW`,ݫWt@hkZfuL.}9y\[Qٴ)W`\M??T^7t h6PZim% !ZՕ-6hE!W1剉W-[wCs5V7Կ&TVZۆ m_cQXCnị}E f?n]KR"BQ;|synaveeK][\_>=qvN5/=ݏS]}qojmmguW_=jԨ2VSYTKW]"e㟞Xz=XÔ'^XWJ۠•W%d_yinݦ,.˭X^c:<|`%pS_\SDJ.Y>i=L+ڢL&ޢv*5jҤIl>i$탬hcGB!d̯g8agѺEa'̝wr(U9}¸q=̾g߶g> ձS*TeC!Y{9(W#W^uNPÎ=S[>ue׬8jhQwz9vʋB s#wD!/^AVؐ=O^^9&BsSG*xAT9:_[C?pMAqq^K]Mc[ݴ ܷnMj *퐫i̥ 2LpzlBs_Wo_9s&/۲&G!3ONmCqë^[[c<Xɂ6(=IQAc/`7B!*k~{k:_:턝]uziW];]zٯ/g/|?ˡ^sv7K3UԷ|Sb>6[G B$@hm  @6H  B$@hm  @6H ȴgOhA#$ 㸭gmښUr6([/+گ*Ak6ֲfE[;r\.k ypP^Qhcs wd2[r 4uE.MmڝOr! hCibue۪[m.-6b{Sp|-nhlٜlnwp`KW >w?;5gb Hem=fԮ,IX˶c}՗>iB$@hm  y},Q'KCF ;Qo +Է !)t^-oSw3g[o?U|7eҟן;͙[4r_6`l劻E(8C4C!7yA8ZZq4\aݱ'˶JGGPsvٴ {[{n5Mnqȓ{ɟ=z'^KLވoң:<3{Ũt؉gmߧp=yV [/Fϛ;Kcߟ'k#B(/:"l˳*o}iqKH] u/;4<%wU1s^Aӏ,!DGw?{깹B&]=Wnˣ~+׎ulMq_rɃ~z5fM~1鍿?zChys.[S!֛EE:j'sn=~11|GN2أwwlm~B}!sۜTI~QBlh3;.\sZ*U\eLBr+SZwŶ±w=]Yv7?k>>7῏8W}s7t;U4{KR;x7~\rԆ7~2^#/uRt!ʤB燸~c735t|rVyB\/-Ȗ[?'zĹU=w?sاue^{3oV4G®wY\[ B_wXyh:u*ѱuرGol?:S<$IZBqե¼ U(ᡧW/aKe?</ qvSaMyൖ8qM:>y:wQSLZW+,]m QzH8"dS+U|Jwh{߾AQgAfwCN8iNҎ%aŊ!̮_SZv/4e\-ϭ qqSMzp]c]ڊʖn}=zvj|6[[Q>ף=zE!\^c=:>p`󟟏W_ˁGygw.@umONk ʋC]ayJ7+%E;GqZ5mq̈́7coɨӯ ߷$ !c+չkYfC~z}^T״>u=gMu,]4Zw6/YsYqSbEBܰbEC=*R.^ !fђN}ʢ6ReC֐cCa[~{ mh6ڷB{lQ>I[~1ttc뎍9 7-Ke?<ؕ} ڧ{{zɇv0]R;|+w|__ث+ܚ8(:+S4!n[ք kw,[`AzZn}ZeKܼ[o}Ç{? s!Y?4)ByCX̪ō>w]3zv𠕁=MM+iξ7ǿعC:6.\< ֭V`E--R|}JR\ejXe7:O73V_\m9]ҽ{Wio_o jsy= BcO'\r}Od]wh):o^|BE^pgY®Cv߳ӘBӚ]ۯ:Ws#V-kx_zzթS]>{?V?"W5ŵ(^Nmw GݒТkZ-*[!kgr\6mjjjhh.Z.|8!Pܔ!DQ*;2Q uMKc׽4W-inCci+0? ^Х^;gށ~~(1]pCTjk݂iETA`}Y[Fe:nFKX=I'! >j)ֺ(B$@hmۢveOZlF4KzXCƖT {nک(Zoc:yHt9 b._`۰m Y}RBYkHR@kڣʖJv{nƻu˵}(t:}d;gb!<;7sz8C:nm=K;q#^Xm7hءwؠB\?yFK =G-箧g,͕3N;nWU:3O?hBL'gpYu]}c.STֳ>{uɬ)[RK:uOrm,//!3rG3f~ݾ}1'=>iQ6nkZUǒZ}_]-8X[#uOL|pl-{qƢl.۰bɋ{o(]ǼhE}\б$]_t)_^þ÷+Bg4BIͫjfk͝2yzrڒ\v3rC?5s*܂q QaIiq~b^T3=zEw壧-n :d-]`Iɰ`mmҤI .|GN?d?bSV66(*=2M^wBG~)ݽk&x1*Gy<'ӷC9&ҸAg󗻧Ƶ&VEg{׫չa/vLٺE3ިVٳg+'pByyum#aQ]Oяw|&L%㸩bwavFkAo4!D8C w'qȠ!.?`}ӧ٪\P7cқqQT=m_!>D!xmAvNyIۭw:^17RzґNg9qU8w:g-^Q؜ˮ !y񂥹Я( Uϋwݫ}<.n:kX]Ǝ;|رc;0m}66**bT`?|So??]VWgBky=G B!*(Xr\q]uM6!u gp7֭uR!(?9 =[_Ȥ[wrB.~kRe.~!ǝq7W5qs_ !(U2p.bB>|ֶB\{Y^!{޽/'+Xߐ Qqii: 8lг`=Iֹ13**-IG!WˢԪ 9A-;g샓+sqTӱ;rW?խ>,}Fu9^h)zuI[B·wV`ֶJ5/F?<ҫoΞ3}q!ޫ{:DE\MO\VT_{q~vǠ(1~>!l7=6?QFCQq]tOVL0a?q-cC/iU W݄D]9.ez7lJkrqj㏎W^wR UՒf>D-|O7D҂l튺8D^Cw8ڻVVֵE>g؈}{u--̤]v)um`?Q}=XIg:t^lŕ F]xZ݊M5MҞ;~/_r}F{3ԭ0,NCs>Ek9=wھ[q>u! tgڻ: WV-㫯z̶n:`h׶@aE$@hm  @6H  B$@hm  @6H  B$@hmL[@{v`[ wO[k2 [G B$@hmCcfMMM---m=2L~~~:nAVeٺ: ڭl6փl6>h{A+Z6 Hm  @6H  B$@hm  @6H  B$@hm  @2m=RkeOT}߉}6Ly&N>gMxԿ7_T) qO=jEw̾EhC`1 s|HBm!o~DK!wYv·>7(BQaQN! mx]w>B\[h*lCw=Նw^''̘9ա}̧tY)iыo{܌E =xܩ> 5|?TJeÏ~}:#egv/_ !k|!1?;Cら,j:ta8h6T|=&ޮ5Srݜ:6u⧯ /UdW+~Oݥh{W\˯8Jw*V/~kL{{?ZDq 7\>!{?.5z88o]uw1Rѣz;{`K>Tlqo?rW.߯ ,l˷9&ٹOlRaSg?;s&L0rڏs* qBS!xhC9( !6efK\BN<|@碎}83|h)Sk#ڵ 'M̟2min}~SiC%/|8fT͢Fz(1ྃ슊BW\q9/kmu-M\SUBK%8ӹsIj㸱)[S.ݺu%? M5UMq!UڥkI3C!*qǮrٷQA\a=sovÂh|QYq93Cߗ>?u9nF @V削vŗ{.됗Nt?_3BQG۾S^hv OΑq/֯[I~:סv;ܹ?ѩs=[!=د}~]:dh1Q\tn}ږރ,?㆗LQFx{߯&6:`aNzr-*[ҏޏ(Vcwz `lm  @6H  B$@hm  @6H  B$@hm  e2h{A+.??GڞDJhKEEE5[L(N [%NwСh{V@6H  B$@hm  @6H  B$@hm  oq㸭گ8ö'vs@pђ]zdmZzϚ5wֵY .5knz,Ɉvv;V, Q[+qܥKJzdm[G B$@hm  @6H  B$@hm  @6H  B$@hm "}ΏIENDB`litestar-2.16.0/docs/tutorials/todo-app/images/swagger-post-dataclass.png000066400000000000000000000633301500564371300264730ustar00rootroot00000000000000PNG  IHDRt&sBITOtEXtSoftwaregnome-screenshot> IDATxg`eׇ3%B BD:H*MbyEb}DTE)TAT@zo!!H!3,!@Є~';;sϙ== (+JJJJJJJJJJJJJJJJJJJ쌷LzƺWLnC#_2ߌyrۂk6O̱j.b ~|Ƥ%詿{S)_$sNwsds//:v!Wӳt_&""j{wrMh̠DO RfTb*D0*""ƺ];v.@ݙAѨ\{L8씏(g^ (C̅5윂v>$+!;nEKJR\Woax ˅kVaבcҲu_Ph6h\ѾVͧ_K/&dw_?TMɎٱqcɨR332s4_-ҳe%+"-nS9|i ?ۿR[/ "&?ٯ3)n.55C"~zOO.(DK?핏G]j=+ŮNd]Ś} )e?;O^5Q |73}\tIouxW+?%ٿtJAKm%)I?M\Z1z/_rJ&7g薳ۧ:巳ڭWF7wWr)fѭ;f7?rcxF (?% [~}tttRSSK[V%9afΰGUǓC[Uw> &Rgfږs6]DDq7oa$Yɧ_LtR~=s{Zy7n\=|dM(&W_UD3nH{[4 VAd޹pƇ?E"5z}CC;v 4(1Cg˅0eX1Qبψ&[?}Q]K9W濥M&TצOn-6]D8Wdͺkr*0O#-iˌ}9is5Q˱^Sj tѵË^lhYu֮f\wТsxQ / Cu(A:\(zPc\1ңyuݚs1-# %E1;92@ҾnN yGk |wE߇WTڏ@uC\Zs*w_xQ mN>! Z ٿSNAʝFۤUq,619#O79zxTTN&͚wy7""x6ya,}>[w ݴװ}j<:x4~aKWlb0FDVr$? OI,Ѭ_R_7'Zڕ2ީzÐBb:uz?cŵo|rtsU̼c+ؓ[;8Ti>|HmG'fZ3$䌶՞|P-ikGN/ 3/`LϠbmGO>}lk߰O} S_?]ThQkV]XslVkD~O;("2 [xzb 囕kn~]קˇU5wvsUtnp7{a]='۩uvdܽRco<֧{~b˾tEDMywY ־YZv-QrI77 ^>]>kҬS:twV{Fh@oԿaG6+X6gSU3\+y7vNIJDDzԨXlBUDtYq79DzÖuR }Oh[09F]sdpz(Aiitv(<й5{xzb=eN\5z;jv6 Mr|"Z΁'=|BV58vpz(C˳i"|VjYN3TYt)EstV"(%h [~٘*t՛+uyݡVl\ADDrn3NvoQgE.բ 6׊ME\·:ߍjϾ=uӑ-eǟPYS77rS'r{>BE32I)l `OINlؽ--5mġ{jbpPUEDS6$Yj2Ξp2)(x:l*&'E2vs»esgk~F$;vlEb&="-]kР]n%9kP{yi]kHޑy}ko'z׿h6"[}T~=ǤO6gΩU> )7pk]_4uď&Mz@C?^`[{_^}z_AJ?6/"wK7+)(UXc L>U"PKM'"Jn0D֔TT}kmT*~akλvPK3vmܞYCr{ksw&b9qCF}xh&N熯kЯwMU1߫MϥmkKG6)[Pa;vDXDDDm;SҮEtvf4z7i#D}\ڔDD6?N<й[iWR%zWi"'Y-L&.5 T /Ej$"zv\v."&Gp3blX7x!!7e⍽-òzo^& -e&VDqmک7m'-5ӣߥg8y) "e|ŭΦ%O0Ldղr')yoVL&~dݦpwBS|YI)~P"b0x\ W`tdD -FW1hejb46R;1->&73S7kFMY=#l٪8S=jn%Z5rwђvuLa6-#5=?p0+"禥 Y)a{W{/z AJtJIX}\*vҷUW薸"GEt'S(n5拺쟫yw&6YVoǽڏVM~Mi[6d9f4CΚfW[9'nW8S$QDjթOsg- ݧV9[VhV vn)IRJlP"9FRMe$M:,vhRD#ԫBQsS_8䞃R[|%;bRc`򔘜#k%|kC_=zxkv?҈g}ܽBj6x""jѧf\)>;8Տ7uw_]i3XˀEP3R$򳒧G=9/_yk)OD]l1?̫ؕ??=nK˰bp0y9kZN-תOQUd28 t-y\ftMMĠ*Fլ^9\^vnZs[k+NZ5iys \xH %aoUh6 [D)v(̏QTQu&dyN?DK^}p)SoHQUԖ(kJJJmPh0Kwo֞!R?ul%"[Rrl⨧r4Z{_,[۠DDF"Aws#(#(#(\,:JQJh[ JtM_*(zZB#]EJ4]Kb[[DJ+Vs%(%w-;;;;ci@٢XDD4Mۺyoؠ[F#jjՁ[7oC+ ku''R#zzZJ٢g=hͧ WF-zçGX {Oqig[fOzOؑgܼwWg2xT: *l,Y17|{Ho| fx'46V (+4M1xOlh0zڟOjx8OI(Ļ?>dKoqʚ~1Mk724opVGkh%I`W%VzbֻƠ0,&>9b^!C lcqw{O SǴ zA\KŽ_g-ܴ'*1[^!C3꠯;`.x cS& Tc0g3ŵBH=޿AD+Gi{W1͙s6_pn=6NzNw^6]DЯwY捩cno᯶IMiu{}Y ^fO_ED㳣5<ʑmǼ,I*>0!oyni&"ß~sQ}Wuhi+.^-tr/i#zol`uC/HՓW7pAk_ lQ˔v_i[Phr1шK;7#;\+ɟhf_Ogq!FZG8h儡 9T5/+vǯ7'4y;g~]k֨3I7WU)sގ<^oQfPtb ,f۸f]T\k5e M]҉/n3f,j/ٻޡMU0d&D q2[@3ki{ykZ4]t=yk}65/Ϧ˷PӨ8u7Ǽܨ寿&>~džc׿tձY,bxc7SNzrz%̱_oH=akJUqyњ{QSklhixCMbPu~l;rɚt] :5uKo花vϷ tPDigSiͳ SǶ3,x\qGn`ӻDPc7l0TdOzOؑg~ˈ-%:OWww{<#wYKϊz͑E.TJAn˱)jtZC6sV뿛:DZ{Tlغa“~x JJurNz5]=aݟOc+ W!5&""9>?(B36cޣv w|h-t]S#褝_2럜WZN ?8x*"Sf0DDrrl"&1רƤ[< U=;a.+~ jG5,"!Hkx֕wxq7;6$ &z]%'Q Hnv^J#*feN25wˋ޵&b vSggEr< 2&lcBe((q h{2=>ռC=?N:|ƿщ5?tݤ6}1ꉿbb4ӪMS1Aʺ~Xr-߫v [ƨ53Iƶ׸r!="zƎFN?_btbE]1WqOEҴs Ҥ}"5T9i=1}k znWtwYĂ~nG3Iykp`HHZn+%optB9r:V ǮBx}֯ڧM&8!'{Ͼ,B秽7kù;v;sѱJ#z:~xDZr1>2,]L=7aC}gvMqbU /XqڴcC?I:}`]9^!M;:e(>SݪNMa4]GrBif򲳳oOe6~$yJӆ!ջ<ܩ(qXE'M=w|CNNNf`0j5@D/OzMޔ#(Y~C^xç$P;Ci2+@"(#(#(2EQVkiWq7ZA ""T uUJJj}*]׫͸ "=͛:?`]K(=͛jY(5p)!T#%)!W 6h`UU`S %j2iJJ(d2LQBPȥ{xEQFi%%!s'_l' g%enR6#|%MYGI+/}_?S%B7A A ]Qu%, J]IQJ\yժz p=FE-A(fDPn?EQUUUJnh,eMLt+x`;;;;;;;;;;;;;X|}b-6Y{oo%%vlS?^zW]wR(QLJܲJ3DЬ6O]Z&xOiۡkE "Ztzz]xŹ\v}u;Y{sx=YJ<-Ilfڰcg/d*kw1l`s_ix闀=ru:5x MO/q.ZCZt{B\/nR ?ycvQf,|G7~? Ju no!.gWx|9/Eqhכ MYÃwz,ٳO9cɻB# a.BP"=G{Wp;ZIo-h7j8'_0yֻ_i\35!oLT%؂)Zy*ZբˣrNmeGӝ?o-qsnfDvӣj±߾>_Eb,kz'5I[] -3&}mZovT$oSO{>!3jS?q]ﳌ1cc3飊m龑o]%z7枨GOϞ:zTsUP'~ic;Σj]#\涭:IGn;GM7ET`dMTn CʗPIaCNmu`6AG tQA-|n#e׮#K |U4ѯөrX?ؿʽh&"YWdXʕڢ}w37S{PP7tJW+]>NӮmNQ%"8ocHa李ZکF*6)wM*/m4VRY`I\?wGK͵躈Zz&~]rsJ^_f/q*%3Ϧ"ٺEDTJ.Ɗ+ 5>;WD9}1k~uRt{YND>"+SݮjkܸV&Z4wDB ji썝l6"_6mʢtw'ŵoHܐ 9o'zjʹ#GvtBQ>'?M7+_dخz}=E.ŵOf9ÿ|z#?zY %ûEX+R:asiK1JB9z<~'FK`PlwodRsN>(,>%-#=z=g[ӊ1 ))bv8qѽ•@)7""X{PFfʶV:}OwHs._G5u/4%0?oϔwu n:bV^WяlCxxTnݵW9N77zڿm7X9d+y-|YQ!f|࠰3~ݳ_oz=);N_;h<_dP]ɴMl6[^^^NNTvփz#iO/w)Rm7 stt4᪅F #(+5J!c3-"];;;;;;;;;;;;;;;;;;;zIPn-(5\BT-Lu]4(*M+)/FI6ET&:JDDܵJJJJ쌥]W(ZB(W(Z GPw]5M;z$rv#(A٤(=ԿfPUU`\hqȋ/sҮhVڵnZ.EΝڏd*BX,iӿ]vEV2TBG t]۽;LQRe&iEٽ;,// x%$(L&SoG[Q+YoS|/Rw\K hV:J&h"꿙ܥ :I9OqfML p#(Yo_RKO)&hN+?9+i]{8hX]M4_e>i/K^,KOIW m+=U\Zc[#ʲcj-a}t_ݦ~7I +/?}y_v eإ]x#Hާ^1S;S9*櫫b$sVֻ4#K45pޱW|;+G:I:b,X(FE:&e] d:G!z8^LC<>R|S }hawQ>ɉu}*jXNyj%xB ]  Urvs̊59qRN,E;Ր7]>PqRf.|DOV֞&f("\PbBgwsUf~b48n,Qk=4㥗7yKdx܁Jgꍢ͊%''ܱ>t R宠ASVz~btWzE߇"DΒApPMnnO("b0ް3b0}˖.X3$1$;7n\߹RɳꢨFWJAw|uon=#j˶;u!_uGTMR͟9m3nZUi{uNM.PcwzN9h15zmKŽ_g-ܴ'*1[^!C3-yOsf>x\ܰsOW]KFDD=Q9_9,V9WqW]7ޕJ8ٷǞ$Pv˖%_^;PUr&BoW1+'v*;$y2#d՜ͧ/fk&W*Z<4flji~a՟OkvȨL7{lya 2h?>:V 34kg6MR2rGπjn=_pG!ɯy0@ɋ۾b[N$]|*lճ]*"کyϾ1nċ~LJz>lڗnl1{wsl2vz o9u~y L=,ç|pcj Ψ'}YaSG4uSr {<dRiM#=~Mu߾3{kD1I -a;'ifߪ+{jZW4\m>ovgռS[~Xd;LVdŇY0i˗8Qti?e)~4AJ""z}gs儢WڰbUءTeJzVbUn7!1S Y1G'D teqm^!'OlY6aWc^<ϳ!̙~wsn'^Wš=xMDԘ}7q`̘+ Rhe_ED1y֬R3=aʹ',]q.׵Be/prS'+&k˼$:NniO/BbPeCM9oܨɽ-g:qA+N[x\2Y/Z7;͔}E~*=u󇏆sӓF䥈,:Ov?#wlƷ],2[W 1 ܊7_뚡žk=tή$Mg*9Vn=@?+=+AM5|}\%&k(f|ԣ-~._}`cA廿;qnyQKf;欈n7<'rj !jqMwi8:^wtV9{KM ;vqMLko}uwGZ""PP72WS0Z紶Wjia7kW!/===+/ʷ>+M׫[pcj{S~_X{n!3q֢ۍŖLe1!i-N{Yaw8#_g!Fsk9b5J,ǮORtűi2m4nƔȟm}tqotJ;wHh ;vƙh^KOconrw?w#8xവEࡳ+V0a¤I^q޼ycǎ}7njSR JlYIY""VJZM""ǢtsO6psN8k{*&bJ&)ߪu¯^X)DK>y,Q+ވ1WTBazV Q=zjmQ$"m[?’r:UEu۾"PGF%[/'U"߾MkkvGo^}NXc֝e gO[+2%O+H 6Kw "ڨiܜSgmV)ʻ=mEk@Y:[n"" \rLBKKMf%GG%i"j-U2=mj:nڞ{v_Ms"WQ<=;+[cף"J uk3N8K'u ~^MHnεOt24ڮ Aqj)uϬ[l]qV[Kv=߽öRo-)6.ۻ>E"BITEUQEo7-j/Wf En篕mggFgú5oٴKM K$96fVAU}d!lgcRPYIݷﬨثaIb湱Nlڒ[KYҿ9cq>p IDAT#FJ"쵢:]P͗5=}-uoT']z-i/\L+ofO|qf)-%zZ)[ O\J:72}NE}K_55|y@ỏ{ĿBw),UKBTN/%/>yĉr'ٯK*1KX^KLS%;5H6zSlcx'\nA:jkd zdu]ȴ?^qxIHn[pTs``/eeݰ/t +?!UWحzg;m&;􋿧-91Θ9."g\)םS#$do慸eyK﫯<2؏/=!lErֳGswI=DM;7F6豲 5+boݤkUIW&0MSt$<OB>xQO-7ϵ~wֹ)H-tI'u;2;]q2>9ig,$* /'/ݷ?ɫj>&ㇴZu0~wl$/NddAކ+qs\*թ{_ߨ9n{=60C2";̙3Wv?wY Uߚֿ,+B͈<eܴaADHNA!mkD`*]%q37ӧO/D!ͺEQl6lxz&d(g=7k]NOYr4vSl(Vkzz3 B]Dzkf){tU_N mw~(Q|0vڕ'3(sX|~dQ#;8. !,yڝ?qqq1:^}e^銨CR!{TxPl:]uh !}^{5E!!\AٮGɫFp[`x`d&={]wtױd}逖= mD(p(qmqn3~C{14f͘{Ȓ+y}{OQ72M[7mަ I$u^;`(aE y$jU'MZwZıժF %("/E{Z5*L}EaQ M1gggW77www\QB( NsuuZ{",z`0dJ8$Iɲl4UUe9 t{R0B (q ]J4 @C(k(VKt)jI%B$IBP $I, YJyE^o/xbEIl쎻h%B P!h%B P!h%B P!h%B P!h%B P!h%B PR9x#P"J4%2X|cONoxcNbzX=S_xseb9ppB=w图*ÁBknR$jƖ3sSol\jFԺo7Ĕ WA9 ifR|vS! {f; hP..~\v;~eWN#G jFjŭ tͿV4/wPeKߵˮz8URzmy.q"f鴯]IS<+h5`@澷[ ]qSo^UB4$X:o#igZ{t IGʕ yU>QEQ&QϤY;MⲄόЭBI95ÞcpX9YazxeLF=Wd3UVYd]ǎ psx.UB^%r[kƾ;a컯M\mʏZ-'z;~9w#ZyJfB- uPۘxh)خ-vc:̄ƽ9|2Rٍ ̺BܷgW|[](?xQ)zmV!Ȋ)ӿ?wGZ5שFBo)Uiݴ%7޿WvV|ͺ۬[P'ee:Z6~iw0DWju\˙??PO> dMwI!l'87L^9*|ayQAf, _yfGS/OYtrO^lQs~V͟)?q)}\)"nF% bΜ9sTR*=1tHcggm1%!Uėzj, 17:[1odU1{vh]JVbo\E9atϧGL}ȉٱ4UFߛ?5=гޚe['UiSM'vM%mGZ*/ CW>xҀOuzjuܱ^UeTfGlvŦ .] IM=~g]]uյވZZ5k~#GLZUjuҺ,=jV_!.E6x7lSGwUEXOr-oX>_w#;c򴃻$;Wl?'WF5E2qq 6!$Z-Tve'x*u!Z2m?p:>-H1IӖ$aMܲRķaF=v}~}c֫I6h˦$!j# sʉ/iF._o_6;|ӷ$iGIn [,c'O<돥M3\-q[nR5ՠ@\EX '*53ȣmK]5B1(Q3b3BH3ӱy76ARN/_=w4={}>k[誏eg\/X^ë́zŸWn߷/Nz HW5?tĤJ 6,% ! 8c,<٬}H }R*բW۷Mע{ 6D?QG/y~z[Kw\=$]-(\J\^ͤ)yw`H #ZBTl7~R7,Wvd!eLQK4$.NqpïzMORjT08(I[q{l6l2aQa3ٿv&|7ѿ)B?BuDLn!ӓάN\dشKfsO\D%7vqDtߠβCo͕yDt(yR()<${EEZ5[}'S7Ӱ}a]ْ.}cMdw[adB(UYRYXSUǛ (II(QU!$TUUH9btJjJ~omBٻJ֫gO\ʺktmXo4Jk1ys.1gWC .B(E(QRT9M3n؟\Ize U$Lv]۳5Ei骾Fg"6IRP^[~oٳY[;~`_o(½ɝ+Fn=vMToDX Uq$IB̟{KOh(6S'L4ŮGw2Sm:ʏnͨf6/VZGWJG6츘U^5>^|~FEar >cvڴT^빞?]):A٬($vq(bfdtEu:6mu߯"I*B}[CuLC 76⇅Cr<7vo=CPB CU/je҇t[.SSc@DZFv+'Go~;#m>4+B:6ƭ[!{~m\fU?nSGEdߜ#72^K@֯+~UTg>yHs:yJ D:m)!7k5j;;o%Ve\-!Vhrze/?%dFDBXJ}urV.>%Ϻ?=ddۀ`k4 ] Pff4ul_ l::oQ@gggѨd-)+JB P!hJ M!"b_{]0dw(|+p@%Cjv=QMoa@fP"B)*d7#%(j e %JyKOwfґ5tPqYɫ3mfk8rP"Tzlݲ]i%ClN' L%vD\KXhzchƪ=֫EJ=;X%3)j6ؕC{BU; c\!J4JNNy:6j( m԰0O&B %vb i`JkT1wVaJ Sa3 Q޳o\;>, z!h%B P!h%B P!h%B P!h%B P!h%B P!h%B P!h%B P!h%B P!h%B P!h%B P!h%B P!h%B P!h%B P!h%B P!hUUvwoDUׇ4 Pި*!p!myk<`pbEBB ;G %~f|MvIENDB`litestar-2.16.0/docs/tutorials/todo-app/images/swagger-post-dict-response.png000066400000000000000000001277341500564371300273240ustar00rootroot00000000000000PNG  IHDR–sBITOtEXtSoftwaregnome-screenshot> IDATxgxTgIo$Z(&RD&U˫b (G=QAX& * ^B !8 %!!3{ug17En k&Fn k&Fn k&YљK_uvD;êYfkxĔIO 1JЖ~JDj֪Y6﹧{ӈXdyʩ@cSkDDl={AYEteZ(UYc.f9;]_}wYga3=zagHO]y7=ξZ& ,4jƊNKnb&EdMB[LdՠCr9?8qzU_;۔af>}.8GKgڶ3&|bHqr 6.CB7Vtr}rglsX,ܤ d֙lę|`}٩ir#:gQr{y}hEv]$4)tE'9ąE]p:Mۏ8pʙDB#7kSbncsב6sãr\BhRFNKnbRTy,1)e,q?zsRZ?3٨[eB݆y<ϛrMj9㰻~27^{m֭jԨ֭[64Nǣad'5{[EŮZ7T@odcnֿɾky(#lZc*]ܵ 4$)"s6-\vA>f}w1TRXW^ѸInu{/w))<l+X(A<s7j{H5n2ED;·f.6cU~~͟oNrj<𿞾6GNV6$JV%M9\ɫ'=7-V3 U۵ =;ED)S5[mN6uYkyf68{t ۹^}Fٞ/=ݧy{NoTO|CgkJ;R^K< vGcvfd曖vO1)UXnqyʅe:mjpø~1J5yj f1Dz! [5o_p<*u|싦-cR2 /0\mڼY˺eyLz9sW=KEisP5M~S<3aL毋>j .WE{um\ˆඏNk90y1Mif)k&Fn k&Fn k&Fn k&FnHFYnSp5Zl^)uPbMɹ A R.\#_}Q8>MNRӤH7)JL4MD'8QaHE'W MNp:g")%jHF'W#nߛsm[I4pg| _X,Z뫏N|fadsʵZKIQpqZ{'ybھ6jj:SʵZ*\JTa(S^eVk4q5'vk֑+!֬#E&gcay(FJ)G^mIQp6M.^Ǵ5b;E)ߺ a&2-a͒{#i/MJZsbn_u{\ܤd/9ϿfV 5rM\#7)6iq\ ;ΑÞŴ;s^v|R !k{fvAjtƾǷo?BxV i1Ld/_oqd;%:˔̶y!%cve\љ,On8GD}Y0ml}ñmҳXnaܰDyZ"kO%"F>ުԨoP!Nm ,Ӽ-3SАoUnOixP_E"%硥su瘩F=ػع _ؾeB߇]krl&^h\Ki횷j׼UzZzq-[_Y0i ^T`۠2U,qh!#?ݻ;δm?5Dڳr #}3WvV_*M|L/k4'5{CDC.9ɸbQ?qШ@|fvW=Vq "~G;m;:u_;O kO1<|, ST[KXE̼L TDI"'U xhfgY4EDBx27-L?nxhûQQg&"?H.rΘE+c+v~֝_{@""tV.][32:'zOzWYZ~y9_wUtZg?ܓgWiGWQ:mء++?RЏ8K73ӾjɪMߓ=#<)ɡ֦_qE'nx6۸)OP"Z.OJpy" Z{Oe ݻ!":OG~ ;p2>"[ '|=r7kߖ9s旻n=@w&4yo6֫&(1< S4۩>%bd8ݒ"QZ3s5i5ػBjmzCG-q̍+elkiKM537)X<;XVM ߶Ok>jʣ-}{km@6eGtcv1BX#j e wwo7m40@~CװK~;wGy7GXe(C٬oO!fڑzA!֊-G߫_;t'"IbN97qI[e`B|+v(C+RJDٽnE)޶KWQAv:(p%uφ52y^ 1,}]:?HML,^"T->z.I>;֨<§hФNnDGڏ;.۲Oxd]@KޛknVM{657Yr^~VOr7ۆ.; 5jt9ϟ۳xiJ{4 GL|rwfY/9P]_Mp)&IaLFօ)p 4NgAAA^^น37+Y!axuYz+Wέ(QAۛo۶w-rN FNZԃkM)/$$$U&J4m fbwE<iWvG:jµp8~]pmkV{,KqWt &Ji ;ܪUjvM)?4M7PyjOBbZ +67P?S2n5rM'ma.R_G>2n',6rtw=7qӭSyizoEp+:[mXU⏩<%BuC'Z@qWDZk}OO,T)ͧTx&@&afni&/:l2%{&)eܜ˿ع'O7Q>tz#htRnj/&NQJlI>N$(kuZ.ןxz{Y9Mݟ`A7tro}P3eWO#iٲÛ1cW+%$g6* [֧myvn@Y4kΤOy݁gG%g回a nxo!sbd7߮Ktwu-ٛQ=O=ڿ[+7$e:Br-Zx'1'iƣSWȍ_;sڂe[Od_Xd<ԷnEČ _Ř|̟_-ܕOonE;vzҨSO/ PjWƦ"IyInڻQ[k|JLVYlem/HQ"cX)-umܭr6$kَm߾nګG^M+֊>t"_lGBr2QYʷ|fQ'F~X}Z5 5tMg-⵩s56d=~&@<-HO]ޥt.f?`T}zHe̊S1ζ!gſdyRٯ,5ke1JjJ-INfuY"CW-$Y}x@=2<8~pIyz(QKDD.8ykP "UƆF٨q@rֵ5#D$ekPZyR&ԇY%sD,yrC; e-Ӯ}-l7Ӯ^91^81q}eF3wyOncԜ{my)ytykTxZU|:β' |cmFfbja7-#F1T^~*oˤglƢm=n#/M5 tj5a[X#Lul}o=`zڨ2p¨!5}=oQyZYxuLJđ~4;@ d?NhuO5rObIFrd9jNGho0&d,PY]_e<.ڐ:Voc^J9|5Ud5jrvQ31SNfxt [EıSѻtqH_z.^x{mA7uScvaW;6_nuX}M.&YTʰzX~{kte_N~_Jy/JOIu&fxǿͰql׶˅ath[f8"*mtnV)%"r{|N 1yRo;6E2} IDAT1s:qN9([ÎwQ"U~ҖCǶMsvPX*5봾{ed37v9FF2EYh)\"*١.~"'T)"j;e\!)nwiIi""ÿbڕŧE:o>ͳ'K6뗽UҸWnbty뇚ua羟x3[J'+Z?0LF^o}2Ejv|w2c>fmyΨKjU"WU8gǶk֪r k QUZys9MOexf[&C'$jW#⣖Z-e;դ(Yla:TD-qTN[Nt#Wj=@C1ؗ/ňx0 33ttsfY0a[6~÷;-pܸG7MjE*nԵmsd }OZ {HSZװ;I;L9r8C5; rFpmG<w=&)Hڽlw/b9El"b Y׈p6?{ϕe*ϒ:+k?q^ðDDr֏ݥǢc2u @bTc$serގ:[rRվM*%lԱ|%3A-t\ȕjKr$˶UE$;QANhYٷ1RUɽfwˠ,:>:pæw1-VTBHz-9s*nqFo*D,CkdyYnĻJ ?:jfu˸FHj$?4L? Z}͖tr^ykƱy͇RZ-ӳE;믟-aѨa1 kԤ#ֶo֤ˡ V\J>%K+Kh~|7&NyQ?S6OFU]Ge-1:~q\|[̶-ˎS%!-#^ /cV29y\%$KM#{VZ>#M]_"SEZ*ueǢ>~iU|sMȩvjKc/=K_k "7B{%Wl_'z2j-fz*z^>2P@@¯,Papl. -߸HW]t%"!]jHVbtI9ZlUzwӃuZm<-Զ낕Wf7lW*#F_\6khCt!bRM]vUD Iab e6aEt`MR'rtںEDIGO5J~ ۛm#M XuG͋LE/}"b;ZrOػobVqDKZiN 77w n~h+˥{qqL>0LD>SW/oQ&ZzsDF\_fVaX jEn~bAn k&Fn G)QJep8Wk@r֯/Z3f7 …ZjnX(Javczfh]x9?].RJk-"7hanFURJT|g]wU.]aI{ 7 }>GM(al6{ժt:5&7jҎ0|iGcQJY,fܪ߄@R8*{S4T!^ˋSrf!7PN꟬qxnn\7MLn8G"tr^[>n*~nDq}kFn߄]9Mج"(D*!ZɀN)eZ~RJ ôO) C CNj~[;*8bM\#7p5rM\#7p5rM\#7p5rM\sľ}Oό~ɱcC.κsG{(D%&""ʦ"834 Ze׾Kzx]}ŢS>kDxcnjGGD}?mOQe~_J)K=+0dPVF=Sx8Jhv[7x+8e-WgynS:qֆY^os͜<{slW_Mz}e KБ!?l/ks5@'h`qޓ_΍(f=瀞8w,q}N=ڦfvzZ8,ZrEvpZJ ߚw޶U)V]Ո(S:RӾzEZę/■l^tno0\uZ?p ٸqϙmW<{&U+7ާ}РJx{سSDr͛{6Xt-{?+t-'4߽E,^*59óbJ .QYi6r~Q; ˈdW ǞQ5W;;귄WtEkʕcN _}S|"FX S""Fpp߳+~:{CNSj1+^dZ+Tt*8sEԑج#/ ܺ*Mb>(BN'[z][ڤIMlV%&"]\^NyNYk?]mO{nok֒|gmW1,3/oxwD0z#g5RLm2t~< )yu)ޙzˎM۷o~]F-Kt5~nrp7f+<*V ͋;X&w8^#{>X)fq_Pn"mJq`8ڼGe= 3⎤^lr+{$lty0<\HWw6|smT*3rDĨp۠ζu7,,UtkLضO7Xtn]aE$i$<G߬RMn%0B+77oK8w~E ].,(eߖ-:)Pr!ʯq_v:~hM f2/)bHΨ̌E<2k㎥۴@V@ع@su:""UK?ߘ{g\c)_y2>0fXv{n U 3u)%Rc|g70R.=-mu{?5ɯ=/l.}|t{D|al9}< Ru@%J?_}'gBGOYݱl]fi'1"µi:΂q3g4xh+3ܨݻYܵE XZk1֝:؃6\~%gݧ~(6E)Ml6TJmUPP> 7ߤp Jh7g g~\ )c{*2G~7Zn [U4MM77bW0 ;J ˦)"7)TT%H6FK77?=G(MwOS7f3\nvϚ}rH> $vRygm۶MYխoT߼ڰ_lR[ZZMZ7pY'={]: xl-Y2suS<"qAw]![M\Ճ=Y8b\.JssC\=)t>i,R-=*a%qj8OogKUw9$]z Iݻ۱׫'"m<ѹ۽qRtC3mE9.no3~ȸ_B W@HM︯_nܡp9SU&Oi)r.C6DhSC%GIcMpB⁚E:ַNQKvJDh $٫)8>83_wmvu+dx8#79M)߷n;ѹ>ҧ˺qpN^k #,8sB-#ERM?5@ <#V*"b^o~fXt>jkܹŰZb]#n&MTl>aJ?-ʰ8'7M;CM5^kv`weu̘ӿfߡzm>pZJDL2n=_ua5psS3^cC,ͷԼD:e~wm_~J~lSf V˽,kgʦoM]loPh*>xeOPWP6 RYrdڪC Y/r M9jhqrfejur6az?{fR[*bfo~sב<;B^>qw-ʉPrjfN *W<н沑}." E|Z؏z>ȧ])HX7k$giϐ ZUyhƓ4nl_PW=~[;gܟ{5R<᣿]#٣rf2q+RMiKxmIֈ[5'FߟlzV^U(વkԮ]bMmSlS(H?zۯΎwHϟmܨ)FpߜṁKI-P?gQtntߖ7ELXC"Rt}j:t mbj'xzUQQh-xr O_C:=;֔?ԺTNݿPR'CěF퉻vv߰M5Jsݎsq~G#N~+Ru}ѬM}8Y]RY;:ELUz㛚4}i XRQjF֪Q!y>ça~aJɃ89x䬞1$kAߌfL6c)%S>&_>ѫ}-for˾tsKѹZy4y8a߆Vôɦ OSy[&=e3 hsNޚwޢw=b QF_3'f9~[ގg~{Wȗ>rz8=;[m%]ձNTСB\~tzWޏDDN,MMD=ќSfsSZuVV|ߠ n,);`[$ݸrL{l xu\FΎ7x{%ydٴߎڵQ{UMqy"b~FnzZV=k͇c?ۚjkF/WfƙL8}9:fKV^G"5ǹkz=s۩ 4'ڗeS߱'ѬaoHWLNLZۨ  ?\]lܱeM_Ǐ獄ؘX Y{4&i*{w':z+Ñvh!bQXX;:ߜjp!EZKNG?w\CMɺ%<>7 *.@q{(1Nr^KfJqNqٽCh.` SRӝ";FѮi =Eԍ D*G[U3qUc-J ;DDgg8Udm;[k^pٴ<3i[- j&Lܼ-.`!-OߨIg)xcON|_K3Gv1}#G>S46Kl>:'gif=6'ٳSN:U}U_<@/"Heg\3,U^1'sEKʁݔZ}=MVNzn{=N|کMi6xFV%2ٔ},}ы-JI(-=wZz}0wGPݒ5!7qƳI׮lܡm7^pb#ۏ9~̼ؼ~5ʒ:km9i e ~gOujB8ǚY޽;E:3Ngyt㮥嬟/މFcN<7ţ|oM-D]Kɕ=2F#oˌ%Fxsv\0%0&S]:d~~~"2qIJK)K5+¢⃋Q(5M}n۞p$Xېm4K^,,(h01u.AʷݨgՏMWZZS_ֽ~[Ŵ>x7QnqM:7ںx&~Vmb\3:Z􄱍=텺7Ո mZc'xQxuݮUSʫ/Nz[[|:,=]"-|2r=.{G#U Gzypoµ_H +_{gpJؒrߛ3R?} |.ٸs|}}}{;WbXn^2|+vϱ;VO&D(-::Z#yS9]F*{Z,ázY0ijl6M ΔRE40lI]+tÏ@P+tstststsM-3(s(J9JS:|J)ժMRin.n|J)iiJf:)&Vk Ɣ&%J79`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`ZȢ47jU*?0B4ɯ&yY\B4/IYbQT\MRLSnZyYS\tk9ILDPNFd -nPMMMM=?;+|Q \MՓgxO7y;9f/,<_'|KA:{:O|L|z^*38yO9RV؞wҷɱo]]uL,TnglvGOۀ#{k]}i/.~Xu&mȈ9~wR.~U-?Rv/9I/ ;kFٶRlR.tkU5;{:udw,_xԨ;䁿Ma㖈~v!7_%^Wh%?2n9kY_Kxգoco&3QlKnyw'/ɊjmHޝdh:.~gg*uhMDDUm-DOql>bZ֭ "7u0p:Xk6O=qW+ٻw9= kL#aCOȹGv\\=RXѭmPҲkWU;wIhc]/ѵfޅ߭Ϭ޹Mo@dxۏ[6蒳{g5tKڹoG~\vfKD?vL#n@;oJn׹gܡv\-wta19? _s YfLwn#>7u]:7ޕǷ"8+ӮkoN- cDSٵyը_C}?#w9'úav,%">w]v ݃;6k9~Ϊz]lsmz9U\{%?-5Uw^oѮzҜ7O~Z=W-FZkIm⽭"oq5ߞ>jX,)u*|{[Z5kDK%~H ֺ}\3bvQ驀::T=﹇.[jգρO׶[ȫO3pdCiJo˱]?qǗFo񱊸D w5ȢN9_,ְ MTpDJs}ƅ\] qQI=ʔ]j]7Y3scκKKfyCJ>78Mhz_eS#FuKrƔ3,~5{?f9u=kV\:eHH_):G{U@U>u)hO;UJD (bC&,*DDRY-~qP,1׺rFnʑنo=Vsc^ L'K(D{ LZ.!2o^g:D2镑+w`Ԭ}ÃycGWt٘0rVϚw!/wr+oK?hVEE\,׸}sI;ΜWtoW\[?T3-8N3yʃl^;< *MDDDYm6%ɫ||4cϖe_G ڮYvj꽙Ebdڼt{tv54WӓΧwHu¬"=2*Pmʹ>uZ4 oܱ=;W͏1٩XO{|^R?pǴg>>[y~ҭccl͜^u||b6UflԦֵ+sknpȞS7NDM v/~ANo\6^#(J1\UMtӰO[|ޅc߬'6|n3]=~gjկ?5l+ץ/"Zx[+ck-C-[] 2>~s!WpiBS"3Zh?&rfZ`T4o3*{Ro'kN]jK+le!]{3O6pb|8!6nm^&"z3F5?0j- ڔ`﮳Ծ^HM.""V| -Kmji;maη5~U0XjyO.1k5 3Κ w*Mܲѩsy[6-ܙ'^ z>H(M/\6+GmΙ_x\?^y\N;IU*\1{o>rV8]bipk(vCvnƂ7Y޸A5&u>1}); N:ooMvޚ<7ȱ]4QUOx.?FJQr]jW],...,,;AZ#GҗO<}3÷F ]MS"F7L3I7ygfh>DY?7կWsb[z5u'U iH41螯Nk-!!^\Np b7y e;p+8KؽwͽX,e=R[YA IDAT*ο;`Z;袔:qeI&"bzŸ,VkI.1*rM*Hש}/MMRν?޿>(TޟC woM_1?u0j3rQOѳs'>J#{6 xiFr:pӳVO"CyCÛHaf k-9ՒS]poѮ"/VnqMzٻ&߿LԽ;z8dKtU 'n% RV82E ۾`u|SWùymlB(;ԵQ=Z!?nwd.B-ZpBǑtG`Bs#PgN֭u=#NVPuQn O([nY_}q Gs-榤ಿ[x_kȥ-5%"Fw:)w}e)L6TY"í*ۮ'ؗ9R ͿN>$(]r־[ ;wu%7Dտx4n RY˥Yauh,XzOW_?4NձZ3 EǺg8 EsRӫ"%O3ņSĦ,a>J $.έ'ӥ$ٔ1.ɓ:n/~cTqvmj$qcXJܬKonPc?{=fábk2oU7Ѣ:%{>zq= 2NH-rۭkźk)yϝwDenei8M\tdJJE3IhV%ڭAzhsDD|e,]wq" SW5}"mѝ.J.~I?pn9+RN&1.ƉyqoJɯ>?So7aqڼ~ ~>w/c\n9vmە6ѧwc_My~ý7.|*6er:3e\-,...,,;/ݑaEE_{aGP֣7&'qquU`ݻ=3fbѴ/|stststststsSp܊gR彛(W(Mq:,7 #Jl'=I(MYWە,FY91JR0 ߴ4*4M shӢI/|9|KT@7Hs_g'?lY""FEnásE߰:mzK o%b'>}YEoHT\a_NW6Y Ź7ͱ1fa6s/l,x7i7|Ta62$#lݝZ]z#HXj牳īRw ;vED~w7\#]O@9U\dqrr DD }K~0NrzӞx"/^軭-Zկ}(a{rޜs,N V;5o=}OH^~20]?~+3~yqz)Lע8bMĸݿdkO|'=uo}y [7흟mgyC@yUZt>#@|xt%bd7h_fӗL~K-StC '[xk"zAFze8l xeR-;c_vj۲h/6^xG=>~΁gpǣBz=xO{v#׻rZuUs1rV{O.:Nʫ藞h1ΥrʱrM""Jyԫbq;tpߔCyabi}\k]˜117u7lGFm?&%:?TlSCco%׬U:S5.vͿ^z^J$R&b^ s#:7_i?y8iSǕ[:⽀U4lmP[Q *_D >QQ{Wvxb,^֥W_i>}WXԓ&;r; (Y;Xsل53nnnJDDӴKU2?#KۼJU Ք_g^!ɔS:& +k@9&""%'4Y<36>E,*pV0xLӎ*{*û1퇉g>0xޑg%;OmX{lXj[bb-5s6.>eS.b #7Ҵz5O-鼣طMi kSC4#?f#s3#O:n@9U%wL^z{Z4h c)I'NfҪQ@ϽT p+jt֪m&ZPM%K>z V[Qw^/Kf}=oE3v1&"Qk줗*͘'R-aڵ>oˠ߻WՏ(ߜ0kU;vl>Re,iPGd-e7(\]םNgqqqAA#.hd׾E>ymh\|^mbtGM0G70WNoR*l\ܭMMMMMMMMMMMMMMMM̕V7QVʁZ*{Q:upQ*;~J)ժMRin.n|J)iiJf:)&Vk Ɣ&%J79`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`n`Z(y~X/-i RCZ*MG|*]':5ؖG߁n~)MsGq*֏}`s712丕4f\#1ſjZG9|eۏlbMv37V.ڶ`iz@<9*>bdzN]B:{n癞(-byk~W)CwDl_M~?>Xs0M}llO={\9_HGtk.F4B3rHZ*F.MB ֧ęњ>u;6c^1[poԦ_f.<~}C0f5r2>/p|h4YطuW[W(f@hw"rLjW9NYkZ(-b;akYnҩee91k|h}}w5u;=آyL}FҮnQ!_PXFi"R0Qxvc\,>/M|nƩ>0Ж϶%""ʽWuI9#I'UT ^+Ywؔ8yյ壛כM/4J{/EW =Y͜TD;Kd-((@좜s((/%Bԡ̋No%v]78䧔y׮94Ώ=p쫝ظ.|tm *\8~f齐ZB -*H Ha؎XQQA,  HRBf"ۂ M9/ݙ{y16aE$,N{z:3|# ^+2Bj5SgIǣ?_-窺JBuEcwOlЉa;4o Vqwfw(H4CfVE.dը]¼\*d7oY_0u|Us'NI^ʤ?N~I&ˎ<ZzW4Kvmw캌zۆ[7kl,NK<|˗] #y 4).)EH}G!ƴ۸ԑM[|T=yr/TI'eRqT(Brtv>^T+5=  wp}Jh+GB͒ka|Ϫc~h%]koCjId'mc]&H>Lp25lnbwۧu_~Ƶ o`B/0Fr9,1awpڷ>д97+7q;mmzDDy4N82mMaBi;~Vx3jCN{GZ߸iWޞ#9E0y GXad%fG\b5j=$[#]IUK;RQb4 Ü#5ne͛=?;0塳zNh4ød'XGn`]Xߤzt헏ں p97:rM#7:rM#7:rM#7:rM#7)xw[3 C^~ޓt:J{?>[W֜ߜS;?D_%[W@STr8>ڜV !I]MQfJaF̀#Z#9eٝ/S$Iܼ[w9r긡. }/=C.6СmKE4rtKթSĔ%*UJ۝ᥑ˜5GNWN4ctgI!Ԫ3?\}LFU 7G)Ro~>|*¤soݵߍOb$9wf;wkiTڈ?7~tp iRKB"$}iGBg` B5ˇOP&$W@,I:Y+oiL"i]]k;U!ZTʄ-ޔozuRrlƷ2ytl`ZEqֿWzs=n SNw&ftόy_4;ctc韼6!ڬqngwp~?Og4fn[Sy5_a,]o jف%+;۬ !):TJ2I7 IDAT]fkպRU2WMФ?K頨Uv~$\<7(I~|U/B5V(GXԪK|MtS7 5SFS]PY)Ȯ9nY#{3\DjEܗmO<ƩDW 5I\RvHBۦT-jHJlWԐN^=uՐϜMBܷ}"5X$R?}|}4)KeY:v|&lԽk@ѷ0mBz㥺coWy X۾Գ˒NfYiuFNN^B=ծ%/#%h&0 @+CI%S*ΰ|/i4 Zퟎ=7guh)R_`Q*UdUa>}/)GG* 6rmm݅H$>7| k͑oJ\-$9hĂGbkVMd>jáo__|^Rmiމ҂n12g9?!g My:UH!e;@MYKщcJ_.XqMX&ᴥV6kV!,) GA.M27Q7-r,dQmZIְY ԈJYBd ^=1s7>$ȹO.^ÕI;:h? 4Dk^rG[ҪdF͊*4EYw}׏ܶ߮4+Z8$; k*O|\r(#uܵ炣=;ODSmQfVzNmy ܤXU7Į0ԡ̰/w vvwJЪ8~VQ$չ{+r꘷flZR+$ũ`M#7BEQyﱔEת]z%!zU_ٗx&7bѦG oF'FuS,zpIJ]fv"5B굻UNΚ5O+V~2T^bz= fve|/O9s\ѣØS˜j]e&WۮϾ(O95S߲7JfO}r{rNE'gL9񫗮ߙUfv{=3[}I9;GM CO|r׫vaNlKk Z˳ь7i:T\Sa>$OB)k=ZgxԊ/۞T7xS'p+2kOQ+:{mtxm|UJ\dQC&X›N'KEE;>Pk?\s$ߵۤY' c(,QժK|MtS7 5SF[o1v}gN}ecE\ hp٦s@u1ƒgWY66&~&tൻ'&q6a$\upub>e)7$μh?SHqIB(B!$M))>{b]`v7}ek֟gD !N;ڪ1U_WY`je4srm6$亩wj-Q?:P?-ǟ79`h-Ħߗzw2n۝Xjæ-R7zӻxp 7p4O>Ԅ'R=JIq"Tew߿~jAA۰n>7o%4b#EBx$a(/(7U! !t޲(*)QsUH&^l¾~}`ˇ6 I}]̪0]Ču.-R??Cͩ(-U*.PFYy*${V!oH+tP?J#צߝ8'6[ф OljMMw>viO2<)]^高u瀹O.^ÕI;:hB63HJgd!yk%a3 W;SNf"$WOs Ȓ$]D67Wded'kIꮕ~Qn e@kfBN~y6P7}Y%%?aVŹ,Fr;g/%]סQ>&ЦE6evoؿ5+-ˠnwh%5f+Pm@Ԩw;-2bexhs :{ ~w>Wi[l6_j{LZt2)q؃O,C𽓻{O^nο|q1޸r:t\o7BEnK;7q[bUFnbȭg.nhTːg7 uklXGn` u&ֱ.Ljٗ qƺ:[ {=뎹#m]XObbM{Tt<jٗ&!uu_-U%ٺLnbc4p^S{L&7:rM&M;lv$lBkpaƸw罺%+Ou!\Mڢ*[W) ֑XGn` u&֑XGn`If7 Mhm]Y$+kIUWqEpmc u&֑羳fѯu1oEN5 0M6ںZ r@rrvqq)&[@ <@hdaXl]- IN-)*Ql] - t59xh i '4*M5%w3wqFQݼ2O|1Ѥ غZrl2)BиMZ`YDnޕ'z?>z:[W@AB |ql] -I a)N+u("]===l]G3!i]=tZr\;͍?~E})]lCv̎=N'?_;H-5C/G\:tgՁD6Z />jTFǤwc М??m!?̿ !>pϏ<չ^Ǭ{&uwhžo~WuaqJު2u9gWuzXkߤ"UC<|iW5M? !=hіK}(NyIM{W.V#s#LzWيՁ%N7=Ex]_Ʒǫ.Ғ"9ocWZ#ZͮmBHz:!<{=6Oڲl_^maCemrv &Ah%2#>F3r/!Dȭӭ B!{_nv֙*Er5mοZiPK-$n1n yC\bx(,${F`m1֕S%?iGb1?ѯgvXWT㇏U4lT[jKsN%qzݛew3ժ(D!w;VRʂT9g1fٛ~}DfI 1wdXޕ?B!}z \N~'O}|LR[w&!ڈ;>^̰N\yEsFar 6kb/(BN_:lpto +N<~nGiRL)*77GjZF#˲$] \3c[Bq~Co9jشvcECOH}θhsɻ6 y`36x"}^m>o.]W(v;yxk̈Y!?_ !8!OP7pATmf *Zm)navmx!O ۺmB)9ܶcG9F5pZMjDw$裩3}On: ٶ:gwF'o3CpsEŔv"~p?\{veVswu˲P'Qvp{ o9V#$tgOA9?=1vq܁BU)ܶ᭦\?鱖#9[ d~OOXNoޔ3^]F킙^,Oůb&%/-i_>݇kE)m+}|/7mLYM)~E[;Y2T$&k<~w [[ɆMv`d7 $S˜=>+ھu{uVw+hv$Y, 6!%>*bXF`byƭ1P[IdUUڰg0tk\vQ^E7?c!&cv]s,]:[tW\f1W.Ooz5;'"&gwuo&4 7"֣`T7 .6ǒ->j̹q}g*hMq[ѯۺF wr[̂,}h~K#5C+x2ݐwNx)$jƾ';!"ώ^5qլ佋F器kžo~>b}4/jݩ3ɉժBsgF-?˯[-}`+&8LjϾx`+v1=[9fXΫKc _}Z ܪ:dac]cVƬ~/K.\wSN]Rŵw)$y#wIUem_jՅ*{wytx|5YO8 3h؝$CZFw'\F `5Coo8c9!2 FOlg>)a3?o]?`ޜBwu(:pO0ZۑhB̧-}qRc_q׍s#1OYާsŮ^y(>d}.Jc ^4<"3IDAT9tqnox&ҧekB:"GB)Zsg*u& DŽBېW#vcFR;ؗ xq~.~,Kظ7 쏅B!oԓ۷^!q%ʣOJU5ZbN};{wYm5$zi; f!RI»GSUmgaڟ`Ʉp7:&؅mg1Yj_/27]|t>s}ۙP+#lݖ;Fo?cpk߫] O]>Ydzg/9+ ՘Z8A義J/3 6see&1ddڴ#%,j(’KJĮ(29*`}/p 7)i߾~Ew_ɝB)>O<<9]k3|o2 ,!\ouɲ0EWWC5ϥsǔe$T*.3߻{Cy릔CJIܵqN:dҮgMB}Cn{~gP]|ݫ]x7@Y̡0trbyٯ8~bn4}^S(h4 9i+Gkʮu n^kDCXbn oƅlu9tyl^4,_{y1 څwkPChڅwr7/ ||Whyr\f^&M 7ܤYnYύ o @YYS7>3wNApWGXH><nXHsZ@kpY,y'vǝ5tvl] \L~73o_غ#y 7}\iFWpP >Zr Oiq2rOU,4 \1=ϻ~;/(/Ha0Խ̃ }d?U'!TUU9I GfTYz\*rJ޻SRruIM>\>1'5rƗ$Yӻx]<}:wܳi宜,ܤ0%|ށ޷b;[-w͸êI+'Ca#:0U'[ !;zȠot\LrAnh#&䙳a_Ǟ"+E_v.j1*OS*=O+ٹsϰͧ=f: ^R Œ灣Z9,hqYNz˩u^Bro"}d!2 <:O[_mP]:P,8iFW0WT9z<&$s՚Mg$'6ܼ3Oh`p~U+Us7 wF4`L[Is7 7itA~"۵j|pIʥ)SuzKjٵK~:\!yqaQC?[Y7^ԩuF;$.. P+TsAZt%"nSރ/01sd"]5Bɏ^d[>hyyX{jU^ww0NNj;o (v'.Y'FAWI B5MMޟ:J<Ѩ'~"$!YgNVBegG8j*-J3uk*!UOH=$œ_U3gԩBTUW^M!S' Bgxh|dQjиQazCd'''YxyBUg:u5ꝱrO:F5 &pcWGS&MD]BKJy`GN~rZ4KB.ͣj2֯GOKs:z:Qxf{;!&Q-yAbi &͚j{9t+H}h"gj; ,ZuPAQs򌪏^6 =MF67BB礗)^{ؙ2rJ+^{*M漂K@[]9&V8B KVjUrָ =P#8"]+5Sh}C|4kJO/̎Jso?4ɥ;j]澭ST!NE'dtf=sVH>.BIN!Ra,YYu|SvPYZRh]Ny($҄=',RnQ;u7Um=JM>Z=JnҼ+rVغf~zΔ#eX[]Uu IDATxg`3%B BD:H*MbyDb/(HQ*{ $@B!ݝy^B )|?/4;;sb~{Gu]p{Ԣ.4 dB; dB; dB; dB; dB; dB; dB; dB; dB; dB; dB; dB;0կ"""Ǿ|D팗yly+O̱j.b ~raw>NSxvONn` !"Y+^([/B.gkMDD2mWY)]EO_ fTb*DsG=/*""t.}vPTEq=T](Jњ!znvNA[`p ";nͼ JR\Vm!=[y\ C+v'Ҳu_PhVvkX&Y=7vf/cȀ{uݺcQGccOgfdhFGwJ5?Щ{ $NZܒgy~~oYLu_3UXE2E1K6YOwƆ;O)ʺvډȚ5k<==;.,Zɯ} BIɰo>ՖW_6/vwZ}4.ִI{,{w^?V?~"mhJN޽N*7{e={HvJwrWZ(?MXXl<{'.&,jrqs6n9e<|5jtpqspu0("ݚus,W;gp谰5kDGGk.55SF'KN;2!UӃ[Tu>%ʢg~6]DDq婷la$\nKtRw}7mKwZx43vegYpxu(&W_UD3lX5RAd6o'F"5z݃C:v {M^waQ_?\!@S]~"vڶmۖ~䅯zBljZݘ7sUDZj~9(zUkϧ/c^hj1yrCE-k66(K3\R-QV壽[.;<]--r [\NN/n2{/Ko Ժ2a·,ZbdTFAۼMcׂ|nK<4Q~=xOe˲šBiXxΣiۇ\x|[sΥe$,(f'G 溃G<d[3r5oQ!Yգc;ylcH^ՎOuC<4alЬEKDH8xlnА. vIcZiWls(]}ƚv@`,v@`,v@`,v@`,v@`,v@`,v@`,v@`bӖ?mjѬI]uOP:ءҘMwTrHBTE@4gN`E4M gwGAj?UUYn=d)HXl6jlil`0F)œC$%?aX,~O s$ {{ի\.`u,&K~jX,3O=-VW(]͊~n5\M&h,XnB6GZtaDzn"J~({JxcYX$m,"K9wou;pSVsd.{!KS4ME%".R>Ni(ڰdk=3nԨv/ ![u!"ljfmZuXɱY/tkؘu'-WsGB=K2#! Y쀐Yмgaf+b,.)dċh[֩kNo--|$mOn~x;s " >M $LkgѺӯy =%mШwE- ;=n-vȏN7^w(n4Lqd5rY6hp r1JQ I\<=TwMʴzFJ :*M7Bݫuv0Yzv|Ye;vm1Zʊqڭز{Bߧl=c_p'mҼmiiE]ˍ* y{ް<̦ztQGkOӏW0Yk?>8}۽UjFG\_3u&bMVhp9U1k:bݴˏ"̊c[cjB7|i^c݇ HKM{c{E䡎]ӣb{îD5;6(exES{9(܍LkpmigDD1T p6;5f9f48""SEstV"(w>`6.ͳc٢V6\.b`p]ɉ[=9O:ݵGAdƷ$\7Bqlޝ?'k2?n [~rTݟ|]egEO]u4KMR4$}=p^~r<&fiiC/)9Kq Y}Р&UUDt=embUࠊ-Q''Oe`rR$cɝG8|ztKr}i&Fh"7.8J"""ys4o]|ANĪV6]buZ(wpWӎ4x13vj|PDD?Ou`Bj3f}yȡk~NUD]ߍ%_s|/7gEa{OzS9˘_[Xv~7k+HXXwzuEU`Yۚ,)UXbNLOT`DQ/4(^aRR5SڏֺA+=m<Ҭ˃A7^}ݖjڔ1x@cm?;R4uk3 ](&5r?7|.c>=ٯU5{.=m]=޿IQ݂ ii޺5"""oB=F6ʦ{SE7ѻQFqGmQp+]>P.|}bɢny&hpr0UROh\BȰ˶*;Q7;9YHysH aE آ+,ۯg7lkhފ(;4txݺS{fzT,!?/%AD̔To-ɊVzgӒϤXOԶnTdOrt߬Lf+lͻKJB|9Kg)!qzg֞N[ѡ>=c3x6Y eGX߿ssȺLݬ5MDftLufhqV1~n/""z9Kު1={fD ""LØ-ddED?> g667ܼk$,JDRlCB)1 KEvQQkĖdԭLEmVQwfpN"1g4Z -kԊ ~gGi 'nҍ=oZj{^WDġO]|Ԝ HU)y(xY燌U-+DDZI}kn’,%+dŜ۾"2r%X{l4)"iU.(ٹ)G_pl>GS@dQֿgyO?ҹGAl;~esC飑kݽB7*""5zŒѧzk\_V_˗iÆ~oΨ o: Ϙ9KEL=)Bv{ɾ| "Zg^)Js+ew@G=>δ]<}wyց%!$<\{t=c;nQ=Iĺ]B*?Gz?wg9{e}-zOΈ|;tKGO$)پEX|5DqE^%E.`ɹ7ƭyIIJcW qr UO^A,o,x=p72,?pZ=p(bݢ~Jw gD?mV-ء}{M;k/wjٚkևcטNfּm>1}D}FۖըؔwF|o6P=(:\1ysCyobs 3).5װFEGgLY‰mYդ[{3gC5)gL>Hҳ,Pz "ِFlQVl2NڵWrkNw}mt*VugJg>yNSU)erWL>8[@ɓ+s/>{q+inh{U1 YUoZּWfA譅L99F6M+\ߍu6għ41N>~tbxΙAGܞ)=ߘ|W%g_]f-?o̊[t~MM. cH_jLx'-\pְjκ%ȜrtXc{:("ִS {nlҤ6E;bnܡUb>\Ď61o߾5F U6m3eSe?nnyQ _|n3[g.n؅gEUBݖcSU!NGo'8iyZAb9H {u td=9j)nN/ZjrPmʎJf۾uyj *梅'DK>v8QSKʑ*WFCG25ѵOvYxqDdM·,^ޡΊbrXFYCz,\fχ (T53d2T@E?ްC·*ƪ-;T?oEuݶ"PGFg!K!,b>?fq'5v4pL÷l~1q;l IDATq(Mddq^u˚VLushvɨ""FQIDDS/mhڹMe\!Z?fܴ.cw񕃇GAS-]v aOf{~1Ph(bK8)Z[V*(5&""9> +F!KOLѻK9ذ+Zk'O뢘H{'eYr^ջrh5>H^bĚN"hɱD_nh2`pcWED]VW.Z$XQH}:#!2*CIRKEl﷮MZ9{nMu:E5""9za+C+=9h-/z㚈! 4MQ]Ւ$Șyk&w)8q1[䂯fdTזkWV'Zw2 ˻};ήzS4'Ĝi2E`է|un͔e8GlQ{)'?yaBzS?rܿJW%sb PqǎťQV7th PFou1Z]rӮOs%.=A'O&Z= "!k-ۜ ArܛUܟdcy5 [|יLP{CڅxH?Wu9}JN/N`@zӇn?|cfu uqi5|\|d#YoCچZOm ?VY!+^Xq~tbۣsB?T?˚ɿnOx"xƀz3o iUʥ 8Ml6[^^^vv#3+h麞gEw)2LTLSEF;88(ʭ4LtU;99fw )Kğ_XXXڸ&,! Ŕkv?8|-B)vhWEoJB; dBCQZUfV\pUT uY庮W )Q OTUiӣQQ{ ߫zQWT )״v\:w(4JH;D*!U ௪jmfRUd2vf  d2Jt' ! W(h4NWrXkYJtEP|,\_)pݥΉJ?>W%B`,v@`wiMV]:YX`"(ɢJ\ժpSE1ZCEQDU̚}(*(EاhKkw+ڠg; dB; dB; dB; dB; dB; dB; dXv~|şc-6Y~'ы.{LVC""ŋ%eYmZtX.a,3n9}-FrM==bO?~$\F?2f?ϕ?,l>꤈b?}USg35ҿQDo{nn?<`GOeCYCuyB\ĜhJvc6_|AG67~?/ Ju ng!.x|oDqh7 Mʣܷ-t8ٳ{3%[bɻ\ծ f A%dCÚox_=h-~wf+՜̝0 5PIJʐƍuxc3-<-Gj^97>)_ݻb~bE R97WU3̕V~;_h1+1MM.^~ƩF;y*w\gǼI_~N]eƨѱg_QEDܼhw1ْm=[}gjz駎:ppSt!Źq~ڑ9Cc8Tkj۴bcTCUwSDupvoHDLR?4lr3g fu{@U1992M=ز^Po`?To?XTNlXٿRn};)u+&=l"diFˑOh\Lz=wgs==t u3IIIJwPn2^ej5ܺm,+ŦED2퇵sol8ּgCV6Tj%} x6WoNE.a?;OZt]D-W5]?UDDr)&/~3m8gtSl]"" ӔTwOcG&wOgk|vr2&\7Fui]kbmW߳(}.d`ڝy[ݰa:4kR|1PBShO[.m("͓'Oyq pw2[K󵋍0P؎9ngF~2Y;8g'/I63tC{)WSR\ |F}; ?3wZPltŭЮAKmϼl+DF\}H_í'ɽCT`;y$ڃ]y8ǜ"8OcټoDDT_eƞ3wuȘ1;=^9NB""Xs@m>Z 0m *9Ϲl֏ؽЄX !.%Zx)"^F>v/Qe bV]ܦZ^^WsQ'd}pΒ)dG-z("W¦L{-6ybt2l5e~BZWw{C@RtVfhfrrr<vfY~此֥Kwн?̮4l6 V t!,Xg?pY쀐Y쀐Y쀐Y쀐Y쀐Y쀐Y쀐Y쀐Y쀐vEuPB!d5|2]+۝.h]JuVSm&KEEPe[BPJCp#dB; dB;0u;KDQBDQr[Yu]ӴC#]}nBOפ}UUF-LRKӴ_~mwu-l۾Wފ8pPӴVuu]ءOL. (b<凕Λ;T-,t@z^^ގa83LF>(ʎayyy%w^,P:w߯3LՂmID' ˪:K%&gO<(W-*iZ Yd)tD̦ۙ:HOqfMYJ%+JXYi)#GZʯ+OCvn!-Pm_WY[ھRG+~}:JB5wy%|'HoBvLRm&hx䘤J3}H/(6?&gLҢ!=,{]ڶ?n)kոN<ěvRb<^}ʬiQ}HGek4z]B]#&>3zio!.rz*9;#9bTG=&OK"I3W/\;z3H>'+NnSuɎTy(1yRֳ*abg<1w7U,k?Wz(Ar͊%''-?l3.R\;}D =??1IKvDldgIm(x]|e0P?99ZQD`fG {Sb0fŋOht k ud(3FŻ'OQ=uEIثpW Y 0Ofl<"=d=YOdVnSIzeP"d ek&WJu=2fl듭*kzv?[ew؈L{l{iҳo0h׿w8:sW7k'OǓR2rGπ*?nnk<:mED$y']C^dk!{8@ɋ۲toŽ&|Woѽ**"Ͽ5fgL M|ךnl1vl4vbKowdռg}Xd{KVdŇ;ɫ􋗓8wyǂQ[uj9qq~mZտF""z]ggB娢WڐbUުTqVлchWޯCOIoԙʖ8 >+s~R,@I[DyԩP?I:qGc˽d[)`Ό߷/~Ƽc b9l^a-+6ht o˩=ۏnݳ&;xmY77t+F,zO7'%g+KѓW/\+>-W9mdqd82uCAo>aC+=zxIiqUrvOz%kfز:GW5;G"bM;j0^5YudQBL[E[SdZbo'V/=:OWv}ܼg\fhN8BEcޘpu{t X17Ъ 1}tbhʷo_xب?zHl…5Y,/ۯStűw 2?u)-/I{W~W0qw} UDKغ-T[#ZR|bw}kL4p3pcA_90ooFƾ'ͪ{[tҏ>hC-8{ѣGۣF*b궬,Eq+Sn&۱y9& ?9G>}V1Tnv_mѲʤ/'ɢ%;ȩ?%oEGZשQp86jZv:7x)n~i"խBe7[ڡy]TbΡhuKђ4cfM*DVoʉ=|\{k3׺~oEů"yzvV."ڳ%X[]7EjW/c8zWѬSsv^}FK -3+t,0jԨzJDsٳg?SC )V!UXGLPmbJsVݪvZս{oO?يDD$7ʧnvnS| 85>i|UaO<XI?YWzIe( k@M6=/ؒ5GclVoWCڔn{%ȒOxRsjfN;Ogu]_,+k{MOq=mwߙx;]-/5r劦Z*Q*&'-UUED]EDOܱ+PWTȄO*,[p=blڗ3*OXL6MDs? ^S̾[\~lRl9!8Y=VO?1'/EKZ9{!k?{wPE0p̽}ppߵL}MM33J۳|||4+\pgDDd_2~@j;wp3׮fB5OEfvM%}iUK-!}ʅP/kфX7rc^Ca$]ZB-/]ИkIזAFnxqͮCaZo{] c@CQNVZڅ6%EJy)GJ4I* 9 A3|nA]UM|Ҙs*5ɡ^͍!HB<*33Bm7n@bPniB%=)Eqi}睁WeVTɥU.Yd8jK^-sձ|94P.BXn됃zg f߾C̒=HrKٷzG 7"mKV>:91dp'WI:]C+8E{uڈVrxHSj"7%Kxzz !~m{ncT<ێH9aӗG+S~ieɇĜγ4u.J'_m=ҹȗ&4I]{fT@WBg6U^/|eT胉%z/QHZѲ8q3^ Z1&!$^c&v vP/'C'rTFc<* pn<2˚]Α$!л}o0S)JGǵ妶w7JBH{۔zy[#6SMI(Jղ[:M̘>㍾z]k^xgJvaƲ9eNM(BrijN߽9:#hys1czE8 Dy Ѩ$;-O oSwvB9iۡ¢z43N9t³^aӦ7xyo"Yl6\^^>}dM79d64!~7g(߿p; ɔvN|<[j-,,=z y,{~=RiFeWkt۫S:UY'lrD5\ r;`ϗzxxW?z㝜L&(7$o]{2K>ok E+unwbN!Bt`5\TUgNjmcl^ 3sdn~ V~b^7(Pr@DYf&I$I~b[@" ܜ$IeU-\΂jb,\TӴVZȲ\sSt!@I C߻za뎭?4ރC$M$I{WOPs# 3Y$Id h0󙩭ZpӪU˙L h`2jnda& ܜf6j0$$bLfAuSյFd!MUQgggժG\,h4FYkn,pӒ$IQYM&iLcA$]c[,p ]O>,: " t,lR ΁BG8Kuj'I`Y$I`RD$Ie!˒d΢LAe5~&K"B: " ,: " ,: " ,: " ,: " ,: "KMuQ{\Ed woퟺVeքA'⟺ğ`?7 GM57J{mlb11eSfxѥ& !V'0np}ƥܴíGR.6}͝$ [H5,IA=$!}}05o'Ɣu" }RS:ug"loN}wkaVt?61\DC1OϺ񧯭_Yx҂~:g\:>e[\͉sE6[@փ'#QX5t^I)kv${Gxإ 65wcjg/>Y*G71{E2޿X7eXjݮĬB%ƌTgA׮~}y؎c:Kѱ֮KmJ?_R9x76f=~^^lqgX ]Ifޡ^J5EvxСwkiG׾s1WD+9dr\zj9Y+ )JouLN;vԒ5K>ߣJBaK0ioM{rɊcBkW 3wՒyn_b]I!y}ŪZ{穕)^z.|,jt!]ʵ _sKod$OdъSrK5!ove1+~nC=%! {GoܓC[; kvB\O ¥cϽ4{R`YS}է&ww5ԟ}Cf<:dԣnie _vWх)rQxpH5we?R}PE]h='* t󗅱o9s l޿O/dI|Jʵ?E8u?B򇗳mZR9 uVli+[' @V} ]ݓes>?i2_x1MKbW!*Ԫ(Y:|/Lĵ/Z̷>OIΑl_^m$s_GJL9iǫ ٯMצ&ap.&u. !49?>.;JZ/.Yr/%WSIGn_ m|{+f\*s)r.b/u\լ9ɗ~k!sw~~B&TsazlLR|ZyEW`mgEhEGwͼM4L8;8[!_VZtwZ7tGVuGb0-&i\o8z%vM/l}:S+ i)RW ?>zB\[wh. !wLS[-O3!)w@̅T -qU/#nyUg/߰fD)g3gWޡQ=gLZK"3>X:g{岳_{D4p)w8Q,egPx{G {k>G-Wv 5tyşf3}DGϸ>3>3SjGޣcM&(o/ߩ>˅j0" Ϟ,9_cQ聵BT+d@DYt@d@DYt@d@D=!81++=298jf̄q #=tB=J^dܿ{OCs/ѝ˅V~-\Ybs𐽇쯦'GQ# M" qdkF`"sw޽˷GF=8E~Ɔs'Ī4=o.6oCpYdϺՂL7jQeA2vK5,B\^t勒%vo˅j" ,: " ,: " ,: " ,: " ,: " ,: " ,: " ,: " ,: " ,: " ,: " ,:d198{jz"diն쯦'G1R&Ø =%a :tLjdrpХ F{,B/G& M" ,: " ,: " ,: " ,: " ,: " ,: " ,: " ,: " ,: " ,: " ,: " ,: " ,: " ,: .g4MP:DM<<y ].iPU]ש}'C.Y VGd@DYt@d@DYt@d@DYt@d@{@hӬ'۵G5Pg̚7gw3'~U?p!#e㭜W|KʩEC>x04:opVqظMde5ɵU^-fn")˄B|C73 !-5YZE#_="%ڭmMv ߻6~lvvFlLکݩ겗UVkժ9L%GGGI!dYr&d_3 i*,cxmY|΃ IDAT&IJ<w ~_Q"BH&ߖæypYJE%,"$,w1!A!ԒGZqV[BR?33%S2m]UPڻ+uTp܃;OZ4!9QPt#mڹܟ ԉsEaDn,!`5vXm\ǁA>OLii)L;Q3r~(g^XqYB\MJ֚Foe,8WIE ɵ!HB<*33Bm7n@"hfˏgmy Kgd~{T]> 8,z au߻Iadgۑ}wiN^Oj/{}ձK,ZͻNIMOȎ/<qda:2r杗^cؤτpBƓyٗ4u}:h"yޟn^~wZOgx{3&Xo pӐ4Y*G71{E26_<4_](x#W b]bDZrG杆O)$,1%q 6v>C[1O%x/f"~5h>}卝Fc=7699om3j7jK-IO>rL['ؿ52rۚz8W-$Y}ݼd2 Ś{ábU(AZKrRwGq칯M{e3wSԢدF>v!5WP/* n5%-{{rWG 8oo%S]'7 !':rjt5'os$/{յY?82KV6(jU5al=}Cx1-w=Rp'7^)U%Cwd.Zmy@q9E=!}9sO.~ 뺔ed.?NMUfƭx} Y90ȐԦ[Wߣ?v!Ʋ IG3¦Š^ oe/Æݙ_?JF.Ef_.k_u6\K9`tn#KeI867Mt qc%ϕpԾLUվWOmWRG`_Ark|˓ wgN=}QE{>0n57UHaun"EdBi=e7ostƥ2];AێnsT#o~r)2l^nk?`>LBX/F=7eۋ/Wrks/c+ΞjӼ[V|.`rӸ}ԝu wn-'LJw~n7Y:\brԪ6wbڲ,u}rf<ڸup{.)|hRoI6#F{"a wpzѰ_0.[h4{POMTOo\T#y幧#unp0)̱3VկAz EIʳ_TUlf޴OW4S/?¹ό5coZ@v+B:::L&EQ%our?j' Dg{" ,: " ,: " ,: `g=P-:GTa,: " ,: " ,: R^h-(;--\gm^TBݳtv5ʹ4{@0v7l?p&Tsϸ4*ҷ~\S`;' I?];%%%be >7_@`]3?^r)흄Bֽ^yӍZ97WB+LCZX7W+=gQ.7{5ɢ?WÀ)/>:.~LU!Vzpy=^xJ4Wdn7/ٲN%qPu l=x~_2!>)b@"-,T(,Im5u|rŦC']–y*ސB^6V!V=n)eND#;W!I¬<e!li5Ek\٧isĔ }@TqT0[nRE5Q–gnȮq־pWgHD6 7 voUB2oػW&2dp ۑ+=c'7m.I߼{]i⦥?3t{}jXT,R ,KmZYaAv%^Pg!_Ooalʌ+3Uj6c%!DCˉ=W4"gC͉=U#UڥLM}d! 2"}2!Vp=k^ZE}!l[&\:zy{{|(N3Y]p4d;^ܰ>lY'66kdL;vȖw:gf&lؒ{T;7beG/˵ FM}d!*R"*U%%ͳ;EΟϕB2̧ӗ&4+\:h;7>[TzQ_Rm"Zx䃷}Sz_^%9{8Wf&EG55Zw{@EH.oT'J8,LG [5i[[sp_*4 S "E'R MF\.Ğl. [flly`Ӆ6:~nWoQ3U%2KK9{6ĪV^L?{&%~r:>r%]4UB童Fǜ3 a-[ݞ^@={(ytAΗ7dI hR#srla󺗟^4ȭ['+ӄ=/YBŸ4kh(j)j--zys՗]:v{!?{Eqlɦ7 ! 5E: DlXQ@@ҫ U4i @h=ݙ"7;ϜY^~qJZTgNY4w{W|Iڎ~õ|ҫAo j=Z| GȒyV;be$]|r%lY[#[wv(;/Ot!Kکy!?@ >^[\O*φl-'jy,&|a9ꚿlʷ_Ͽ r`iW֖6h9CKVX("a} ɔE~_a H"dɁ,[כ~ݫT=/EDW.f×3~!ewԱz7OO;ˢY9z%6;L:z)Y{>ٕǶ ~\׼ ~s Gl)[A{y&Q/R瞫}qo9cHoGCK,Z43oO?ۣZEDDOAl5]x+)\]v['~ogL~d1 rd3ߵ<,kK OwFa*o~=1/ fȕ,b?۹uYoUDDsق.XIg;eѢ>]dR禓D~-(G7g֚×RMKTЧ?z"$]̛"8j}.g5[^,nHi[N_N Un޾gGy~jWwUe S^t:˻z2hQ+6xA̶Vl9r9.M^~Ak|M9'3 %GR}+V 1?w2^Q9n]w.tl^孈d^ˮ~`b]Mgߌ޺˫d]h_YOxآsk0~t1-T-ᗱl7U2ڕKg~u.lg|k|Љ/z^>,8]WIY_y&">< =+b$kFa5gWҫv_`KEg#0qW*6t.W{و kՀe]"j2Y%yzaMMr6S> IDAT~]ix6!h9ШgM7}/ !<;|U+JCT!P';e,2$%&QjvKa%-ڹq鶿ua #də<~&9oQ-_+sc^|**"P?{cv߮:{qj=a 5WhŲN|ݵED8o¾-)Y'ܜZnGkH%R|mdnEoMNQ˴}k/E?k*߷;MHOOEсDD\Ч*ghXyگr)]% |;K1vzgreJ0o뱮Eʛ ~&Eqrk-=p4ĈWFzaKXDLBѠ)|aԳR_5=ub@AD2^rX6 )_"_#V/Ҷd<ߛHPχOw ;|Bܢu'N6 NIk?_i))R7:>"))%'?p{ =ՠX\]~ݠh6I)ʄgzOuoMIpr_&T4#>,۝iiɉV-zɨNf7M?_nuw1#fozf)^~w]o_o6 1ӯX3sŎSJy3SEUoooEKM!Co n9r:X䉛* (\r8ʚxR "bn`5?z$/$gC FȒ#iqv_-U '%%i3,ᡨ^&R|~+鸇wdYZA^J1yRo5uBDEus02i{Bo=.y_01lI>&S~=?=-5ѺsD/_OŞK;ci1qi]yѧP⥌"eBˇo YhƮZ#kUbw/7IKJhiy!d)U.yɷlwCO?q\C.Wtlڥ}7+φ7f[O0rfּ܍UTD+U&vrmWo(A%J(b)V1zci:T\>s{Q=ɨG--CE{//x?UQճ^ag/+e>i{O%A}]kVohq{wFz`h6)zVUьEK49xS; .~׋vV*Q&*"z5͝g3sXCP^֓;v'>׌[0.s;DTl)ս70*W(jr%/_=R* _ɫ@hg[Vrϼ|1Q)x<ɒhWh̤WNeX >p[b',]|֫l/ukUxI^!F%,mׄ3-;2S駞?:۴nټn뗏VoA/{~ټT{GS?ϯ/}6*3f&]9}R)i-u-hٷpڲ3]< zw'蕷Pއf,f~H|3l:pݷƖ1od[3T3U"n1MeɌ?ej {:ҝZUݺEߪ~kKwMq_bCB\Zt(غf!cuV>ջnw<&" ǎ+6 5!$ GBKeF/ƞW|^ WIQ.ѫ@BȒh7~8gEQ fw\Atx~:,ܸp'g5n7s Zj/{@pf\c񧆾2y mnv!9H'|-[v% V_@Ph%eNv_4vcW7=,b.;cv1)㗯XDϿy^fx*^ِGNL!cϧ,\ч 25KWfD,y1×|C7wJIrfUP4zmEKjʪ-WRP'!5iͥ֜L)cv_Ll[Ғ|-Gix)tBz ,{Q|˔,`8vHhh*R6Nвb#X.&}냎OEzv՚œ82 ӓg)T_gnXf`# d̓.nSt!oT6t8! 8! 8! 8! 8! 8! 8! 8! 8! 8! 8! 8! 8! 8! 8! 8! 8! 8! 8v?ֆ}nNxg=ag}YhBú3^I]kmO$R5x)"w W-ikY""ZڕWSel\vsߞ{װ321Xqw+f---Jz K3fAb[G6nɺmcdpbn|YBz1n :YrŽxED~ F*wy#W"Ju>v>o{Cd(Ԣ]gߌ`6b;y\ŭlp+r{ӆR+Tbnl+^ C?lBɃhfۭVkFFF sfo]~k]G b׏xT13=O]b1AU;ڕi(y`5vF&yJ6{O2leo]C"Zұ_ 5WP/ XҬ]:ub2O`nY3yfĭc&Wm>ڕi(w;"+;W~tǁs1)PɚlRCѢfzkfQ׫G;j)P+ZPnUyaJJjfykfx!Msft4F'\ӗ__Qf2Uy V,`]eaxc/}-^xĜèwɼ;'WT95}ͱGM;:kuS0zѫv.?d *VjX WMXUx\.b_DђŃoƽa+|zGނoQϏlҶt њ}ܞ|dR5*f-~0{牛Ƃ+U/us#ѶągT_-%z¯N{;]xŋwɒ)^Śf͈\GzK2_2=ӧriG?.?ؽtuG>˛gyj&ђ/]0hIszx )Y);&?`ځm6I.#\~n#/K<6V\9jԨѣGٳg6lĈ/k' 3/mn=ڝdAIMY y+WEĭxbU33.^W/(Ofv]Ԁ:OW˞Yz()"bO<^Msh ѩhqh""U DĻj풖;2.unU)"y,Xt]DDzpcٖ<7d1)pڑvX/XhxO#{kbUE_UԴ{lˇׯd,gׯ_IXYr:L~Ys ӹcx>ׄӶ_vW6{,P#zɓE)>w_W=r#i-""bru]*"."T{V>6:~˗nppǑ/>~QX,D6md,{=S>q2.G߰BZ>ۺY*ri)ᄂEBv3b˱,]K@ã}z?.nl]MDDK|]W2uQT:\jhQ7Y32V )Kq9KDKܽd.%D?)jwa|d\WGĺu=v̹I1mڴݻ׏QttBAKk|bTkib lTwG>Ԡm: XmPfФ+^NIܾ Ӟ~k(i[;wA[=x.7t.^KmP]􄃻N{T}lDO3Ԩ|!Aܕb/>>"2zhgO| zPc졭?nQM(>[7-fp ,vоe-})U^KMҮG]Ks-Pt>(u^|k֨mkw] j\Mvsѭ[n}ɳ7q# 0D_}C| I rEH#^)\!BOѣG?J^4n[֌.=l6[RbR)E$ڕi(w;6x=uӨ]o!ulmF~ׯ8Y4O]E Q^^F#@Nم[,l0u7yX@Ou+ӢOL\XMT9ϧxymw=%1F:mS鏦> dyjZz_ڎή{=t,@,Τ*j-مiTUم@F0k"gAi~k C(D-p]9E1Kx~ì#隳CUTMQzg4 YAdqEQfSŀ7,XugWܟ 5l~ŀ0DB's(b0=ܫZڷtFLMӈZdkX\= t8.`puslt!DFh4T7x BQ`0j2u]9rg9!  dpB dq6 ۟k1TWe&T}b\-eCu-joj?zo0G?M)bND7 ~ߌ:ʻ0BoYrhtwz?đ~_޸T%mqhgb ]TAհ ƳE9bvDfyf!O]șqǝ):ߠj(|U*";8|8U]kF=5+jXVmb]7gڼ$y=;|Ҫ՗{wO9`n^J;~:R&B%FtΑgy ELk-W*y~}|-}?ZCUEbsUǕ 1_߶0 ϳy;լ箈q j-줄E䈩~L%^9q$5;,91XmZ;rJ^k9gGKλxu:}qIvg0"Y{?k7~VMb|:/K>ǃ7>AKyeEZ{[KzT͜1=ϳԕD=Tݺ b*ZN5`rݚ^sױLRvQ9)?F]K] ۭ[ךyM^'{kYl@'-jHW†,R]#N_ Y<!.;iף.fԲD.WZ/' LQ)▧l=WDCp,T{ PEv‰6aSMKz)Y;?~ye@'loۥtr݆آYspZb)dӾTe&$؎x ~7`ぽK7iöpWg}\q6_=qG+oc\5!(Kri `c\Jҹ foOԡ'rػJF7/tں{h[tGTmөF`ePԭdړ{i /jQ7%[wm׮aeN:J~)G9q/ÚkV(Ɖÿ%R EKx[2~%~=荟ͥkӖN9eӡc~0kg} 6`آ?HH1 gے/o=Gl枫Ғo׼9]չ8ne?ê=R2BȒёO=}7Ru1kjiYeS}cў{c@v[_E<s'p1hĻmZ@5wӁz_=/U^bv ХQNn{=kw%<ӺM5nlZu(ފ *d3NؕhMEjͫ+|fNu׷Min=}UTU7fSlrnib=۱v{#PVu]O0z`9o5veZ?[/MUBfAZ7 qUM~U1\tSۙb:u-gRy:wh[_nUj^=(5WvsUbGEEERu*sQUjyFhx'ӅrS'=]HDD]MՅ{6 4bK՟'\=FJ332n,…,顈,V(E$~"Sܹ̊d$%(kN ݝȩjYyֈcITF.yzg4_icmd-EQ[#dv E;ᓾx'[ZPSIUOzv:wlg} RTS,Woŭlp/EDtVb;kNQ.`Qĭ&Nћn^n7=uԁN+H ^=cS7NX}L)dPk 9b֫nKvl&KԪ{73tѬQN~KgdZ|DO:ˏ4 %'Г#EDDM2.8ާo^wWߗuO[s/P?}0:mZ,o(žM*j~ZbslfO|+s4N^eG*^vGOuFˇ~Un{v3BOxj˞u |sH,뵕Sߜu9.Su+hX:T 6uǽǦnyw}xQa*>2yݣtOP= {CK6Y\d?yx(:$Mvjqar}^+ 1;S#(VUr.bf 8! &Zwuv\!@D' 8! 8! 8! 8! 8! 8! 8! ]ݙΘo^kfk8ťR*z,Vk_DLgNzŁ+kfm;frӅiY$,;sgrvYiߞ."dq&X㋐YYr CsF  X4g!KuZ(@,@,@,@ȒSjiC<.g?7oHM15gW!KN"d dpBǞklؾ{/6v1_{/W~jmn/wrOw!ݺ|*b?7.}'t-ۡCM/_AS=$޽g.LEĚyd˚Fq {횮 !WJqݽYORêYe_AR nۺqmsE=,zҖ;xkW39BCY_o4qgzݚ{'*wxoyia ࿄1pEeY~E)PS;iɦÑ7ug!Ζ%n Uѭ,!N̰G`\_)nT=3#&%,;[Ia+~qCK ~ӹnO_A~F_~=KD\8:%5$࿃I`Or!ָKqN-" 8! 8rC~y?<&xqlfxfsvgB'3ʅ+o_qB,@,@,@Fg2hJ bpv)8Ss^ʤ~3Vsv5~̙3ѧ) ŲOSK.zNT J ~j,z{^!g|ڟ;vzә..וQW8UV2XϜD<(|-<紴{϶p!`Mߗ)&E%D ") N"Yd6kdQqy2xp OYæ`HzlzwPݦ7,ߙsfmҪo tǠ^5ͷi֏.jgf>A8VeXIfMM*iDASCIIScreenshotWB2iTXtXML:com.adobe.xmp 200 507 Screenshot ON0@IDATx|Oz#=$tzoޫ*ŇRD@@HC'@P$@BBBBH';nf&al6?Þ{=wī_0bhzp`L 0&4<ȓ 0&0" Ye0&( `'LLL b 0&0N=FsUy FG 77e>ܜljtWʞlgY_&P $SYȗaL@ALM|;0%@= pϘ@e  ySKzq 2s`L <ye@`rLf2X'`L 0H@i=TcfL 0@@A*\j#`L TUB =V=6Bvvv[oKCp=tc= *9A..0sLpw7Ǝ [v[4o?#o?`6#(?&5jx=zֻP))>Eu?O`aiiSMU1c^>}{CӦMD=W\† ڵk'ç|Vv`' t_ZKškeAK+;S߁ppeՔȑ#pqk"Yn*.]ƽ"HF`L\3>D[HXm{3 4@U?w}^^ЭkWppt`HزoxyxcQG?VNr'0gcjؽ{t K!K5hRI믿~ AOՏTq-`5@ nnnXu@׷PQڦP> zҥ|w.]QÆiߚZ I򹃽=yQVN)87WׂYڷo/~99:(!beh}>_!AOj}i㦊[mދTN< IIiOVE" -SKHe7}4=K-j,LN0~+0|GY&d $g̘!-M;sZ1~xh@аaCĉ>|p9rrl޴Zi#Ⲳ2 K.-G::d0̛?'/ ~F?VBQ޽ONJNJ}A“'rܰC'0rAKŋrzuK.`aa!⒞&; }"| q!8߿?U.ggeAPPܼuKз+N05k߇6n' -֮3~WN'~_,|={3$\><4|bj{Ӷ];锿cbvk_l*K45f~~!詬򮡯J~}e%={Rː|?I(3߲e |8Cq>ĥf"п-?XeP .\Mwwd#-Bs} AO+P9pK+$P֭H?%!e􄸸XԩWt2褘`t*Huw}SBB* =+PF!F/?y Ο>M/L@#w40jd 'z3 R G>i _e˖/h^Bq%Xx>j'U` *moI /[P {wګQ'=j a{ ߨq#1 %ecFS;Y(wMeFww>PÇESڇ 7#~`ii GOB">)9J -ܸqWxoi&@T$g.=P  b$:4svvcǏA wpkv QB%)~{=Xj9yklSG:6u@?yJrᓟAY&M5E[\g"phҤփTƐkZ W7y=##֬Y$8qBi$l(pWQACMY5N(@Q6VBRbq}']5aBk&Y ONN ;RRREqD /C2S \q\\j$5P?`-=} k gΈi&Tt@ITg47B Eʼn|4qؔȹ&I6$ǐh={2 ii"=zsgulV쬠)GZn+R_XZg-v2ի״^.y$KIYMeH8}|\ۢwu~CH֭ASru4P[kC \Vrfa.LO_9& TaRIpZ> w҂i v與a2B`%!N7+)7Ѧ*[ӳNX4cs{NO,T$Lm-.)fIɀ܏B /rr@ճ8qa".Їn ϟ;7GyL./B_ $EAMW^ ꁴlP[nbK}*2G?@]v$9</I\Qk׭zm=ܾcgt055P$MW rhW d` ɔ|Njvdo[akﴖN~Ү%zfi /{Δib<11 b-/E Oe> 4N@8-!WExdeY*([XXdffǖKJib™_F4~<+? < | ҹ~j-$[je-^X=ƽ6mVc&vOҚ!WRV^ZC1h\]zqLqd.={w9<2租ţ?!C`a3Xl(sI MvvZ|Eo:xmin@ 9Mƽ<[XhMՃ[7o =ig/T:v`H4!ƥH_`%wA(s Skǽ A M>)bq",kR]bO}Ϟ=vmRЁN ԇLdK^%"9&+X'Ǿu$iة;8% P{$ hR( |t"TU!>R^4"$ZC۬Ak'A͂O>ThRyf%2e*aOuE/BgG4FbP$/6D/ց!^jIӚr_g}Qy] #3H̲˅ WNBs| Kl22&N(j/E_h@,~r{@m[xoCBBxߐH>qN.Va2G9o\!c]-'TtZG''RrQ~ѣ.s4mjZp֣ }MVn~mcgX&iKNJz 94E3uŬ/z+'[׃'&Ҥ@0w="^?ij-@iGx睷ќ}ؙz3ߓG7cs $?6-.Ei+{ڶG$}quK#O 4h Nm(M*uu}RxRU> j=/6^^ÇkB F?2Cvh/*mɣ@|O{p`D@Wa/QKDM͇?:d"eJN3/66¾_K(7R<2 @o >2>Wo|-2s&0>,p--we FdZsMAV>bL 0&*#\#>3&`#-L +=-׾scL Y180*c71(_SskL DJ(%t&@ag3~a0| )>;NqMrULrޱr]8m" >>`L4 <4p&`L@ 80&`FL@goė`UVXŻ8fL 0&*?AFX?,u7@`L @7ڗaL 0&`5{\%0w 0&Ѝ@ftƹ`L T*OL 0&.XB2&:q`L B ͞ 4`L27㳃^ehW&`(335ϯؾ^laNR<(//oHHHv)GO>Kϒ%>||| ._jocE,,,yZ-L={w}Hg-Z*_bsY;6m1&^XYY Ff͂m´iӠWްv:1e_`ܸqn:>||,Zq @Ya L~ 8y$NPf/,/-9Y _}56ojB6k͸;Sڮ^+"}{¢lSF9 0&4/rӦM ,, V^-p<Q,DFFªU!J lpQ_N,Θ1ܹZ²eE R8L4hcX~ܺuK`Uz9sӦMQhoc ŋ!$2 J?,w׮]roHX5k*ήx"r4 B_W^P^=oss hԨ4%u;77+84H͛7C޽YiݻWTj*L=И67= }lllj{}aYr hϠZ5{>s琞"&Vo\ :w"G$֞5q&@4*Ф͛7>PkH `LfmwgKKKa޽;dipd@I5fHTx7CpԒW-Rf &M+j^p}S߁a&ME"&T 9@ݺu 1)dggk%쩢8Q7 +W~Ғ#8]k׮hWTPF<<<`Μjk' aÆoU&6tUĝ;w>cqٳgQH7ƥ  ֋z/^>!_~ȳp|A'Z$c}вY`L@a@&zFwaĈᨍ';ݑ>ɕ/r kEg}ZWr* @(*)Y-T!|Pܹsdky֓%PfMIhlu{)W< ︸X5s}q$Kr^>fL +gAf -wp>#S?&L:y '2]j0/^O._v 83+"ȑ#bS ^41!tr22ɓ'@Z) xLaah}֨iE^<@^A(|(VD4'++SO*.mxC]\ ,#VV8IrUď|z)N}WNӦN93FO#\|0n2P˗/t=*N6Y)4~2JӦ/э5RhN$0e2:8?XE _I,@VhPvӧOm\\=z$rN$t|cu9,Z`VJ0ih|~'Yܸq=zD7¼OiDKOZqy)Y7A5dee N}l1)9t ]"yhN? /0KA#ڕA[3i߅ D^^hJSvvv"]t%Œp"q֬Yꯇ( 0&`<L<<eL 0&`lX0&P!^2&06,x`L 03(ڃ́ 0&0F=F|!)IVc( 0&@U%`nBU' C> `LP<ǠhII _۬Hj73f?. 0*H[^y z&7jWz򵩈Hj73|90&ʛV^Zuj/JߚXh7F6UixxLLZO64b `L 0*F?U-Gu-ָ)&@QBcž(aL 0&`$T---!3ph XXX@VvVJl iP4kkkHNNcC꣮}^?<fcǵ>tTws+q;ͣhkk6 z̎؂~'emC㽔Lm:G2&7offfGsGFbI"1)zܺT[GnТE ѳg`׮=Vl#z b ab.cǎp"Y֩S7n7ǺtgЦMBE6m ޽[(N&N|.]&&0ODc׵:t P>M6Q zplu47ph6hԴo^E0G+)1M(z {8ajն3xz{겳!LDs3U3 B7o\x8oG&'upг{ƍC_6r HIIѾe=|+W0?DIi]UV 0 lٲX~s˯ITdF! ]{;ffzpRZE~b iypq:h⽜ZlS(^:d Q(''\<F&{ׂ,Ư m5C47V:mESm:uG'8w(FE&>v[ovp)Q,wd Pgo_MB$V l Ǐ]; N<ݻwpih߾nuec_d򏍍 j(@W}"Jmݺ Fz޽899[aw>aI%ԨQ…`8q"HLǏT/"ӹs'8x𠬩>}Hhxx> @}4U]vmݻΝs.\uz4hd] ;  C,U۬xŋ]z`"NQիW֯dyW￷=ǏCФIc;v  4@L>IH5m=Jy4IO %PwvqN5Nt07k] a^ll=ܻ7CK{jB[# 9|_!'7Zb.SNPAx~ܣh|4Y%JHHx"Ք)o JރB Fڵ`ÆMh?bR!iݰaaƥݻwUQP~tH 6n,=G XR!C6VZ_[E۶m#mnZ]:Fڟ88(,"K9tLܨsHLlݡY"Y8HP6; ʐfHZ[&&4Yj,۶m W&*4q3PZ?$I ;;; YbQ˥tq,_+ëN םϜ9',(jbk%*nL4gϞyQGq7ƍ4 V>F=&PEoZ XШH=zLЌY[hT! (: (/// #۷1|Үn:ZCdaa.~8so___lԿBڵl{bbb-KPi'lsPMB> BIR-Cm۶ 4I"aHuHA8mtZ}||Z7@WRpŵyZGc\'Ltԣɓd&Gn .b@Dܹ)3- hQwo{wÄUCХQ]sƪ!T.;Ypخ3T[\3N@k!aɘL!{MBˈÄy>66NfK… hW89sV _C?􃟎N>R 35 y?,yy9BH~Kϝ;'e5@Ν;r97]R4w}\*LMKA8B#oԨ!GX]HS q?r(K5Yh9B݅+NL\].ޣt}AM, o11$EO Tj]"RϠAD !]v/Ԃ}Ągp}3mG;pnH3^"v( :X~<%K*CB4xoJJpFM(viكƘݻwͼh/|Ҿ}[bJXǟDsj:0S3v-orRN:&N<[ W.zLw s玪Y9۴@KDd&Mj Vd^'G$\" #cдk.%qV kLI'N,PQmJpk׹X 5j5~xѝ*d%qNxkp`L"rr#G)҂Ih?VII@>}>NpGA!c6*iZ.]?ғ')"g?Z5[ 'OzLիB+6l0޿@lڠ~$իQK&IR aR@Ή$,Hp@!M|߾է_+㸸8D0O)T8p|o$svpD#!o>ŒL9g+/-`BQʧ7 Z|ƍ '6'|DSdFVGD]t?X'1k*d.?HIq֭(Z. +t#x+ߗ4A ˁQ2щ(a_HV bmx?|xv8tz MCWÎ;/4B\ gD;hahK($,P<ЦCWܽU!]B'XrY\])P;5\>#RI yqͶ׭n5kzj &۷ry]EHc\giGIuZ+#aO|E4)M-OVY`V uוԜ^4nP\dR0+uiT+)=NcL@7i]R EtgZPG_נ˯i)[5Ή!-QoMō4eO_iFOOKHzj^y]=K04Q9J' &ʖѼmKX;Vha%dqdgSN$j 8dp`L X+f_&ZȄρ 0&Pf|ZfL 0&Pv[:BЎ佫!̢5):R5ūXh7F(UixxLL<Q>W'5[L6&%.TD~j73RlSwXX%>0&P(} kϼ{˂GK!|WeF{^!\7`_;>U~=D=`L 0M@e0R`L T~JUK#`L 0&@7>=J}eL 0&*7YWap`L 0M:q<`L 0M=Jzz<~MW`L 0!zWFoqo]glsk[;9Y3Rn 0&@%Ȇ 6Yp|;PE1`L T|;Rٳ X"ifBvn>"ݣIW?AWm,k "9 0&^ ׯ_ڍ7Ԧ{\4w$&Ryq|`L j%<<{X <5jT"Pd8jxxaw@ס?a/Ž[@vF`L j֗c .:8M!cTaGj&86!)~vFdk`L 0=zmk$_bBNfDFok_zJwk #~GLSL 0&@z/NWc^NX4MkVWll/gUh٬P 0&@0{2h:$90&`O@Ͼ% Dž`L <;-.`L T፯^fL 0&JM@h8蕺%.`L TŚ}4͍2&`A@f6`L 虀Bس_X:&`C@!x>t{`L BQρ 0&0^b1&@'f|+rofi<| }kk]<3ZCGEḽO,4Hjpv'+:Ot*tpStMʹ.ȥt yP[ڧ<-CTa tM80&`<Ќ\G_[V?zvݺxz>8q4E4C9i$Ȇ!宑W{"_>:̔FcN e7>вse{k0Buԇ.TF'oWPt- q'abѢi; 9g-&n=M\/e]j5Q gb \ 6VjEW6z$/0 j@%%(Q먠p}:{-=u-S$ͭn hڽP<=Ο W]hhWc~zPG Z[9 V9151uGVI)ŗ%'m%\%y:lvpPt8uNs -ғ(8ͥV᧎E:ׁx>ȇg z{w* .-'Ԙ/%3M4QM罢Z73&`LHTp ZL|CICZBD7"`feqܯ8sk[6c#TsSsKpkYiIplŎ~8&Nv`I߁W}8H*H$S]˞M5wk~MȴG-, ] $`t-SDr, Y[]R8{t?;/[gȳg 3.ˏ,J,INSg0[Vû&@Lc[k0O;ǂ(G߰^qvqam㘦Z~? ]=Bk/)Wm>W뎄[O!'/f_<$y/_w&W+n{IQ6y+|Z ybq o`_OK#yv?8 '5U^A>=D8UYm i9`E;+Z5ș?! 7zn_'6CnvS?Oà)r\vF(dԧ`g;WNtպ 3Kk1kD)8|ֲ$Pr2aǽpqhS2XAk`?a]cRV2L SG =\*tk Cpq1Y~aL:'Eb4k9l7Og$„#[e7kg~ ]wvbRk?:ҟ7ۇ7B fhh\z:υ ^ &'K[ucW.'rqrHBܧWz㮉87V.>* #9X}s3> G 沰r*{C3`yJ\. Bأ%i ır)--))80)RFq!' bMQ[oUuҤ r23[^2^ϼ-1 5n=&}Š=MzvdAjt,]j 5lD# c:0+8t*2ZBʤt Y;ͅ@+&VL9W¬sM$G$HP8u}1PW^YԿUdƜ q(l־:J#ES^sgM*s@!0-M-q"e 9OMM̄A(AfNECK-ކq++4AGU9` {?C&h9`&~`fa wW6~+GfX8+2ʯpz&6KyTbRpj_@`BR?yVv[Dm?g'aBj@m U$C!PS҄BwLzh6nI&HXLNZ əah_pkTo ߧUDN MhĎ?HhX#BE鏺{EJo&BأBnu۠ |-F~:qar#aPBJ ? }i쀤0pY[8torpfgUɅx@?/w٫G>)db'm{s>pd_ OIDATGurfڮϫ$3'K^J>AkWm-?к8+7vG.Bmfcjx543ު?Vәk}]kT~k_dW2Ӛsf~ן܆? {/OMg5Hmi:Tׯ }hZSX@{, QҴWt_ @藰_S\?%h+ݏ&|d#ҽAH ]<ڈ{EBur:3&`|L<}jDC} $$FÈu,i!!@޺*k] yI@Ҥv6}H;'W-ƴ.#iߺM\կ.8/0fpyZM'@=u?ܥ-J2Y7'AyLl_|ΗkY yiL-qP{4߸ʃ {H `L*@aY Jꍕ#=`ꍞG@U'tV1gU> 0&@y@a6m0&(}E52&`eM@e͙gL 0&Paf kfL 0&ʚ@U3&(BW~|EueL 0&ʆ+{K;GuUElPpL 0& { e>mA_*Gk{Go{1&ʙ03uʙz1͙Yfת\`L@{/1L=v+s!/;̬l!6>pw٢7C6́w6o.K;'pn9 ރ'4á cuxpv;_j}%~x ߨ=A=v{U5v~IQ N~`bj1`app 8vNv6p p 3=zV]窘`L 5{Ct#vR8$|QbŁXz ;/KB`vk_4]96n ѧw-菅jvr\yl ~I4{|YX},)L;ώAX6ye5 ?~+E¯!ެ:и1&@]/Qe?zQ)~sϐCH?]1quz֋Dk- ;#UꟄ'89'wCu\ڿBSVX:H>`L 0&0LMk&B~/, 7+S0y SS5"Mw =V >;# rs(~X_9ي1Z?skO&hIJU=aOf`L@}Kb5[Bl5@y:=uGg q7[7oQ ql=~a/r\EdB^nxT߼{ųLA@p r[ ,a l0U 0&hv,^W(ilG0"-t,]da,1icK˝b0S\F]}x oHt;!ƣ[蓛#g;2]/&&d)% Fy 0&OȻлw 1K΃dfW dT*_nNPTN\!+=ŸBODY80&@"`,oJzo+dL 0&*@3h5n 0&(¾l*Z`L 0'`OЫx4&`A:(`L h$F4`L8(=:7q\P`L 0U a썯ϙ`L YpxL 0&*y͞h 0&q@a5{6Q0&P%žǏSL 0&Bg\O`L 0"5"`L  ؊o$`L(h<0&`FB ߌϪ\O`L 0"fozEp`L 0c! 4{co,`L(:`L 0$ oG`L(4{|70&0Zb͞`L (4{v; 0&PxdL 0&<Ś={W0&P{1&@'Ѿ 0&ik{uydL 0&vk|30&0Zh7K`L !fw`L 0%051&@' {㳟^xL 0&`ю`LP{^7`L 0%{mydL 0&F`L 0&`߱7_i`L TY ago*{`L cL 0&Pe klƯ 0& {wG1&@U%ž~̙3ѧ) ŲOSK.zNT J ~j,z{^!g|ڟ;vzә..וQW8UV2XϜD<(|-<紴{϶p!`Mߗ)&E%D ") N"Yd6kdQqy2xp OYæ`HzlzwPݦ7,ߙsfmҪo tǠ^5ͷi֏.jgf>A8VeXIfMM*iD'ASCIIScreenshot9%MiTXtXML:com.adobe.xmp 295 514 Screenshot L@IDATx|Oz#$$ t(>޻mϧ>g/((J ZH $H'sf&d76ɲfgnߙ=sgj OMMAQT~BKlۖgTU>)Q9s|Clgˁ d #lq.S8Ï_. ,0*$`rt8{{ЅAnn% n(E@.'&,?E5]A411J4"C-V%t@@@8% TUh@@@J8%[IW &G(PM=ˣ̬ĹА@\Mݐ @ ~@G `Kn粏c pM\yo~G1B.zl EQ*V^qo!j0O@ J+_  6J@ jmcX   `@ ݃!@@l@ `x̰͎;#E=ݻ[}@LJSx7aÆ /H5W_%oz^ &д%K-2ȶm飏?0͜M2^j2 ANghԨqt@{DW\+F*tvv!WgJOUUƏ O[GVXEsU5jH?0C4i-\\ރ~m%|Y$,'NE{+W2]6l@ϛW"U޽{iĻ~Ȉj'*%2<:lHxկGz$!iʥ41]Q98I;n,G:g';O?Ϥ%KS׮˯*2ʪ\D"pDoP(!@H5gAFB@@I4x jZ$MשSQ$vR^=߯Ki={-Zh~{Bnn$)x{SX0rqV\= ~ԪeU丬:խKr^F맟;صk386:vⲳ裏CyBիJ?eաe sԭ[7%` Mѓ+Ma "DD)ɴJemF(JP#P4n!@B30qyTBE{g@/^$_ `@*M&E.A+N,g$k' NπWvQ|Mm fڕwC3fSZH;t`߈4?["&MJ|U|ll} qZqc+[jTZCyLY}/ZH)ʭ\R}K200HU˖<_5k֨9jjA֮f͚+ۼWGTZ?1\|ʪC Yӓ>LkS,[lVyzzP$_@~)eV5K/|dҭ˂#9D(;'G {VqjQM??:u9r6M̢.F(o\}|m]W{ $R+DW Xd7 w5k[4(֬E1iDV;Q$PJkwÆh/D0;w6>UD9}H1+Νt~t2=N>H ٴY<4dMEQO5j԰QA$tBO*ti%tfDt!%ԪBA#6w'6ıB gϝW. XDZVRnu -0шǟTCɓvBBBȭ@]楟2!׫W5!4N ϵ3BpgC_|IiD(Y&߰7oξZSESRN6pz>E Aj )d'A !`mb.aKYQX huޝ-^rr'BoNJEb.4%#}6q'ok*\EJN.q*ȟ c SS%;*G8`A+UV=Ů%DU ,8XUGӰTT%@#UB`-[O;f/ٳftmzDCvvFu2\5,GRNN^|̹_ h6бeRu"kdճe'Xbu~k8z&>ryC —aĚ g.]6%^/T@p#jNͬa{*"fS&ijSY\>$?SǚiHYH>/t 2jK4$ M]SVᣥK xܱoB[+@_ޫhDf͛f(XD޽ݬX4NAkש@&xG=oYgQӦHO.\W1<1Dg|T"MqXVj(ASO?WŞےf3fLWNz2ww7zpΜ>MUK˧OXl ` w?T=~Yz/FI_2[nV[:uB~ox>Ku$Ha]ޣiӦѧ~Y#jb=YJdo)<Ύba! ?=wmÇ Sm5b gP>{#C_weu &n[if*N8зo9vY9b>͛Qd(,dge*q2%3;. {U~466-uəgӞBӀn, ֕M7YNT&@멬>4? YA/Zh exf>K=_зCMIKY>fK 焑{$ʄœ:tғO:հb8p [NcK`r61ye6! 90Ro[JV$+@A߀ @@VW:wEzgщ' H}1 R.]-~/>}?z)$$Ȥ&km4nx/W[..Զmr孬L}'xDo/l_"z#oC5kr  `xu[]]M{7sOS~i֬sϩcW_ĉi4jhz7xBk-5kF|:?NjET $=т...yf^ޠӧQvv6,הə̢-77jذ!Ox0PfU _+}{ťrSF  p],kݺu;vfΜ:kN:u*Iwq _ӫ׮]C jB~,X@[l@>}- 3H4ǩ]4r(r?r}Wonzc}Ƞ^zSǎ܇O?,-_>TǎQZg{,(M8qCM%2cM w}5-d^t"DpJ8LLLTωqt}+RBBJ$:t Z~])+BǏ;_5Oq4yԼy )믳ѣE 3XvIu<ϧ &(3(*jOU/RŋڵkNnwJ )n]UyQڳg}l21NTZ<׏6mB-[TPYˣ+)mÆ SyԦM[x-[LU?ul޲ڼ5kR``P^O>4>} Og襗^5UF,|/Ez衇Сh{z}r`Na\Z D8{I?JGgyZ(E7 㭷"WWWݻ7LZշY%ڵҚ5i)^H6""ZSZXPyMS=cTT"H|ı-774iLii)''\Tt\[&QUcF,TO8pJͫ*?AAA;]V-Zo!G1ԳgOcvzD۷o K\j^ϙwƌi/u\ZVyN@ȉW\Fb2Y4 LE/:34z(^=;nQPA ZiӦ/T\qpE/A&G}'nc&L8gI8MW"N,ZzJ Daa HҴ0~Fwg{ɷ{4tz]ӫNK~$a:np-ЦMybVodicǎUye5?fX=OGG'5V͗;'(?GW\aH,VASŊZTŅјH&w}G`IaРAʿaTn]8*Dx2JS39hy;9kI&$i8{$p]>|ȿ"e۠7EGDѤI[o_M&??8S fH}YrJ@LDvZ5NHS->|ޭtpjV* 3&lp%d$ЂxK{R3HTH2J0W%Vß,0iҽl. b&)3jTY^O|,  =WEUG -ܐ`R}Y|Yf+di"ydAUцD?@@ GN"A.&  %AĐ@@lumc(   `5v=~ @@4v}1x{' d5pvPB@>?]dxlAxA@@^ 8;<>9,>m) [&P' ʛ||TyCݥKiu۬h[kY7zb. E8;8z/F=iW{˞q?Z/ Ft~##Xî2jꎛk\|U\⫪_Ԏ9⭩=RP@0y{+3#8K3NdflC󽔕i2 O@mQ۶mi~DnzڱcgL 4z( SBMZE_y%3%.O^I"4effKرcv_wBأG7ڵ+M:M$7nLZy~'toԩS"E~w-WU'/}J1/LPO65kx\ކApj:8J?0vrc8,?7:zjٺbځ1Wc6 IKCyҾsw __UCmsI܉]{~B$#=CNoZf"T\p oҜ6Te P1m <,YFIƫCӕ+W,ӫ rߟGz 2x~WHS?5f-I_kp!"mTv%#ݡCLhh<o&):O ji7_^oۆ s^ZuОVHv";$^ڿgzH©h ,_guitbjہwFUMu֛||hut77kIҕb%{xtBvnQeD'P5TDU-] zl׮=޽6lH={vgAmڴYݻ'Ϣ-[-t;p]9wTr`M@8OzA?{;hèw^GGe˖+M nnTNv]vƍTE=a&U_r޽ZJ_nݺdB p2WuF~l.c鯿$G >!4oޜD+!j|tWXԷoG <lZ8eM޽e iimNy>nՊv=*yD/_X$9'oV=QӖjf դMV*A]ȯf-%޶Q;tOM'#eF O9ڌڹj"E@`mYJBobo҂0NHtg}e  RS/IGQOJPBLrY8kl37V'?5;*)_[aʏy͕jX~,YBCZjUh_Mds)!@tXF|F Wܹ cǎjHd&#r+WRe_;nҿ^@jӦJ67DGA˗TedE)e t~q>YjmwܑMId\ RgL2Y׬駄D6ձe<-_syI;aKm;EyzzPݺJ0׹J `{=ITu6&+ڝwg7vJxUqŋfMZ`S/ՍA->sd>Ï߾yBD{RwGugaۍڲĤPp&$IGRϿ%ǩzEА⚈ _^}1I61tkoNo Tgӕze"oVd5>nXzԊZVBBUT&Ob.(WzٳgiZ&YA&qgbg|VoŊUԨQC=hQRRsSW)}qNN.>+"&#>BY*^F4 ,TERЂqHۢիsmϟga|U4hui2ʊע]H-"rE>R9x]0nLr-E(^Cطo4QK8q25ӆ\WD % hT3X{qY ®2MkE WVža Ops7@QiP+oou D^/$'~O#jSCy<^yՒt2Fګ?7D'\q-GN3{wllvХ;qa$XL0 "ZԤUO>&tFT%Jvbin۶*M&X |6 H;.A:;iAT"Ȅl& -qˏq;Sh*d";~^. Rq/@'(DKZ07Koٲ 4@ikd+Av:xAh OjeN,G`\]ng=kH 0շJ3/ļ AH(^,gذ!*FBϞ=ia>^o[owp۾W!66NI;[E+r ;)nmb%D<ӥex;RFeW\UZ>SiJxZL)2|ɓ'Yu&&[n|͜}'9KkY}d0a<`((02Vۭ߭jU+e/6)~,{ųsŗT DswL9"sy8m^bbFHbsA~^>9VmJ}mt֨b*ZAƦ޾~3E5o\R]UZw~SӖm((.ǕQ#څķ ϳaaU]iKV2ܹ]^Egfmr'A ;Ib޽{?CW4̞=F~aA9͜9Sld+;FԖ7 ׃NV8p 6PׂL4QR&d++\Y3)hJ Bh2229Oz8QT'SL˗/ghD8HŔ#݄Z x| C4_)7wubg3wqșQSݕ`Oժ~ժ4~8Z+3%7{J%&nI0+!!!})“G(iTbZj'XSy/.x֧1>8ң/ i{t+o7lIl6М Uf~$=DBi:DڦډfD'678L ܑkWR[{RރH? Lpv ֩GQ73:=G PM[h5ɤR  `yi׋A Y@A'^ @@* l(,yWjOLTn]s&(s훋NXyX\Wi@G S޽UkvMk?k#6DS5\"@tA5}׻^oɂmh/mόӯyp%Pk!.8\ cd   `tA@^c0&(4`0:_,l(c"A@@v vǏ]0h`_oC>s*kayISܜh u\i: ȭO}K {P&()<   UD5]-;\]Lv)33fDW@JIqCQܚ)n   PF gCÇMy<6"zy]<}ȿa$yq8ZJ#P3oA.-[]tt4I#~NZ&EN~fA>GӢ")'㪩P%˦MH&~-ȱę STR]i׏_h+IIa '昫   L,XM(Kb)7+Yɾ ^A+Qj5Y9E/*$,M `\8m"fQ~)=K~A   PCM~PTTT4/@@@(on ʃ@0TNݨ@@@ {׀Am1|A<*&(AO=x}A@@ i5P U@@@ 6 p 4f @@l4@o"m )}3F5B}w!x +wL} }pv!%pv+',ё4tPF0yzF \ep   `{ XTu@Fǿ^$ZN"ZцsԄ>*| m1OZHs-޹v޾"j5Ӟk%NJ'&lC*͇Ck>N/у-&(a㈹ԧ^W=   C ],ZCΫ&rt'2DZ@Tu"_>CHrYEDbhZWzS㟷QhcU7<'mvgI   Y:w j؆"'G~!t-5-#KR}Lrrɧhϩ8gwO\Fή ]Sk[P us#:M5uܜluTvz'$RH?%T'/% 8PV.^9 5qi(f7tbOWUORjEcߒw)H Jbd>zYd*]MITƝk'Աg^L&gWeJd.h>)vywnc(z*$.na ċpQUוkt(+Hbs,,L B[pG؁M)='SVs>fN-ڎ   6O 0y׀u4"888S욟hB籏Au'wˢC > ]j% @@cZhPCYeƫB;{U]N=ëHy ಢ': lэ{S=ZQe6ZN:M㕽CI_/2٭i;   ybV}KCo-Kgbh48㯗ќY5y_ISHN@Fǩ^ }<8CpHXEEEiv l   `G@VջҵJ!Z.  PXvwk T4  LhA@움 }=0v87T}`  J@ic 0nGJ#`͏Nj1@U`AC{]   `i;J   P  >h΂^!/~jTM6֐{PTR;]ߝrh kK͋D0&1|38 tFzҖO1M72/fRnV=I>j_m   `qJ#gAs 봭M.ӜUςVQmʏL   `Az=C wK9YI7~;:9Q?AQsޡTZgotœtAuerM/dkΏ~Fqk~ s5>UrLrpt X;R]?\A;B%aڎR'ۿL~n\ƅ y|Uňtm݅rҳ)v)Z?u{!@@X;?9} md2U;hÔ;E],4Irݿ:LP{PLJ>?H_M!)8&\<}ȿa$yqU}s(R&\P"H8J^H?.ĥQ ^ϗ:+!`C+itlY^KKYM ے)$  `_i@-?l5)nݯO3k$gхh64Hg#?I'("(*ٰ'PY>5_ҸEOERNU\NLiK^줴e1qY ZNE)G/w"~!>ʟ ?'M (;?IkZVH!A baGC^N&Q 6IDATFyYeɑAY Ճ$(BA6rZvr2Q^ne5^=p%*nO z/;cʥ%07ܼ]-Ů=EI>@@>8j 75vR~CuѻWj+|Q =)6=AܺP4+prq'Az֚ Z诎Rqr4y][&Zi͇6Пő`ye~}tl •^ @@l:h9?3.صm Y A=:d|ٜ/p,+µgH}/iw2!H<& /4)1ivR}Ĺx<)́GPɭie;u aU4%;BO?IQj0FI qԿj80p`BTZiGļܲVGu4'W':ͫYEԦU'9ӨO( ATyD>"V6_d~3 #hmqI>5  `Պ:;2   (؜oxo P/F    `D XQ_q   `a,{ pը@@@ @#`WJ$`X#]HU0ܹYi7- Q9nL}; ,A8P ;+&WXP508 k"?A' G/A@@ ]qo\=&@@lG6džQA#]e`B2& ۀm^] @@ ` 20!@@l@@@> 87 - lޅ]'e2355ٝTI󍸝v7o\$鷳99:1Hy'XRNS~*Av.T"NNkuz4k9xÈ9JҦ-{5SO#APV=ty8iQ껖GMzEDh~oJuq-E@@: i;fŋ󪿡Iz)Gw)K4@uZ.Kۿ|iљ?6/K/=?7}_Wo WF9yJX:7v壔?@@f$lOԸqe:qdrHj6Q=.'*1% _<*\8B5"'WwJܹ!A+ b8Q RрPfn-:D?jޝn 믢]Qn.߂\YkӔOWr}[4Ǩ ߴ4~-eЋޣZXӹMM#06kэodeYks|:xyIT@•*oZ"z&hҿkI\6M,D#[ke   ` 6Dp(fNk~sӣ r>^1~87ïp>o._Uf-:t3Gq[ F#?w+y9{Ж{캨=XTZk)$F#Evh8 ښFtmE?,|n}{ZLv>F~M#>W䊤GS̪oi4ݕ*-~"t&NcޯIn>zsmz d/!>q+I0,Y}>Q_US.(*\ȳ 'nUeCVsy>hflJk yzx9 8;D }nQXJl5 \1#; ,*)lcB\m7\& @@ ς*j% F~x!4   Puw4PuX û7@5U* Xe@'@@@z΂}l6&{xPʼIYeF'ԫAn~oL @5P-rӶ6]NLs&..WNPI-˕@@@@#`Ղ@hw)?'<\FoG''pٟ5sޡTZgotœtAuerM/dkΏ~Fqk~ s5>UrLrpt=v<@]K(b.e\ȠGWeZhL<і]('=bWSZA@lɾߧO&Wӱ~!آmr'?:?99 [@eҒgVS¶$j> "8 sކdYWʧșzB|DN|v[TşxlJU zP/ i\ע")'gm.'^N%@rtvRZ,tKChǻUOGT|iÑeUt"P`>gAGG ތIT 8Q^v~xrdwAv<2qptk) JPkEYͷWmO%\IJ~:D[SߎrŨFlF 1? o%7oAKkOѩmgz8VkHݫ@~CuѯH_Eܦ7o+#tSqfg@}Ӑrدŝ][ڶUѰTSNfʗ Dz/(\;$_7R</*d3hrO#Fi'.Q-GɓhܚO^FC^`8&OQE{[#kū9 pS'8$ EEEYPž/A3wЁ '{Si#b^n󋗫Dy_ +;r e!(bם*o  `CV#1.mϿfGъS.|̅k%!@@Qc   `xsAnA'@@@@SzY4   ` Xi.  A#`0 |UG&@5M+fA@@ jlMT+}pRA   `' a)Dā6]۾" p4q   `tAс" 0 ƒ8mAlBct   `8FNƋaFA@@΂6p@@@   ``㋏@F$@@@@F !Y1v|1t   ` |,h  ` Lp{Cc0 A@@΂@@@ @㋏   `xc  vN5:h  ``㋏ 0 6{%0    vI@7 g.?   ` 5`0|Sxİ^x @@4@@@ >vC% 5`  `tA.A@얀.- @@였.4`w  `0 =1l㋏ #]s@@t!Ӏ\j @@Ji$Ā(8̀1PB 8 ͥ@A@@$% @@v ͥ@A@@$8 D}8 A@@$J2A  8 ͥ@A@@$% @@($(X b@@@>F[Rc   PL% @@ 0 @A@@~4PU,   L@7 gЖ/3   kt"bA@@l1lR @#P*$m}l{) ] @@l.`׀m_h@@LSm Ӏm_h@@Lз4` @@@ `m__@@J%kJͅD$x&IENDB`litestar-2.16.0/docs/tutorials/todo-app/index.rst000066400000000000000000000015461500564371300220030ustar00rootroot00000000000000Developing a basic TODO application =================================== .. admonition:: Who is this tutorial for? :class: info This tutorial is intended to familiarize you with the basic concepts of Litestar. If you have no prior experience with Litestar or web frameworks in general, this is the right place to start. .. note:: Basic knowledge of Python and web development concepts are required. The goal of this tutorial is to develop a TODO application with the following functionalities: 1. Retrieve a TODO list 2. Add items to a TODO list 3. Mark TODO items as *done* This is not a whole lot of functionality, but enough to learn most of the fundamental building blocks of Litestar. .. toctree:: :titlesonly: :hidden: 0-application-basics 1-accessing-the-list 2-interacting-with-the-list 3-assembling-the-app litestar-2.16.0/docs/usage/000077500000000000000000000000001500564371300154675ustar00rootroot00000000000000litestar-2.16.0/docs/usage/applications.rst000066400000000000000000000310401500564371300207050ustar00rootroot00000000000000Applications ============= Application objects ------------------- At the root of every Litestar application is an instance of the :class:`~litestar.app.Litestar` class. Typically, this code will be placed in a file called ``main.py``, ``app.py``, ``asgi.py`` or similar at the project's root directory. These entry points are also used during :ref:`CLI autodiscovery ` Creating an app is straightforward – the only required :term:`args ` is a :class:`list` of :class:`Controllers <.controller.Controller>`, :class:`Routers <.router.Router>`, or :class:`Route handlers <.handlers.BaseRouteHandler>`: .. literalinclude:: /examples/hello_world.py :language: python :caption: A simple Hello World Litestar app The app instance is the root level of the app - it has the base path of ``/`` and all root level :class:`Controllers <.controller.Controller>`, :class:`Routers <.router.Router>`, and :class:`Route handlers <.handlers.BaseRouteHandler>` should be registered on it. .. seealso:: To learn more about registering routes, check out this chapter in the documentation: * :ref:`Routing - Registering Routes ` Startup and Shutdown -------------------- You can pass a list of :term:`callables ` - either sync or async functions, methods, or class instances - to the :paramref:`~litestar.app.Litestar.on_startup` / :paramref:`~litestar.app.Litestar.on_shutdown` :term:`kwargs ` of the :class:`app ` instance. Those will be called in order, once the ASGI server such as `uvicorn `_, `Hypercorn `_, `Granian `_, `Daphne `_, etc. emits the respective event. .. mermaid:: flowchart LR Startup[ASGI-Event: lifespan.startup] --> on_startup Shutdown[ASGI-Event: lifespan.shutdown] --> on_shutdown A classic use case for this is database connectivity. Often, we want to establish a database connection on application startup, and then close it gracefully upon shutdown. For example, let us create a database connection using the async engine from `SQLAlchemy `_. We create two functions, one to get or establish the connection, and another to close it, and then pass them to the :class:`~litestar.app.Litestar` constructor: .. literalinclude:: /examples/startup_and_shutdown.py :language: python :caption: Startup and Shutdown .. _lifespan-context-managers: Lifespan context managers ------------------------- In addition to the lifespan hooks, Litestar also supports managing the lifespan of an application using an :term:`asynchronous context manager`. This can be useful when dealing with long running tasks, or those that need to keep a certain context object, such as a connection, around. .. literalinclude:: /examples/application_hooks/lifespan_manager.py :language: python :caption: Handling a database connection Order of execution ------------------ When multiple lifespan context managers and :paramref:`~litestar.app.Litestar.on_shutdown` hooks are specified, Litestar will invoke the :term:`context managers ` in inverse order before the shutdown hooks are invoked. Consider the case where there are two lifespan context managers ``ctx_a`` and ``ctx_b`` as well as two shutdown hooks ``hook_a`` and ``hook_b`` as shown in the following code: .. code-block:: python :caption: Example of multiple :term:`context managers ` and shutdown hooks app = Litestar(lifespan=[ctx_a, ctx_b], on_shutdown=[hook_a, hook_b]) During shutdown, they are executed in the following order: .. mermaid:: flowchart LR ctx_b --> ctx_a --> hook_a --> hook_b As seen, the :term:`context managers ` are invoked in inverse order. On the other hand, the shutdown hooks are invoked in their specified order. .. _application-state: Using Application State ----------------------- As seen in the examples for the `on_startup / on_shutdown <#startup-and-shutdown>`_, :term:`callables ` passed to these hooks can receive an optional :term:`kwarg ` called ``app``, through which the application's state object and other properties can be accessed. The advantage of using application :paramref:`~.app.Litestar.state`, is that it can be accessed during multiple stages of the connection, and it can be injected into dependencies and route handlers. The Application State is an instance of the :class:`.datastructures.state.State` datastructure, and it is accessible via the :paramref:`~.app.Litestar.state` attribute. As such it can be accessed wherever the app instance is accessible. :paramref:`~.app.Litestar.state` is one of the :ref:`reserved keyword arguments `. It is important to understand in this context that the application instance is injected into the ASGI ``scope`` mapping for each connection (i.e. request or websocket connection) as ``scope["litestar_app"]``, and can be retrieved using :meth:`~.Litestar.from_scope`. This makes the application accessible wherever the scope mapping is available, e.g. in middleware, on :class:`~.connection.request.Request` and :class:`~.connection.websocket.WebSocket` instances (accessible as ``request.app`` / ``socket.app``), and many other places. Therefore, :paramref:`~.app.Litestar.state` offers an easy way to share contextual data between disparate parts of the application, as seen below: .. literalinclude:: /examples/application_state/using_application_state.py :language: python :caption: Using Application State .. _Initializing Application State: Initializing Application State ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To seed application state, you can pass a :class:`~.datastructures.state.State` object to the :paramref:`~.app.Litestar.state` parameter of the Litestar constructor: .. literalinclude:: /examples/application_state/passing_initial_state.py :language: python :caption: Using Application State .. note:: :class:`~.datastructures.state.State` can be initialized with a :class:`dictionary `, an instance of :class:`~.datastructures.state.ImmutableState` or :class:`~.datastructures.state.State`, or a :class:`list` of :class:`tuples ` containing key/value pairs. You may instruct :class:`~.datastructures.state.State` to deep copy initialized data to prevent mutation from outside the application context. To do this, set :paramref:`~.datastructures.state.State.deep_copy` to ``True`` in the :class:`~.datastructures.state.State` constructor. Injecting Application State into Route Handlers and Dependencies ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ As seen in the above example, Litestar offers an easy way to inject state into route handlers and dependencies - simply by specifying ``state`` as a kwarg to the handler or dependency function. For example: .. code-block:: python :caption: Accessing application :class:`~.datastructures.state.State` in a handler function from litestar import get from litestar.datastructures import State @get("/") def handler(state: State) -> None: ... When using this pattern you can specify the class to use for the state object. This type is not merely for type checkers, rather Litestar will instantiate a new ``state`` instance based on the type you set there. This allows users to use custom classes for :class:`~.datastructures.state.State`. While this is very powerful, it might encourage users to follow anti-patterns: it is important to emphasize that using state can lead to code that is hard to reason about and bugs that are difficult to understand, due to changes in different ASGI contexts. As such, this pattern should be used only when it is the best choice and in a limited fashion. To discourage its use, Litestar also offers a builtin :class:`~.datastructures.state.ImmutableState` class. You can use this class to type state and ensure that no mutation of state is allowed: .. literalinclude:: /examples/application_state/using_immutable_state.py :language: python :caption: Using Custom State to ensure immutability Application Hooks ----------------- Litestar includes several application level hooks that allow users to run their own sync or async :term:`callables `. While you are free to use these hooks as you see fit, the design intention behind them is to allow for easy instrumentation for observability (monitoring, tracing, logging, etc.). .. note:: All application hook kwargs detailed below receive either a single :term:`python:callable` or a :class:`list` of :term:`callables `. If a :class:`list` is provided, it is called in the order it is given. After Exception ^^^^^^^^^^^^^^^ The :paramref:`~litestar.app.Litestar.after_exception` hook takes a :class:`sync or async callable ` that is called with two arguments: the ``exception`` that occurred and the ASGI ``scope`` of the request or websocket connection. .. literalinclude:: /examples/application_hooks/after_exception_hook.py :language: python :caption: After Exception Hook .. attention:: This hook is not meant to handle exceptions - it just receives them to allow for side effects. To handle exceptions you should define :ref:`exception handlers `. Before Send ^^^^^^^^^^^ The :paramref:`~litestar.app.Litestar.before_send` hook takes a :class:`sync or async callable ` that is called when an ASGI message is sent. The hook receives the message instance and the ASGI ``scope``. .. literalinclude:: /examples/application_hooks/before_send_hook.py :language: python :caption: Before Send Hook Initialization ^^^^^^^^^^^^^^ Litestar includes a hook for intercepting the arguments passed to the :class:`Litestar constructor `, before they are used to instantiate the application. Handlers can be passed to the :paramref:`~.app.Litestar.on_app_init` parameter on construction of the application, and in turn, each will receive an instance of :class:`~.config.app.AppConfig` and must return an instance of same. This hook is useful for applying common configuration between applications, and for use by developers who may wish to develop third-party application configuration systems. .. note:: :paramref:`~.app.Litestar.on_app_init` handlers cannot be :ref:`python:async def` functions, as they are called within :paramref:`~litestar.app.Litestar.__init__`, outside of an async context. .. literalinclude:: /examples/application_hooks/on_app_init.py :language: python :caption: Example usage of the ``on_app_init`` hook to modify the application configuration. .. _layered-architecture: Layered architecture -------------------- Litestar has a layered architecture compromising of 4 layers: #. :class:`The application object ` #. :class:`Routers <.router.Router>` #. :class:`Controllers <.controller.Controller>` #. :class:`Handlers <.handlers.BaseRouteHandler>` There are many :term:`parameters ` that can be defined on every layer, in which case the :term:`parameter` defined on the layer **closest to the handler** takes precedence. This allows for maximum flexibility and simplicity when configuring complex applications and enables transparent overriding of parameters. Parameters that support layering are: * :ref:`after_request ` * :ref:`after_response ` * :ref:`before_request ` * :ref:`cache_control ` * :doc:`dependencies ` * :doc:`dto ` * :ref:`etag ` * :doc:`exception_handlers ` * :doc:`guards ` * :ref:`include_in_schema ` * :doc:`middleware ` * :ref:`opt ` * :ref:`request_class ` * :ref:`response_class ` * :ref:`response_cookies ` * :ref:`response_headers ` * :doc:`return_dto ` * ``security`` * ``tags`` * :doc:`type_decoders ` * :doc:`type_encoders ` * :ref:`websocket_class ` litestar-2.16.0/docs/usage/caching.rst000066400000000000000000000066721500564371300176300ustar00rootroot00000000000000Caching ======= Caching responses ------------------ Sometimes it's desirable to cache some responses, especially if these involve expensive calculations, or when polling is expected. Litestar comes with a simple mechanism for caching: .. literalinclude:: /examples/caching/cache.py :language: python :lines: 1, 4-8 By setting :paramref:`~litestar.handlers.HTTPRouteHandler.cache` to ``True``, the response from the handler will be cached. If no ``cache_key_builder`` is set in the route handler, caching for the route handler will be enabled for the :attr:`~.config.response_cache.ResponseCacheConfig.default_expiration`. .. note:: If the default :paramref:`~litestar.config.response_cache.ResponseCacheConfig.default_expiration` is set to ``None``, setting up the route handler with :paramref:`~litestar.handlers.HTTPRouteHandler.cache` set to ``True`` will keep the response in cache indefinitely. Alternatively you can specify the number of seconds to cache the responses from the given handler like so: .. literalinclude:: /examples/caching/cache.py :language: python :caption: Caching the response for 120 seconds by setting the :paramref:`~litestar.handlers.HTTPRouteHandler.cache` parameter to the number of seconds to cache the response. :lines: 1, 9-13 :emphasize-lines: 4 If you want the response to be cached indefinitely, you can pass the :class:`~.config.response_cache.CACHE_FOREVER` sentinel instead: .. literalinclude:: /examples/caching/cache.py :language: python :caption: Caching the response indefinitely by setting the :paramref:`~litestar.handlers.HTTPRouteHandler.cache` parameter to :class:`~litestar.config.response_cache.CACHE_FOREVER`. :lines: 1-3, 14-18 :emphasize-lines: 5 Configuration ------------- You can configure caching behaviour on the application level by passing an instance of :class:`~.config.response_cache.ResponseCacheConfig` to the :class:`Litestar instance <.app.Litestar>`. Changing where data is stored +++++++++++++++++++++++++++++ By default, caching will use the :class:`~.stores.memory.MemoryStore`, but it can be configured with any :class:`~.stores.base.Store`, for example :class:`~.stores.redis.RedisStore`: .. literalinclude:: /examples/caching/redis_store.py :language: python :caption: Using Redis as the cache store. Specifying a cache key builder ++++++++++++++++++++++++++++++ Litestar uses the request's path + sorted query parameters as the cache key. This can be adjusted by providing a "key builder" function, either at application or route handler level. .. literalinclude:: /examples/caching/key_builder.py :language: python :caption: Using a custom cache key builder. .. literalinclude:: /examples/caching/key_builder_for_route_handler.py :language: python :caption: Using a custom cache key builder for a specific route handler. Using the cache_response_filter +++++++++++++++++++++++++++++++ The :attr:`~.config.response_cache.ResponseCacheConfig.cache_response_filter` can be customized to implement any caching logic based on the application's needs. For example, you might want to cache only successful responses, or cache responses based on certain headers or content. .. literalinclude:: /examples/caching/cache_response_filter.py :language: python :caption: Using the cache_response_filter to customize caching behavior. In this example, the `custom_cache_response_filter` function caches only successful (2xx) responses. litestar-2.16.0/docs/usage/channels.rst000066400000000000000000000453501500564371300200230ustar00rootroot00000000000000.. currentmodule:: litestar.channels Channels ======== **Channels** are a group of related functionalities, built to facilitate the routing of event streams, which for example can be used to broadcast messages to WebSocket clients. Channels provide: 1. Independent :term:`broker` backends, optionally handling inter-process communication and data persistence on demand 2. "Channel" based :term:`subscription` management 3. Subscriber objects as an abstraction over an individualized :term:`event stream`, providing background workers and managed subscriptions 4. Synchronous and asynchronous data publishing 5. Optional :term:`history` management on a per-:term:`channel` basis 6. :doc:`WebSocket ` integration, generating WebSocket route handlers for an application, to handle the :term:`subscription` and publishing of incoming events to the connected client Basic concepts -------------- Utilizing Channels involves a few moving parts. To better familiarize with the concepts, terminology, and the flow of data, the following glossary and flowcharts are provided Glossary ++++++++ .. dropdown:: Click to toggle the glossary .. glossary:: event A single piece of data published to, or received from a :term:`backend` bound to the :term:`channel` it was originally published to event stream A stream of :term:`events `, consisting of events from all the channels a :term:`Subscriber` has previously subscribed to subscriber A :class:`Subscriber <.subscriber.Subscriber>`: An object wrapping an :term:`event stream` and providing access to it through various methods backend A :class:`ChannelsBackend <.backends.base.ChannelsBackend>`. This object manages communication between the plugin and the :term:`broker`, publishing messages to and receiving messages from it. Each plugin instance is associated with exactly one backend. broker Responsible for receiving and publishing messages to all connected :term:`backends `; All backends sharing the same broker will have access to the same messages, allowing for inter-process communication. This is typically handled by a separate entity like `Redis `_ plugin The :class:`~.plugin.ChannelsPlugin`, a central instance managing :term:`subscribers `, reading messages from the :term:`backend`, putting them in the appropriate :term:`event stream`, and publishing data to the backend channel A named group of subscribers, to which data can be published. Subscribers can subscribe to multiple channels, and channels can have multiple subscribers subscription A connection between a :term:`subscriber` and a :term:`channel`, allowing the subscriber to receive events from the channel backpressure A mechanism to prevent the backlog of a :term:`subscriber` from growing indefinitely, by either dropping new messages or evicting old ones history A set of previously published :term:`events `, stored by the :term:`backend` and available to be pushed to a :term:`subscriber` fanout The process of sending a message to all subscribers of a channel eviction A :term:`backpressure` strategy, dropping the oldest message in the backlog when a new one is added while the backlog is full backoff A :term:`backpressure` strategy, dropping newly incoming messages as long as the backlog is full Flowcharts ++++++++++ .. dropdown:: Click to toggle flowcharts .. mermaid:: :align: center :caption: Publishing flow from the application to the :term:`broker` flowchart LR Backend(Backend) --> Broker[(Broker)] Plugin{{Plugin}} --> Backend Application[[Application]] --> Plugin .. mermaid:: :align: center :caption: Fanout flow of data from the :term:`broker` to the sockets, showing multiple plugin instances flowchart TD Broker[(Broker)] Broker --> Backend_1(Backend) Broker --> Backend_2(Backend) Backend_1 --> Plugin_1{{Plugin}} Backend_2 --> Plugin_2{{Plugin}} Plugin_1 --> Subscriber_1[Subscriber] Plugin_1 --> Subscriber_2[Subscriber] Plugin_1 --> Subscriber_3[Subscriber] Plugin_2 --> Subscriber_4[Subscriber] Plugin_2 --> Subscriber_5[Subscriber] Plugin_2 --> Subscriber_6[Subscriber] The :class:`ChannelsPlugin` --------------------------- .. currentmodule:: litestar.channels.plugin The :class:`ChannelsPlugin` acts as the central entity for managing channels and subscribers. It is used to publish messages, control how data is stored, and manage :term:`subscribers `, route handlers, and configuration. .. tip:: The plugin makes itself available as a dependency under the :paramref:`~ChannelsPlugin.channels` key, which means it is not necessary to import it and instead, it can be used from within route handlers or other callables within the dependency tree directly Configuring the :term:`channels ` ++++++++++++++++++++++++++++++++++++++++++ The :term:`channels ` managed by the plugin can be either defined upfront, passing them to the :paramref:`~ChannelsPlugin.channels` parameter, or created "on the fly" (i.e., on the first :term:`subscription` to a channel) by setting :paramref:`~ChannelsPlugin.arbitrary_channels_allowed` to ``True``. .. code-block:: python :caption: Passing channels explicitly from litestar.channels import ChannelsPlugin channels_plugin = ChannelsPlugin(..., channels=["foo", "bar"]) .. code-block:: python :caption: Allowing arbitrary channels from litestar.channels import ChannelsPlugin channels_plugin = ChannelsPlugin(..., arbitrary_channels_allowed=True) If :paramref:`~ChannelsPlugin.arbitrary_channels_allowed` is not ``True``, trying to publish or subscribe to a :term:`channel` not passed to :paramref:`~ChannelsPlugin.channels` will raise a :exc:`ChannelsException`. Publishing data +++++++++++++++ One of the core aspects of the plugin is publishing data, which is done through its :meth:`~ChannelsPlugin.publish` method: .. code-block:: python :caption: Publishing data to a channel with :meth:`~ChannelsPlugin.publish` channels.publish({"message": "Hello"}, "general") The above example will publish the data to the channel ``general``, subsequently putting it into all subscriber's :term:`event stream` to be consumed. This method is non-blocking, even though channels and the associated :term:`backends ` are fundamentally asynchronous. Calling :meth:`~ChannelsPlugin.publish` effectively enqueues a message to be sent to the backend, from which follows that there is no guarantee that an event will be available in the backend immediately after this call. Alternatively, the asynchronous :meth:`~ChannelsPlugin.wait_published` method can be used, which skips the internal message queue, publishing the data to the backend directly. .. note:: While calling :meth:`~ChannelsPlugin.publish` does not guarantee the message is sent to the backend immediately, it will be sent there *eventually*; On shutdown, the plugin will wait for all queues to empty Managing :term:`subscriptions ` +++++++++++++++++++++++++++++++++++++++++++++ Another core functionality of the plugin is managing :term:`subscriptions `, for which two different approaches exist: 1. Manually through the :meth:`~ChannelsPlugin.subscribe` and :meth:`~ChannelsPlugin.unsubscribe` methods 2. By using the :meth:`~ChannelsPlugin.start_subscription` context manager Both :meth:`~ChannelsPlugin.subscribe` and :meth:`~ChannelsPlugin.start_subscription` produce a :class:`Subscriber`, which can be used to interact with the streams of events subscribed to. The context manager should be preferred, since it ensures that channels are being unsubscribed. Using the :meth:`~ChannelsPlugin.subscribe` and :meth:`~ChannelsPlugin.unsubscribe` methods directly should only be one when a :term:`context manager ` cannot be used, e.g., when the :term:`subscription` would span different contexts. .. code-block:: python :caption: Calling the :term:`subscription` methods manually subscriber = await channels.subscribe(["foo", "bar"]) try: ... # do some stuff here finally: await channels.unsubscribe(subscriber) .. code-block:: python :caption: Using the :term:`async context manager ` async with channels.start_subscription(["foo", "bar"]) as subscriber: ... # do some stuff here It is also possible to unsubscribe from individual :term:`channels `, which may be desirable if :term:`subscriptions ` need to be managed dynamically. .. code-block:: python :caption: Unsubscribing from a channel manually subscriber = await channels.subscribe(["foo", "bar"]) try: ... # do some stuff here finally: await channels.unsubscribe(subscriber, ["foo"]) Or, using the context manager .. code-block:: python :caption: Using the :term:`async context manager ` to unsubscribe from a :term:`channel` async with channels.start_subscription(["foo", "bar"]) as subscriber: ... # do some stuff here await channels.unsubscribe(subscriber, ["foo"]) Managing :term:`history` ++++++++++++++++++++++++ Some backends support per-:term:`channel` :term:`history`, keeping a certain amount of :term:`events ` in storage. This :term:`history` can then be pushed to a :term:`subscriber`. The plugin's :meth:`put_subscriber_history ` can be used to fetch this :term:`history` and put it into a subscriber's :term:`event stream`. .. literalinclude:: /examples/channels/put_history.py :caption: Retrieving :term:`channel` :term:`history` for a :term:`subscriber` and putting it into the :term:`stream ` .. note:: The publication of the :term:`history` happens sequentially, one :term:`channel` and one :term:`event` at a time. This is done to ensure the correct ordering of events and to avoid filling up a :term:`subscriber`'s backlog, which would result in dropped :term:`history` entries. Should the amount of entries exceed the maximum backlog size, the execution will wait until previous events have been processed. Read more: `Managing backpressure`_ The :class:`Subscriber` ----------------------- .. py:currentmodule:: litestar.channels.subscriber The :class:`Subscriber` manages an individual :term:`event stream`, provided to it by the plugin, representing the sum of events from all :term:`channels ` the subscriber has subscribed to. It can be considered the endpoint of all :term:`events `, while the backends act as the source, and the plugin as a router, being responsible for supplying events gathered from the backend into the appropriate subscriber's streams. In addition to being an abstraction of an :term:`event stream`, the :class:`Subscriber` provides two methods to handle this stream: :meth:`iter_events ` An :term:`asynchronous generator`, producing one event from the stream at a time, waiting until the next one becomes available :meth:`run_in_background ` A :term:`context manager `, wrapping an :class:`asyncio.Task`, consuming events yielded by :meth:`iter_events `, invoking a provided :term:`callback` for each of them. Upon exit, it will attempt a graceful shutdown of the running task, waiting for all currently enqueued events in the stream to be processed. If the context exits with an error, the task will be cancelled instead. It is possible to force the task to stop immediately, by setting :paramref:`~Subscriber.run_in_background.join` to ``False`` in :meth:`run_in_background `, which will lead to the cancellation of the task. By default this only happens when the context is left with an exception. .. important:: The :term:`events ` in the :term:`event streams ` are always bytes; When calling :meth:`ChannelsPlugin.publish`, data will be serialized before being sent to the backend. Consuming the :term:`event stream` ++++++++++++++++++++++++++++++++++ There are two general methods of consuming the :term:`event stream`: 1. By iterating over it directly, using :meth:`iter_events ` 2. By using the :meth:`run_in_background ` context manager, which starts a background task, iterating over the stream, invoking a provided callback for every :term:`event` received Iterating over the :term:`stream ` directly is mostly useful if processing the events is the only concern, since :meth:`iter_events ` is effectively an infinite loop. For all other applications, using the context manager is preferable, since it allows to easily run other code concurrently. .. literalinclude:: /examples/channels/iter_stream.py :caption: Iterating over the :term:`event stream` to send data to a WebSocket In the above example, the stream is used to send data to a :class:`WebSocket `. The same can be achieve by passing :meth:`Websocket.send_text() ` as the callback to :meth:`~Subscriber.run_in_background`. This will cause the WebSocket's method to be invoked every time a new :term:`event` becomes available in the :term:`stream `, but gives control back to the application, providing an opportunity to perform other tasks, such as receiving incoming data from the socket. .. literalinclude:: /examples/channels/run_in_background.py :caption: Using :meth:`~Subscriber.run_in_background` to process events concurrently .. important:: Iterating over :meth:`~Subscriber.iter_events` should be approached with caution when being used together with WebSockets. Since :exc:`WebSocketDisconnect` is only raised after the corresponding ASGI event has been *received*, it can result in an indefinitely suspended coroutine. This can happen if for example the client disconnects, but no further events are received. The generator will then wait for new events, but since it will never receive any, no ``send`` call on the WebSocket will be made, which in turn means no exception will be raised to break the loop. Managing :term:`backpressure` ----------------------------- Each subscriber manages its own backlog: A queue of unprocessed :term:`events `. By default, this backlog is unlimited in size, allowing it to grow indefinitely. For most applications, this should be no issue, but when the recipient consistently can not process messages faster than they come in, an application might opt to handle this case. The channels plugin provides two different strategies for managing this :term:`backpressure`: 1. A :term:`backoff` strategy, dropping newly incoming messages as long as the backlog is full 2. An :term:`eviction` strategy, dropping the oldest message in the backlog when a new one is added while the backlog is full .. code-block:: python :caption: Backoff strategy from litestar.channels import ChannelsPlugin from litestar.channels.memory import MemoryChannelsBackend channels = ChannelsPlugin( backend=MemoryChannelsBackend(), max_backlog=1000, backlog_strategy="backoff", ) .. code-block:: python :caption: :term:`Eviction ` strategy from litestar.channels import ChannelsPlugin from litestar.channels.memory import MemoryChannelsBackend channels = ChannelsPlugin( backend=MemoryChannelsBackend(), max_backlog=1000, backlog_strategy="dropleft", ) Backends -------- The storing and :term:`fanout` of messages is handled by a :class:`ChannelsBackend `. The following backends are currently implemented: :class:`MemoryChannelsBacked <.memory.MemoryChannelsBackend>` A basic in-memory backend, mostly useful for testing and local development, but still fully capable. Since it stores all data in-process, it can achieve the highest performance of all the backends, but at the same time is not suitable for applications running on multiple processes. :class:`RedisChannelsPubSubBackend <.redis.RedisChannelsPubSubBackend>` A Redis based backend, using `Pub/Sub `_ to delivery messages. This Redis backend has a low latency and overhead and is generally recommended if :term:`history` is not needed :class:`RedisChannelsStreamBackend <.redis.RedisChannelsStreamBackend>` A redis based backend, using `streams `_ to deliver messages. It has a slightly higher latency when publishing than the Pub/Sub backend, but achieves the same throughput in message :term:`fanout`. Recommended when :term:`history` is needed :class:`AsyncPgChannelsBackend <.asyncpg.AsyncPgChannelsBackend>` A postgres backend using the `asyncpg `_ driver :class:`PsycoPgChannelsBackend <.psycopg.PsycoPgChannelsBackend>` A postgres backend using the `psycopg3 `_ async driver Integrating with websocket handlers ----------------------------------- Generating route handlers +++++++++++++++++++++++++ A common pattern is to create a route handler per :term:`channel`, sending data to the connected client from that channel. This can be fully automated, using the plugin to create these route handlers. .. literalinclude:: /examples/channels/create_route_handlers.py :language: python :caption: Setting ``create_ws_route_handlers=True`` will create route handlers for all ``channels`` The generated route handlers can optionally be configured to send the :term:`channel`'s :term:`history` after a client has connected: .. literalinclude:: /examples/channels/create_route_handlers_send_history.py :caption: Sending the first 10 :term:`history` entries after a client connects .. tip:: When using the :paramref:`~litestar.channels.plugin.ChannelsPlugin.arbitrary_channels_allowed` parameter on the :class:`ChannelsPlugin`, a single route handler will be generated instead, using a :ref:`path parameter ` to specify the channel name litestar-2.16.0/docs/usage/cli.rst000066400000000000000000000160021500564371300167670ustar00rootroot00000000000000CLI === .. |uvicorn| replace:: uvicorn .. _uvicorn: https://www.uvicorn.org/ Litestar provides a convenient command line interface (CLI) for running and managing Litestar applications. The CLI is powered by `click `_, `rich `_, and `rich-click `_. Enabling all CLI features ------------------------- The CLI and its hard dependencies are included by default. However, if you want to run your application (using ``litestar run`` ) or beautify the Typescript generated by the ``litestar schema typescript`` command, you will need |uvicorn|_ and `jsbeautifier `_. They can be installed independently, but we recommend installing the ``standard`` extra which conveniently bundles commonly used optional dependencies. .. code-block:: shell :caption: Install the standard group pip install litestar[standard] Once you have installed ``standard``, you will have access to the ``litestar run`` command. Autodiscovery ------------- Litestar offers autodiscovery of applications and application factories placed within the canonical modules named either ``app`` or ``application``. These modules can be individual files or directories. Within these modules or their submodules, the CLI will detect any instances of :class:`Litestar <.app.Litestar>`, callables named ``create_app``, or callables annotated to return a :class:`Litestar <.app.Litestar>` instance. The autodiscovery follows these lookup locations in order: 1. ``app.py`` 2. ``app/__init__.py`` 3. Submodules of ``app`` 4. ``application.py`` 5. ``application/__init__.py`` 6. Submodules of ``application`` Within these locations, Litestar CLI looks for: 1. An :term:`object` named ``app`` that is an instance of :class:`~.app.Litestar` 2. An object named ``application`` that is an instance of :class:`~.app.Litestar` 3. Any object that is an instance of :class:`~.app.Litestar` 4. A :term:`callable` named ``create_app`` 5. A callable annotated to return an instance of :class:`~.app.Litestar` Specifying an application explicitly ------------------------------------ The application to be used can be specified explicitly via either the ``--app`` argument or the ``LITESTAR_APP`` environment variable. The format for both of them is ``.:``. When both ``--app`` and ``LITESTAR_APP`` are set, the CLI option takes precedence over the environment variable. .. code-block:: bash :caption: Using 'litestar run' and specifying an application factory via --app litestar --app=my_application.app:create_my_app run .. code-block:: bash :caption: Using 'litestar run' and specifying an application factory via LITESTAR_APP LITESTAR_APP=my_application.app:create_my_app litestar run Extending the CLI ----------------- Litestar's CLI is built with `click `_ and can be extended by making use of `entry points `_, or by creating a plugin that conforms to the :class:`~.plugins.CLIPluginProtocol`. Using entry points ^^^^^^^^^^^^^^^^^^ Entry points for the CLI can be added under the ``litestar.commands`` group. These entries should point to a :class:`click.Command` or :class:`click.Group`: .. tab-set:: .. tab-item:: setup.py .. code-block:: python :caption: Using `setuptools `_ from setuptools import setup setup( name="my-litestar-plugin", ..., entry_points={ "litestar.commands": ["my_command=my_litestar_plugin.cli:main"], }, ) .. tab-item:: pdm .. code-block:: toml :caption: Using `PDM `_ [project.scripts] my_command = "my_litestar_plugin.cli:main" # Or, as an entrypoint: [project.entry-points."litestar.commands"] my_command = "my_litestar_plugin.cli:main" .. tab-item:: Poetry .. code-block:: toml :caption: Using `Poetry `_ [tool.poetry.plugins."litestar.commands"] my_command = "my_litestar_plugin.cli:main" Using a plugin ^^^^^^^^^^^^^^ A plugin extending the CLI can be created using the :class:`~.plugins.CLIPluginProtocol`. Its :meth:`~.plugins.CLIPluginProtocol.on_cli_init` will be called during the initialization of the CLI, and receive the root :class:`click.Group` as its first argument, which can then be used to add or override commands: .. code-block:: python :caption: Creating a CLI plugin from litestar import Litestar from litestar.plugins import CLIPluginProtocol from click import Group class CLIPlugin(CLIPluginProtocol): def on_cli_init(self, cli: Group) -> None: @cli.command() def is_debug_mode(app: Litestar): print(app.debug) app = Litestar(plugins=[CLIPlugin()]) Accessing the app instance ^^^^^^^^^^^^^^^^^^^^^^^^^^ When extending the Litestar CLI, you will most likely need access to the loaded ``Litestar`` instance. You can achieve this by adding the special ``app`` parameter to your CLI functions. This will cause the ``Litestar`` instance to be injected into the function whenever it is called from a click-context. .. code-block:: python :caption: Accessing the app instance programmatically import click from litestar import Litestar @click.command() def my_command(app: Litestar) -> None: ... Using the `server_lifespan` hook ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Server lifespan hooks provide a way to run code before and after the *server* starts and stops. In contrast to the regular `lifespan` hooks, they only run once, even when a server starts multiple workers, whereas `lifespan` hooks would run for each individual worker. This makes them suitable for tasks that should happen exactly once, like initializing a database. .. code-block:: python :caption: Using the `server_lifespan` hook from contextlib import contextmanager from typing import Generator from litestar import Litestar from litestar.config.app import AppConfig from litestar.plugins.base import CLIPlugin class StartupPrintPlugin(CLIPlugin): @contextmanager def server_lifespan(self, app: Litestar) -> Generator[None, None, None]: print("i_run_before_startup_plugin") # noqa: T201 try: yield finally: print("i_run_after_shutdown_plugin") # noqa: T201 def create_app() -> Litestar: return Litestar(route_handlers=[], plugins=[StartupPrintPlugin()]) CLI Reference ------------- The most up-to-date reference for the Litestar CLI can be found by running: .. code-block:: shell :caption: Display the CLI help litestar --help You can also visit the :doc:`Litestar CLI Click API Reference ` for that same information. litestar-2.16.0/docs/usage/custom-types.rst000066400000000000000000000031651500564371300207020ustar00rootroot00000000000000Custom types ============ Data serialization / deserialization (encoding / decoding) and validation are important parts of any API framework. In addition to being capable to encode / decode and validate many standard types, litestar supports Python's builtin dataclasses and libraries like Pydantic and msgspec. However, sometimes you may need to employ a custom type. Using type encoders / decoders ------------------------------ Litestar supports a mechanism where you provide encoding and decoding hook functions which translate your type in / to a type that it knows. You can provide them via the ``type_encoders`` and ``type_decoders`` :term:`parameters ` which can be defined on every layer. For example see the :doc:`litestar app reference `. .. admonition:: Layered architecture ``type_encoders`` and ``type_decoders`` are part of Litestar's layered architecture, which means you can set them on every layer of the application. If you set them on multiple layers, the layer closest to the route handler will take precedence. You can read more about this here: :ref:`Layered architecture ` Here is an example: .. literalinclude:: /examples/encoding_decoding/custom_type_encoding_decoding.py :language: python :caption: Tell Litestar how to encode and decode a custom type Custom Pydantic types --------------------- If you use a custom Pydantic type you can use it directly: .. literalinclude:: /examples/encoding_decoding/custom_type_pydantic.py :language: python :caption: Tell Litestar how to encode and decode a custom Pydantic type litestar-2.16.0/docs/usage/databases/000077500000000000000000000000001500564371300174165ustar00rootroot00000000000000litestar-2.16.0/docs/usage/databases/index.rst000066400000000000000000000001251500564371300212550ustar00rootroot00000000000000Databases ========= .. toctree:: :titlesonly: sqlalchemy/index piccolo litestar-2.16.0/docs/usage/databases/piccolo.rst000066400000000000000000000005111500564371300215750ustar00rootroot00000000000000Piccolo ORM =========== Piccolo ORM is an easy-to-use async ORM and query builder and Litestar includes the class :class:`PiccoloDTO ` for data flow control. Example of a small application: .. literalinclude:: /examples/contrib/piccolo/app.py :caption: ``app.py`` :language: python litestar-2.16.0/docs/usage/databases/sqlalchemy/000077500000000000000000000000001500564371300215605ustar00rootroot00000000000000litestar-2.16.0/docs/usage/databases/sqlalchemy/index.rst000066400000000000000000000001211500564371300234130ustar00rootroot00000000000000SQLAlchemy ---------- .. toctree:: models_and_repository plugins/index litestar-2.16.0/docs/usage/databases/sqlalchemy/models_and_repository.rst000066400000000000000000000075711500564371300267300ustar00rootroot00000000000000SQLAlchemy Models & Repository ============================== Litestar comes with a built-in repository class (:class:`SQLAlchemyAsyncRepository `) for `SQLAlchemy `_ to make CRUD operations easier. Features -------- * Pre-configured ``DeclarativeBase`` for :doc:`SQLAlchemy ` 2.0 that includes a UUID or Big Integer based primary-key, a `sentinel column `_, and an optional version with audit columns. * Generic synchronous and asynchronous repositories for select, insert, update, and delete operations on SQLAlchemy models * Implements optimized methods for bulk inserts, updates, and deletes and uses `lambda_stmt `_ when possible. * Integrated counts, pagination, sorting, filtering with ``LIKE``, ``IN``, and dates before and/or after. * Tested support for multiple database backends including: - SQLite via `aiosqlite `_ or `sqlite `_ - Postgres via `asyncpg `_ or `psycopg3 (async or sync) `_ - MySQL via `asyncmy `_ - Oracle via `oracledb `_ - Google Spanner via `spanner-sqlalchemy `_ - DuckDB via `duckdb_engine `_ - Microsoft SQL Server via `pyodbc `_ Basic Use --------- To use the :class:`SQLAlchemyAsyncRepository ` repository, you must first define your models using one of the included built-in ``DeclarativeBase`` ORM base implementations: * :class:`UUIDBase ` * :class:`UUIDAuditBase ` Both include a ``UUID`` based primary key and ``UUIDAuditBase`` includes ``updated_at`` and ``created_at`` timestamp columns. The ``UUID`` will be a native ``UUID``/``GUID`` type on databases that support it such as Postgres. For other engines without a native UUID data type, the UUID is stored as a 16-byte ``BYTES`` or ``RAW`` field. * :class:`BigIntBase ` * :class:`BigIntAuditBase ` Both include a ``BigInteger`` based primary key and ``BigIntAuditBase`` includes ``updated_at`` and ``created_at`` timestamp columns. Models using these bases also include the following enhancements: * Auto-generated snake-case table name from class name * Pydantic BaseModel and Dict classes map to an optimized JSON type that is :class:`JSONB ` for Postgres, `VARCHAR` or `BYTES` with JSON check constraint for Oracle, and :class:`JSON ` for other dialects. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py :caption: ``sqlalchemy_declarative_models.py`` :language: python Basic Controller Integration ----------------------------- Once you have declared your models, you are ready to use the ``SQLAlchemyAsyncRepository`` class with your controllers and function based routes. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_async_repository.py :caption: ``sqlalchemy_async_repository.py`` :language: python Alternately, you may use the ``SQLAlchemySyncRepository`` class for your synchronous database connection. .. literalinclude:: /examples/contrib/sqlalchemy/sqlalchemy_sync_repository.py :caption: ``sqlalchemy_sync_repository.py`` :language: python .. seealso:: * :doc:`/tutorials/repository-tutorial/index` litestar-2.16.0/docs/usage/databases/sqlalchemy/plugins/000077500000000000000000000000001500564371300232415ustar00rootroot00000000000000litestar-2.16.0/docs/usage/databases/sqlalchemy/plugins/index.rst000066400000000000000000000015311500564371300251020ustar00rootroot00000000000000Plugins ------- Litestar has a plugin system that allows you to extend the functionality of the application. Plugins are passed to the application at startup and can pre-configure the application to manage resources, add routes, and more. A suite of plugins is available in :doc:`contrib.sqlalchemy.plugins ` to support using Litestar with SQLAlchemy, these include: - :class:`litestar.contrib.sqlalchemy.plugins.SQLAlchemyPlugin`: Full SQLAlchemy support - :class:`litestar.contrib.sqlalchemy.plugins.SQLAlchemyInitPlugin`: Application tooling - :class:`litestar.contrib.sqlalchemy.plugins.SQLAlchemySerializationPlugin`: Serialization support Each of the plugins is discussed in detail in the following sections. .. toctree:: sqlalchemy_plugin sqlalchemy_init_plugin sqlalchemy_serialization_plugin litestar-2.16.0/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_init_plugin.rst000066400000000000000000000121241500564371300305360ustar00rootroot00000000000000SQLAlchemy Init Plugin ---------------------- The :class:`SQLAlchemyInitPlugin ` adds functionality to the application that supports using Litestar with `SQLAlchemy `_. The plugin: - Makes the SQLAlchemy engine and session available via dependency injection. - Manages the SQLAlchemy engine and session factory in the application's state. - Configures a ``before_send`` handler that is called before sending a response. - Includes relevant names in the signature namespace to aid resolving annotated types. Dependencies ============ The plugin makes the engine and session available for injection. .. tab-set:: .. tab-item:: Async .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_async_dependencies.py :caption: SQLAlchemy Async Dependencies :language: python :linenos: .. tab-item:: Sync .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_dependencies.py :caption: SQLAlchemy Sync Dependencies :language: python :linenos: The above example illustrates how to access the engine and session in the handler, and like all other dependencies, they can also be injected into other dependency functions. Renaming the dependencies ######################### You can change the name that the engine and session are bound to by setting the :attr:`engine_dependency_key ` and :attr:`session_dependency_key ` attributes on the plugin configuration. Configuring the before send handler ################################### The plugin configures a ``before_send`` handler that is called before sending a response. The default handler closes the session and removes it from the connection scope. You can change the handler by setting the :attr:`before_send_handler ` attribute on the configuration object. For example, an alternate handler is available that will also commit the session on success and rollback upon failure. .. tab-set:: .. tab-item:: Async .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_async_before_send_handler.py :caption: SQLAlchemy Async Before Send Handler :language: python :linenos: .. tab-item:: Sync .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_before_send_handler.py :caption: SQLAlchemy Sync Before Send Handler :language: python :linenos: Configuring the plugins ####################### Both the :class:`SQLAlchemyAsyncConfig ` and the :class:`SQLAlchemySyncConfig ` have an ``engine_config`` attribute that is used to configure the engine. The ``engine_config`` attribute is an instance of :class:`EngineConfig ` and exposes all of the configuration options available to the SQLAlchemy engine. The :class:`SQLAlchemyAsyncConfig ` class and the :class:`SQLAlchemySyncConfig ` class also have a ``session_config`` attribute that is used to configure the session. This is either an instance of :class:`AsyncSessionConfig ` or :class:`SyncSessionConfig ` depending on the type of config object. These classes expose all of the configuration options available to the SQLAlchemy session. Finally, the :class:`SQLAlchemyAsyncConfig ` class and the :class:`SQLAlchemySyncConfig ` class expose configuration options to control their behavior. Consult the reference documentation for more information. Example ======= The below example is a complete demonstration of use of the init plugin. Readers who are familiar with the prior section may note the additional complexity involved in managing the conversion to and from SQLAlchemy objects within the handlers. Read on to see how this increased complexity is efficiently handled by the :class:`SQLAlchemySerializationPlugin `. .. tab-set:: .. tab-item:: Async .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_async_init_plugin_example.py :caption: SQLAlchemy Async Init Plugin Example :language: python :linenos: .. tab-item:: Sync .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_init_plugin_example.py :caption: SQLAlchemy Sync Init Plugin Example :language: python :linenos: litestar-2.16.0/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_plugin.rst000066400000000000000000000117271500564371300275230ustar00rootroot00000000000000SQLAlchemy Plugin ----------------- The :class:`SQLAlchemyPlugin ` provides complete support for working with `SQLAlchemy `_ in Litestar applications. .. note:: This plugin is only compatible with SQLAlchemy 2.0+. The :class:`SQLAlchemyPlugin ` combines the functionality of :class:`SQLAlchemyInitPlugin ` and :class:`SQLAlchemySerializationPlugin `, each of which are examined in detail in the following sections. As such, this section describes a complete example of using the :class:`SQLAlchemyPlugin ` with a Litestar application and a SQLite database. Or, skip ahead to :doc:`/usage/databases/sqlalchemy/plugins/sqlalchemy_init_plugin` or :doc:`/usage/databases/sqlalchemy/plugins/sqlalchemy_serialization_plugin` to learn more about the individual plugins. .. tip:: You can install SQLAlchemy alongside Litestar by running ``pip install litestar[sqlalchemy]``. Example ======= .. tab-set:: .. tab-item:: Async .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_async_plugin_example.py :caption: SQLAlchemy Async Plugin Example :language: python :linenos: .. tab-item:: Sync .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_plugin_example.py :caption: SQLAlchemy Sync Plugin Example :language: python :linenos: Defining the Database Models ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We start by defining our base model class, and a ``TodoItem`` class which extends the base model. The ``TodoItem`` class represents a todo item in our SQLite database. .. tab-set:: .. tab-item:: Async .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_async_plugin_example.py :caption: SQLAlchemy Async Plugin Example :language: python :lines: 6,15-24 .. tab-item:: Sync .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_plugin_example.py :caption: SQLAlchemy Sync Plugin Example :language: python :lines: 6,15-24 Setting Up an API Endpoint ~~~~~~~~~~~~~~~~~~~~~~~~~~ Next, we set up an API endpoint at the root (``"/"``) that allows adding a ``TodoItem`` to the SQLite database. .. tab-set:: .. tab-item:: Async .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_async_plugin_example.py :caption: SQLAlchemy Async Plugin Example :language: python :lines: 3-5,8,10-14,25-31 .. tab-item:: Sync .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_plugin_example.py :caption: SQLAlchemy Sync Plugin Example :language: python :lines: 3-5,8,10-14,25-31 Initializing the Database ~~~~~~~~~~~~~~~~~~~~~~~~~ We create a function ``init_db`` that we'll use to initialize the database when the app starts up. .. important:: In this example we drop the database before creating it. This is done for the sake of repeatability, and should not be done in production. .. tab-set:: .. tab-item:: Async .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_async_plugin_example.py :caption: SQLAlchemy Async Plugin Example :language: python :lines: 9,31-35 .. tab-item:: Sync .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_plugin_example.py :caption: SQLAlchemy Sync Plugin Example :language: python :lines: 9,31-33 Setting Up the Plugin and the App ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Finally, we set up the SQLAlchemy Plugin and the Litestar app. .. tab-set:: .. tab-item:: Async .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_async_plugin_example.py :caption: SQLAlchemy Async Plugin Example :language: python :lines: 8,31-35 .. tab-item:: Sync .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_plugin_example.py :caption: SQLAlchemy Sync Plugin Example :language: python :lines: 9,31-33 This configures the app with the plugin, sets up a route handler for adding items, and specifies that the ``init_db`` function should be run when the app starts up. Running the App ~~~~~~~~~~~~~~~ Run the app with the following command: .. code-block:: bash $ litestar run You can now add a todo item by sending a POST request to ``http://localhost:8000`` with a JSON body containing the ``"title"`` of the todo item. .. code-block:: bash $ curl -X POST -H "Content-Type: application/json" -d '{"title": "Your Todo Title", "done": false}' http://localhost:8000/ litestar-2.16.0/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_serialization_plugin.rst000066400000000000000000000056541500564371300324620ustar00rootroot00000000000000SQLAlchemy Serialization Plugin ------------------------------- The SQLAlchemy Serialization Plugin allows Litestar to do the work of transforming inbound and outbound data to and from SQLAlchemy models. The plugin takes no arguments, simply instantiate it and pass it to your application. Example ======= .. tab-set:: .. tab-item:: Async .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_async_serialization_plugin.py :caption: SQLAlchemy Async Serialization Plugin :language: python :linenos: .. tab-item:: Sync .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_serialization_plugin.py :caption: SQLAlchemy Sync Serialization Plugin :language: python :linenos: How it works ============ The plugin works by defining a :class:`SQLAlchemyDTO ` class for each handler ``data`` or return annotation that is a SQLAlchemy model, or collection of SQLAlchemy models, that isn't otherwise managed by an explicitly defined DTO class. The following two examples are functionally equivalent: .. tab-set:: .. tab-item:: Serialization Plugin .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_async_serialization_plugin.py :language: python :linenos: .. tab-item:: Data Transfer Object .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_async_serialization_dto.py :language: python :linenos: During registration, the application recognizes that there is no DTO class explicitly defined and determines that the handler annotations are supported by the SQLAlchemy Serialization Plugin. The plugin is then used to generate a DTO class for both the ``data`` keyword argument and the return annotation. Configuring data transfer ######################### As the serialization plugin merely defines DTOs for the handler, we can :ref:`mark the model fields ` to control the data that we allow in and out of our application. .. tab-set:: .. tab-item:: Async .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_async_serialization_plugin_marking_fields.py :caption: SQLAlchemy Async Marking Fields :language: python :linenos: :emphasize-lines: 9,23,28 .. tab-item:: Sync .. literalinclude:: /examples/contrib/sqlalchemy/plugins/sqlalchemy_sync_serialization_plugin_marking_fields.py :caption: SQLAlchemy Sync Marking Fields :language: python :linenos: :emphasize-lines: 9,23,28 In the above example, a new attribute called ``super_secret_value`` has been added to the model, and a value set for it in the handler. However, due to "marking" the field as "private", when the model is serialized, the value is not present in the response. litestar-2.16.0/docs/usage/debugging.rst000066400000000000000000000105051500564371300201550ustar00rootroot00000000000000Debugging ========= Using the Python debugger -------------------------- You can configure Litestar to drop into the :doc:`Python Debugger ` when an exception occurs. This can be configured in different ways: Configuring ``Litestar`` with the ``pdb_on_exception`` option .. code-block:: python app = Litestar(pdb_on_exception=True) Running your app with the CLI and using the ``--pdb`` flag .. code-block:: shell litestar run --pdb Using the ``LITESTAR_PDB`` environment variable ``LITESTAR_PDB=1`` Debugging with an IDE --------------------- You can easily attach your IDEs debugger to your application, whether you're running it via the CLI or a webserver like `uvicorn `_. Intellij / PyCharm ++++++++++++++++++ Using the CLI ************* 1. Create a new debug configuration via ``Run`` > ``Edit Configurations`` 2. Select ``Module name`` option and set it to ``litestar`` 3. Add the ``run`` parameter and optionally additional parameters you want to pass to the CLI .. image:: /images/debugging/pycharm-config-cli.png 4. Run your application in the debugger via ``Run`` > ``Debug Litestar`` .. image:: /images/debugging/pycharm-debug.png :align: center .. important:: Breakpoints inside route handlers might not work correctly when used in conjunction with the ``--reload`` and ``--web-concurrency`` parameters. If you want to use the CLI while making use of these options, you can attach the debugger manually to the running uvicorn process via ``Run`` > ``Attach to process``. Using uvicorn ************* 1. Create a new debug configuration via ``Run`` > ``Edit Configurations`` 2. Select ``Module name`` option and set it to ``uvicorn`` 3. Add the ``app:app`` parameter (or the equivalent path to your application object) .. image:: /images/debugging/pycharm-config-uvicorn.png 4. Run your application in the debugger via ``Run`` > ``Debug Litestar`` .. image:: /images/debugging/pycharm-debug.png :align: center VS Code +++++++ Using the CLI ************* 1. Add a new debugging configuration via ``Run`` > ``Add configuration`` .. image:: /images/debugging/vs-code-add-config.png :align: center 2. From the ``Select a debug configuration`` dialog, select ``Module`` .. image:: /images/debugging/vs-code-select-config.png 3. Enter ``litestar`` as the module name .. image:: /images/debugging/vs-code-config-litestar.png 4. In the opened JSON file, alter the configuration as follows: .. code-block:: json { "name": "Python: Litestar app", "type": "python", "request": "launch", "module": "litestar", "justMyCode": true, "args": ["run"] } 5. Run your application via the debugger via ``Run`` > ``Start debugging`` .. image:: /images/debugging/vs-code-debug.png :align: center Using uvicorn ************** 1. Add a new debugging configuration via ``Run`` > ``Add configuration`` .. image:: /images/debugging/vs-code-add-config.png :align: center 2. From the ``Select a debug configuration`` dialog, select ``Module`` .. image:: /images/debugging/vs-code-select-config.png 3. Enter ``uvicorn`` as the module name .. image:: /images/debugging/vs-code-config-litestar.png 4. In the opened JSON file, alter the configuration as follows: .. code-block:: json { "name": "Python: Litestar app", "type": "python", "request": "launch", "module": "uvicorn", "justMyCode": true, "args": ["app:app"] } 5. Run your application via the debugger via ``Run`` > ``Start debugging`` .. image:: /images/debugging/vs-code-debug.png :align: center Customizing the debugger ------------------------- You can configure Litestar with the debug_module option to enable interactive debugging. Currently, it supports the following debugging tools: `ipdb `_, `PuDB `_ and `pdbr `_. Also supports `pdb++ `_. The default value is `pdb `_. .. code-block:: python import ipdb app = Litestar(pdb_on_exception=True, debugger_module=ipdb) litestar-2.16.0/docs/usage/dependency-injection.rst000066400000000000000000000301311500564371300223150ustar00rootroot00000000000000Dependency Injection ==================== Litestar has a simple but powerful dependency injection system that allows for declaring dependencies on all layers of the application: .. code-block:: python from litestar import Controller, Router, Litestar, get from litestar.di import Provide async def bool_fn() -> bool: ... async def dict_fn() -> dict: ... async def list_fn() -> list: ... async def int_fn() -> int: ... class MyController(Controller): path = "/controller" # on the controller dependencies = {"controller_dependency": Provide(list_fn)} # on the route handler @get(path="/handler", dependencies={"local_dependency": Provide(int_fn)}) def my_route_handler( self, app_dependency: bool, router_dependency: dict, controller_dependency: list, local_dependency: int, ) -> None: ... # on the router my_router = Router( path="/router", dependencies={"router_dependency": Provide(dict_fn)}, route_handlers=[MyController], ) # on the app app = Litestar( route_handlers=[my_router], dependencies={"app_dependency": Provide(bool_fn)} ) The above example illustrates how dependencies are declared on the different layers of the application. .. note:: Litestar needs the injected types at runtime which might clash with linter rules' recommendation to use ``TYPE_CHECKING``. .. seealso:: :ref:`Signature namespace ` Dependencies can be either callables - sync or async functions, methods, or class instances that implement the :meth:`object.__call__` method, or classes. These are in turn wrapped inside an instance of the :class:`Provide <.di.Provide>` class. .. include:: /admonitions/sync-to-thread-info.rst Pre-requisites and scope ------------------------ The pre-requisites for dependency injection are these: #. dependencies must be callables. #. dependencies can receive kwargs and a ``self`` arg but not positional args. #. the kwarg name and the dependency key must be identical. #. the dependency must be declared using the ``Provide`` class. #. the dependency must be in the *scope* of the handler function. What is *scope* in this context? Dependencies are **isolated** to the context in which they are declared. Thus, in the above example, the ``local_dependency`` can only be accessed within the specific route handler on which it was declared; The ``controller_dependency`` is available only for route handlers on that specific controller; And the ``router dependency`` is available only to the route handlers registered on that particular router. Only the ``app_dependency`` is available to all route handlers. .. _yield_dependencies: Dependencies with yield (cleanup step) -------------------------------------- In addition to simple callables, dependencies can also be (async) generator functions, which allows to execute an additional cleanup step, such as closing a connection, after the handler function has returned. .. admonition:: Technical details :class: info The cleanup stage is executed **after** the handler function returns, but **before** the response is sent (in case of HTTP requests) A basic example ~~~~~~~~~~~~~~~ .. literalinclude:: /examples/dependency_injection/dependency_yield_simple.py :caption: ``dependencies.py`` :language: python If you run the code you'll see that ``CONNECTION`` has been reset after the handler function returned: .. code-block:: python from litestar.testing import TestClient from dependencies import app, CONNECTION with TestClient(app=app) as client: print(client.get("/").json()) # {"open": True} print(CONNECTION) # {"open": False} Handling exceptions ~~~~~~~~~~~~~~~~~~~ If an exception occurs within the handler function, it will be raised **within** the generator, at the point where it first ``yield`` ed. This makes it possible to adapt behaviour of the dependency based on exceptions, for example rolling back a database session on error and committing otherwise. .. literalinclude:: /examples/dependency_injection/dependency_yield_exceptions.py :caption: ``dependencies.py`` :language: python .. code-block:: python from litestar.testing import TestClient from dependencies import STATE, app with TestClient(app=app) as client: response = client.get("/John") print(response.json()) # {"John": "hello"} print(STATE) # {"result": "OK", "connection": "closed"} response = client.get("/Peter") print(response.status_code) # 500 print(STATE) # {"result": "error", "connection": "closed"} .. admonition:: Best Practice :class: tip You should always wrap ``yield`` in a ``try``/``finally`` block, regardless of whether you want to handle exceptions, to ensure that the cleanup code is run even when exceptions occurred: .. code-block:: python def generator_dependency(): try: yield finally: ... # cleanup code .. attention:: Do not re-raise exceptions within the dependency. Exceptions caught within these dependencies will still be handled by the regular mechanisms without an explicit re-raise .. important:: Exceptions raised during the cleanup step of a dependency will be re-raised in an :exc:`ExceptionGroup` (for Python versions < 3.11, the `exceptiongroup `_ will be used). This happens after all dependencies have been cleaned up, so exceptions raised during cleanup of one dependencies do not affect the cleanup of other dependencies. Dependency keyword arguments ---------------------------- As stated above dependencies can receive kwargs but no args. The reason for this is that dependencies are parsed using the same mechanism that parses route handler functions, and they too - like route handler functions, can have data injected into them. In fact, you can inject the same data that you can :ref:`inject into route handlers `. .. code-block:: python from litestar import Controller, patch from litestar.di import Provide from pydantic import BaseModel, UUID4 class User(BaseModel): id: UUID4 name: str async def retrieve_db_user(user_id: UUID4) -> User: ... class UserController(Controller): path = "/user" dependencies = {"user": Provide(retrieve_db_user)} @patch(path="/{user_id:uuid}") async def get_user(self, user: User) -> User: ... In the above example we have a ``User`` model that we are persisting into a db. The model is fetched using the helper method ``retrieve_db_user`` which receives a ``user_id`` kwarg and retrieves the corresponding ``User`` instance. The ``UserController`` class maps the ``retrieve_db_user`` provider to the key ``user`` in its ``dependencies`` dictionary. This in turn makes it available as a kwarg in the ``get_user`` method. Dependency overrides -------------------- Because dependencies are declared at each level of the app using a string keyed dictionary, overriding dependencies is very simple: .. code-block:: python from litestar import Controller, get from litestar.di import Provide def bool_fn() -> bool: ... def dict_fn() -> dict: ... class MyController(Controller): path = "/controller" # on the controller dependencies = {"some_dependency": Provide(dict_fn)} # on the route handler @get(path="/handler", dependencies={"some_dependency": Provide(bool_fn)}) def my_route_handler( self, some_dependency: bool, ) -> None: ... The lower scoped route handler function declares a dependency with the same key as the one declared on the higher scoped controller. The lower scoped dependency therefore overrides the higher scoped one. The ``Provide`` class ---------------------- The :class:`Provide <.di.Provide>` class is a wrapper used for dependency injection. To inject a callable you must wrap it in ``Provide``: .. code-block:: python from random import randint from litestar import get from litestar.di import Provide def my_dependency() -> int: return randint(1, 10) @get( "/some-path", dependencies={ "my_dep": Provide( my_dependency, ) }, ) def my_handler(my_dep: int) -> None: ... .. attention:: If :class:`Provide.use_cache <.di.Provide>` is ``True``, the return value of the function will be memoized the first time it is called and then will be used. There is no sophisticated comparison of kwargs, LRU implementation, etc., so you should be careful when you choose to use this option. Note that dependencies will only be called once per request, even with ``Provide.use_cache`` set to ``False``. Dependencies within dependencies -------------------------------- You can inject dependencies into other dependencies - exactly like you would into regular functions. .. code-block:: python from litestar import Litestar, get from litestar.di import Provide from random import randint def first_dependency() -> int: return randint(1, 10) def second_dependency(injected_integer: int) -> bool: return injected_integer % 2 == 0 @get("/true-or-false") def true_or_false_handler(injected_bool: bool) -> str: return "its true!" if injected_bool else "nope, its false..." app = Litestar( route_handlers=[true_or_false_handler], dependencies={ "injected_integer": Provide(first_dependency), "injected_bool": Provide(second_dependency), }, ) .. note:: The rules for `dependency overrides`_ apply here as well. The ``Dependency`` function ---------------------------- Dependency validation ~~~~~~~~~~~~~~~~~~~~~ By default, injected dependency values are validated by Litestar, for example, this application will raise an internal server error: .. literalinclude:: /examples/dependency_injection/dependency_validation_error.py :caption: Dependency validation error :language: python Dependency validation can be toggled using the :class:`Dependency ` function. .. literalinclude:: /examples/dependency_injection/dependency_skip_validation.py :caption: Dependency validation error :language: python This may be useful for reasons of efficiency, or if pydantic cannot validate a certain type, but use with caution! Dependency function as a marker ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The :class:`Dependency ` function can also be used as a marker that gives us a bit more detail about your application. Exclude dependencies with default values from OpenAPI docs *********************************************************** Depending on your application design, it is possible to have a dependency declared in a handler or :class:`Provide <.di.Provide>` function that has a default value. If the dependency isn't provided for the route, the default should be used by the function. .. literalinclude:: /examples/dependency_injection/dependency_with_default.py :caption: Dependency with default value :language: python This doesn't fail, but due to the way the application determines parameter types, it is inferred to be a query parameter. By declaring the parameter to be a dependency, Litestar knows to exclude it from the docs: .. literalinclude:: /examples/dependency_injection/dependency_with_dependency_fn_and_default.py :caption: Dependency with default value :language: python Early detection if a dependency isn't provided *********************************************** The other side of the same coin is when a dependency isn't provided, and no default is specified. Without the dependency marker, the parameter is assumed to be a query parameter and the route will most likely fail when accessed. If the parameter is marked as a dependency, this allows us to fail early instead: .. literalinclude:: /examples/dependency_injection/dependency_non_optional_not_provided.py :caption: Dependency not provided error :language: python litestar-2.16.0/docs/usage/dto/000077500000000000000000000000001500564371300162555ustar00rootroot00000000000000litestar-2.16.0/docs/usage/dto/0-basic-use.rst000066400000000000000000000153331500564371300210240ustar00rootroot00000000000000Basic Use ========= Here we demonstrate how to declare DTO types to your route handlers. For demonstration purposes, we assume that we are working with a data model ``User``, and already have two DTO types created in our application, ``UserDTO``, and ``UserReturnDTO``. DTO layer parameters ~~~~~~~~~~~~~~~~~~~~ On every :ref:`Layer ` of the Litestar application there are two parameters that control the DTOs that will take responsibility for the data received and returned from handlers: - ``dto``: This parameter describes the DTO that will be used to parse inbound data to be injected as the ``data`` keyword argument for a handler. Additionally, if no ``return_dto`` is declared on the handler, this will also be used to encode the return data for the handler. - ``return_dto``: This parameter describes the DTO that will be used to encode data returned from the handler. If not provided, the DTO described by the ``dto`` parameter is used. The object provided to both of these parameters must be a class that conforms to the :class:`AbstractDTO ` protocol. Defining DTOs on handlers ~~~~~~~~~~~~~~~~~~~~~~~~~ The ``dto`` parameter --------------------- .. literalinclude:: /examples/data_transfer_objects/the_dto_parameter.py :caption: Using the ``dto`` Parameter :language: python In this example, ``UserDTO`` performs decoding of client data into the ``User`` type, and encoding of the returned ``User`` instance into a type that Litestar can encode into bytes. The ``return_dto`` parameter ---------------------------- .. literalinclude:: /examples/data_transfer_objects/the_return_dto_parameter.py :caption: Using the ``return_dto`` Parameter :language: python In this example, ``UserDTO`` performs decoding of client data into the ``User`` type, and ``UserReturnDTO`` is responsible for converting the ``User`` instance into a type that Litestar can encode into bytes. Overriding implicit ``return_dto`` ---------------------------------- If a ``return_dto`` type is not declared for a handler, the type declared for the ``dto`` parameter is used for both decoding and encoding request and response data. If this behavior is undesirable, it can be disabled by explicitly setting the ``return_dto`` to ``None``. .. literalinclude:: /examples/data_transfer_objects/overriding_implicit_return_dto.py :caption: Disable implicit ``return_dto`` behavior :language: python In this example, we use ``UserDTO`` to decode request data, and convert it into the ``User`` type, but we want to manage encoding the response data ourselves, and so we explicitly declare the ``return_dto`` as ``None``. Defining DTOs on layers ~~~~~~~~~~~~~~~~~~~~~~~ DTOs can be defined on any :ref:`Layer ` of the application. The DTO type applied is the one defined in the ownership chain, closest to the handler in question. .. literalinclude:: /examples/data_transfer_objects/defining_dtos_on_layers.py :caption: Controller defined DTOs :language: python In this example, the ``User`` instance received by any handler that declares a ``data`` kwarg, is converted by the ``UserDTO`` type, and all handler return values are converted into an encodable type by ``UserReturnDTO`` (except for the ``delete()`` route, which has the ``return_dto`` disabled). DTOs can similarly be defined on :class:`Routers ` and :class:`The application ` itself. Improving performance with the codegen backend ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. note:: This feature was introduced in ``2.2.0`` and was hidden behind the ``DTO_CODEGEN`` feature flag. As of ``2.8.0`` it is considered stable and is enabled by default. It can still be disabled selectively by using the ``DTOConfig(experimental_codegen_backend=False)`` override. The DTO backend is the part that does the heavy lifting for all the DTO features. It is responsible for the transforming, validation and parsing. Because of this, it is also the part with the most significant performance impact. To reduce the overhead introduced by the DTOs, the DTO codegen backend was introduced; A DTO backend that increases efficiency by generating optimized Python code at runtime to perform all the necessary operations. Disabling the backend --------------------- You can use ``experimental_codegen_backend=False`` to disable the codegen backend selectively: .. code-block:: python from dataclasses import dataclass from litestar.dto import DTOConfig, DataclassDTO @dataclass class Foo: name: str class FooDTO(DataclassDTO[Foo]): config = DTOConfig(experimental_codegen_backend=False) Enabling the backend -------------------- .. note:: This is a historical document meant for Litestar versions prior to 2.8.0 This backend was enabled by default since 2.8.0 .. warning:: ``ExperimentalFeatures.DTO_CODEGEN`` is deprecated and will be removed in 3.0.0 .. dropdown:: Enabling DTO codegen backend :icon: git-pull-request-closed You can enable this backend globally for all DTOs by passing the appropriate feature flag to your Litestar application: .. code-block:: python from litestar import Litestar from litestar.config.app import ExperimentalFeatures app = Litestar(experimental_features=[ExperimentalFeatures.DTO_CODEGEN]) or selectively for individual DTOs: .. code-block:: python from dataclasses import dataclass from litestar.dto import DTOConfig, DataclassDTO @dataclass class Foo: name: str class FooDTO(DataclassDTO[Foo]): config = DTOConfig(experimental_codegen_backend=True) The same flag can be used to disable the backend selectively: .. code-block:: python from dataclasses import dataclass from litestar.dto import DTOConfig, DataclassDTO @dataclass class Foo: name: str class FooDTO(DataclassDTO[Foo]): config = DTOConfig(experimental_codegen_backend=False) Performance improvements ------------------------ These are some preliminary numbers showing the performance increase for certain operations: =================================== =========== operation improvement =================================== =========== JSON to Python ~2.5x JSON to Python (collection) ~3.5x Python to Python ~2.5x Python to Python (collection) ~5x Python to JSON ~5.3x Python to JSON (collection) ~5.4x =================================== =========== .. seealso:: If you are interested in technical details, check out https://github.com/litestar-org/litestar/pull/2388 litestar-2.16.0/docs/usage/dto/1-abstract-dto.rst000066400000000000000000000452061500564371300215430ustar00rootroot00000000000000AbstractDTO =========== Litestar maintains a suite of DTO factory types that can be used to create DTOs for use with popular data modelling libraries, such as ORMs. These take a model type as a generic type argument, and create subtypes of :class:`AbstractDTO ` that support conversion of that model type to and from raw bytes. The following factories are currently available: - :class:`DataclassDTO ` - :class:`MsgspecDTO ` - :class:`PydanticDTO ` - :class:`SQLAlchemyDTO ` Using DTO Factories ------------------- DTO factories are used to create DTOs for use with a particular data modelling library. The following example creates a DTO for use with a SQLAlchemy model: .. literalinclude:: /examples/data_transfer_objects/factory/simple_dto_factory_example.py :caption: A SQLAlchemy model DTO :language: python Here we see that a SQLAlchemy model is used as both the ``data`` and return annotation for the handler, and while Litestar does not natively support encoding/decoding to/from SQLAlchemy models, through :class:`SQLAlchemyDTO ` we can do this. However, we do have some issues with the above example. Firstly, the user's password has been returned to them in the response from the handler. Secondly, the user is able to set the ``created_at`` field on the model, which should only ever be set once, and defined internally. Let's explore how we can configure DTOs to manage scenarios like these. .. _dto-marking-fields: Marking fields -------------- The :func:`dto_field ` function can be used to mark model attributes with DTO-based configuration. Fields marked as ``"private"`` or ``"read-only"`` will not be parsed from client data into the user model, and ``"private"`` fields are never serialized into return data. .. literalinclude:: /examples/data_transfer_objects/factory/marking_fields.py :caption: Marking fields :language: python :emphasize-lines: 6,14,15 :linenos: Note that ``id`` field is the primary key and is handled specially by the defined SQLAlchemy base. .. note: The procedure for "marking" a model field will vary depending on the library. For example, :class:`DataclassDTO <.dto.DataclassDTO>` expects that the mark is made in the ``metadata`` parameter to ``dataclasses.field``. Excluding fields ---------------- Fields can be explicitly excluded using :class:`DTOConfig `. The following example demonstrates excluding attributes from the serialized response, including excluding fields from nested models. .. literalinclude:: /examples/data_transfer_objects/factory/excluding_fields.py :caption: Excluding fields :language: python :emphasize-lines: 6,10,37-46,49 :linenos: Here, the config is created with the exclude parameter, which is a set of strings. Each string represents the path to a field in the ``User`` object that should be excluded from the output DTO. .. code-block:: python config = DTOConfig( exclude={ "id", "address.id", "address.street", "pets.0.id", "pets.0.user_id", } ) In this example, ``"id"`` represents the id field of the ``User`` object, ``"address.id"`` and ``"address.street"`` represent fields of the ``Address`` object nested inside the ``User`` object, and ``"pets.0.id"`` and ``"pets.0.user_id"`` represent fields of the ``Pets`` objects nested within the list of ``User.pets``. .. note:: Given a generic type, with an arbitrary number of type parameters (e.g., ``GenericType[Type0, Type1, ..., TypeN]``), we use the index of the type parameter to indicate which type the exclusion should refer to. For example, ``a.0.b``, excludes the ``b`` field from the first type parameter of ``a``, ``a.1.b`` excludes the ``b`` field from the second type parameter of ``a``, and so on. Renaming fields --------------- Fields can be renamed using :class:`DTOConfig `. The following example uses the name ``userName`` client-side, and ``user`` internally. .. literalinclude:: /examples/data_transfer_objects/factory/renaming_fields.py :caption: Renaming fields :language: python :emphasize-lines: 4,8,19,20,24 :linenos: Fields can also be renamed using a renaming strategy that will be applied to all fields. The following example uses a pre-defined rename strategy that will convert all field names to camel case on client-side. .. literalinclude:: /examples/data_transfer_objects/factory/renaming_all_fields.py :caption: Renaming fields :language: python :emphasize-lines: 4,8,19,20,21,22,24 :linenos: Fields that are directly renamed using `rename_fields` mapping will be excluded from `rename_strategy`. The rename strategy either accepts one of the pre-defined strategies: "camel", "pascal", "upper", "lower", "kebab", or it can be provided a callback that accepts the field name as a string argument and should return a string. Type checking ------------- Factories check that the types to which they are assigned are a subclass of the type provided as the generic type to the DTO factory. This means that if you have a handler that accepts a ``User`` model, and you assign a ``UserDTO`` factory to it, the DTO will only accept ``User`` types for "data" and return types. .. literalinclude:: /examples/data_transfer_objects/factory/type_checking.py :caption: Type checking :language: python :emphasize-lines: 25,26,31 :linenos: In the above example, the handler is declared to use ``UserDTO`` which has been type-narrowed with the ``User`` type. However, we annotate the handler with the ``Foo`` type. This will raise an error such as this at runtime: litestar.exceptions.dto.InvalidAnnotationException: DTO narrowed with '', handler type is '' Nested fields ------------- The depth of related items parsed from client data and serialized into return data can be controlled using the ``max_nested_depth`` parameter to :class:`DTOConfig `. In this example, we set ``max_nested_depth=0`` for the DTO that handles inbound client data, and leave it at the default of ``1`` for the return DTO. .. literalinclude:: /examples/data_transfer_objects/factory/related_items.py :caption: Type checking :language: python :emphasize-lines: 25,35,39 :linenos: When the handler receives the client data, we can see that the ``b`` field has not been parsed into the ``A`` model that is injected for our data parameter (line 35). We then add a ``B`` instance to the data (line 39), which includes a reference back to ``a``, and from inspection of the return data can see that ``b`` is included in the response data, however ``b.a`` is not, due to the default ``max_nested_depth`` of ``1``. Handling unknown fields ----------------------- By default, DTOs will silently ignore unknown fields in the source data. This behaviour can be configured using the ``forbid_unknown_fields`` parameter of the :class:`DTOConfig `. When set to ``True`` a validation error response will be returned if the data contains a field not defined on the model: .. literalinclude:: /examples/data_transfer_objects/factory/unknown_fields.py :caption: Type checking :language: python :linenos: DTO Data -------- Sometimes we need to be able to access the data that has been parsed and validated by the DTO, but not converted into an instance of our data model. In the following example, we create a ``User`` model, that is a :func:`dataclass ` with 3 required fields: ``id``, ``name``, and ``age``. We also create a DTO that doesn't allow clients to set the ``id`` field on the ``User`` model and set it on the handler. .. literalinclude:: /examples/data_transfer_objects/factory/dto_data_problem_statement.py :language: python :emphasize-lines: 18-21,27 :linenos: Notice that our `User` model has a model-level ``default_factory=uuid4`` for ``id`` field. That's why we can decode the client data into this model. However, in some cases there's no clear way to provide a default this way. One way to handle this is to create different models, e.g., we might create a ``UserCreate`` model that has no ``id`` field, and decode the client data into that. However, this method can become quite cumbersome when we have a lot of variability in the data that we accept from clients, for example, `PATCH `_ requests. This is where the :class:`DTOData ` class comes in. It is a generic class that accepts the type of the data that it will contain, and provides useful methods for interacting with that data. .. literalinclude:: /examples/data_transfer_objects/factory/dto_data_usage.py :language: python :emphasize-lines: 5,23,25 :linenos: In the above example, we've injected an instance of :class:`DTOData ` into our handler, and have used that to create our ``User`` instance, after augmenting the client data with a server generated ``id`` value. Consult the :class:`Reference Docs ` for more information on the methods available. .. _dto-create-instance-nested-data: Providing values for nested data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To augment data used to instantiate our model instances, we can provide keyword arguments to the :meth:`create_instance() ` method. Sometimes we need to provide values for nested data, for example, when creating a new instance of a model that has a nested model with excluded fields. .. literalinclude:: /examples/data_transfer_objects/factory/providing_values_for_nested_data.py :language: python :emphasize-lines: 9-12,20,28,34 :linenos: The double-underscore syntax ``address__id`` passed as a keyword argument to the :meth:`create_instance() ` method call is used to specify a value for a nested attribute. In this case, it's used to provide a value for the ``id`` attribute of the ``Address`` instance nested within the ``Person`` instance. This is a common convention in Python for dealing with nested structures. The double underscore can be interpreted as "traverse through", so ``address__id`` means "traverse through address to get to its id". In the context of this script, ``create_instance(id=1, address__id=2)`` is saying "create a new ``Person`` instance from the client data given an id of ``1``, and supplement the client address data with an id of ``2``". DTO Factory and PATCH requests ------------------------------ `PATCH `_ requests are a special case when it comes to data transfer objects. The reason for this is that we need to be able to accept and validate any subset of the model attributes in the client payload, which requires some special handling internally. .. literalinclude:: /examples/data_transfer_objects/factory/patch_requests.py :language: python :emphasize-lines: 7,20,27,28,30 :linenos: The ``PatchDTO`` class is defined for the ``Person`` class. The ``config`` attribute of ``PatchDTO`` is set to exclude the ``id`` field, preventing clients from setting it when updating a person, and the ``partial`` attribute is set to ``True``, which allows the DTO to accept a subset of the model attributes. Inside the handler, the :meth:`DTOData.update_instance ` method is called to update the instance of ``Person`` before returning it. In our request, we update only the ``name`` property of the ``Person``, from ``"Peter"`` to ``"Peter Pan"`` and receive the full object - with the modified name - back in the response. Implicit Private Fields ----------------------- Fields that are named with a leading underscore are considered "private" by default. This means that they will not be parsed from client data, and will not be serialized into return data. .. literalinclude:: /examples/data_transfer_objects/factory/leading_underscore_private.py :language: python :linenos: This can be overridden by setting the :attr:`DTOConfig.leading_underscore_private ` attribute to ``False``. .. literalinclude:: /examples/data_transfer_objects/factory/leading_underscore_private_override.py :language: python :linenos: :emphasize-lines: 14,15 Wrapping Return Data -------------------- Litestar's DTO Factory types are versatile enough to manage your data, even when it's nested within generic wrappers. The following example demonstrates a route handler that returns DTO managed data wrapped in a generic type. The wrapper is used to deliver additional metadata about the response - in this case, a count of the number of items returned. Read on for an explanation of how to do this yourself. .. literalinclude:: /examples/data_transfer_objects/factory/enveloping_return_data.py :caption: Enveloping Return Data :language: python :linenos: First, create a generic dataclass to act as your wrapper. This type will contain your data and any additional attributes you might need. In this example, we have a ``WithCount`` dataclass which has a ``count`` attribute. The wrapper must be a python generic type with one or more type parameters, and at least one of those type parameters should describe an instance attribute that will be populated with the data. .. code-block:: python from dataclasses import dataclass from typing import Generic, TypeVar T = TypeVar("T") @dataclass class WithCount(Generic[T]): count: int data: List[T] Now, create a DTO for your data object and configure it using ``DTOConfig``. In this example, we're excluding ``password`` and ``created_at`` from the final output. .. code-block:: python from advanced_alchemy.dto import SQLAlchemyDTO from litestar.dto import DTOConfig class UserDTO(SQLAlchemyDTO[User]): config = DTOConfig(exclude={"password", "created_at"}) Then, set up your route handler. This example sets up a ``/users`` endpoint, where a list of ``User`` objects is returned, wrapped in the ``WithCount`` dataclass. .. code-block:: python from litestar import get @get("/users", dto=UserDTO, sync_to_thread=False) def get_users() -> WithCount[User]: return WithCount( count=1, data=[ User( id=1, name="Litestar User", password="xyz", created_at=datetime.now(), ), ], ) This setup allows the DTO to manage the rendering of ``User`` objects into the response. The DTO Factory type will find the attribute on the wrapper type that holds the data and perform its serialization operations upon it. Returning enveloped data is subject to the following constraints: #. The type returned from the handler must be a type that Litestar can natively encode. #. There can be multiple type arguments to the generic wrapper type, but there must be exactly one type argument to the generic wrapper that is a type supported by the DTO. Working with Litestar's Pagination Types ---------------------------------------- Litestar offers paginated response wrapper types, and DTO Factory types can handle this out of the box. .. literalinclude:: /examples/data_transfer_objects/factory/paginated_return_data.py :caption: Paginated Return Data :language: python :linenos: The DTO is defined and configured, in our example, we're excluding ``password`` and ``created_at`` fields from the final representation of our users. .. code-block:: python from advanced_alchemy.dto import SQLAlchemyDTO from litestar.dto import DTOConfig class UserDTO(SQLAlchemyDTO[User]): config = DTOConfig(exclude={"password", "created_at"}) The example sets up a ``/users`` endpoint, where a paginated list of ``User`` objects is returned, wrapped in :class:`ClassicPagination <.pagination.ClassicPagination>`. .. code-block:: python from litestar import get from litestar.pagination import ClassicPagination @get("/users", dto=UserDTO, sync_to_thread=False) def get_users() -> ClassicPagination[User]: return ClassicPagination( page_size=10, total_pages=1, current_page=1, items=[ User( id=1, name="Litestar User", password="xyz", created_at=datetime.now(), ), ], ) The :class:`ClassicPagination <.pagination.ClassicPagination>` class contains ``page_size`` (number of items per page), ``total_pages`` (total number of pages), ``current_page`` (current page number), and ``items`` (items for the current page). The DTO operates on the data contained in the ``items`` attribute, and the pagination wrapper is handled automatically by Litestar's serialization process. Using Litestar's Response Type with DTO Factory ----------------------------------------------- Litestar's DTO (Data Transfer Object) Factory Types can handle data wrapped in a ``Response`` type. .. literalinclude:: /examples/data_transfer_objects/factory/response_return_data.py :caption: Response Wrapped Return Data :language: python :linenos: We create a DTO for the ``User`` type and configure it using ``DTOConfig`` to exclude ``password`` and ``created_at`` from the serialized output. .. code-block:: python from advanced_alchemy.dto import SQLAlchemyDTO from litestar.dto import DTOConfig class UserDTO(SQLAlchemyDTO[User]): config = DTOConfig(exclude={"password", "created_at"}) The example sets up a ``/users`` endpoint where a ``User`` object is returned wrapped in a ``Response`` type. .. code-block:: python from litestar import get, Response @get("/users", dto=UserDTO, sync_to_thread=False) def get_users() -> Response[User]: return Response( content=User( id=1, name="Litestar User", password="xyz", created_at=datetime.now(), ), headers={"X-Total-Count": "1"}, ) The ``Response`` object encapsulates the ``User`` object in its ``content`` attribute and allows us to configure the response received by the client. In this case, we add a custom header. litestar-2.16.0/docs/usage/dto/2-creating-custom-dto-classes.rst000066400000000000000000000021431500564371300244710ustar00rootroot00000000000000Implementing Custom DTO Classes =============================== While Litestar maintains a suite of DTO factories, it is possible to create your own DTOs. To do so, you must implement the :class:`AbstractDTO ` abc. The following is a description of the methods of the protocol, and how they are used by Litestar. For detailed information on the signature of each method, see the :class:`reference docs `. Abstract Methods ~~~~~~~~~~~~~~~~ These methods must be implemented on any :class:`AbstractDTO ` subtype. ``generate_field_definitions`` ------------------------------ This method receives the model type for the DTO and it should return a generator yielding :class:`DTOFieldDefinition` instances corresponding with the model fields. ``detect_nested_field`` ----------------------- This method receives a :class:`FieldDefinition` instance and it should return a boolean indicating whether the field is a nested model field. litestar-2.16.0/docs/usage/dto/index.rst000066400000000000000000000057721500564371300201310ustar00rootroot00000000000000Data Transfer Object (DTO) ========================== In Litestar, a Data Transfer Object (DTO) is a class that is used to control the flow of data from the client into a useful form for the developer to use in their handler, and then back again. The following diagram demonstrates how the DTO is used within the context of an individual request in Litestar: .. mermaid:: sequenceDiagram autonumber actor Client participant Litestar participant DTO participant Handler Client->>Litestar: Data as encoded bytes activate Litestar Litestar->>DTO: Encoded data deactivate Litestar activate DTO Note over DTO: Perform primitive type validation
and conversion into data model DTO->>Handler: Data injected into handler deactivate DTO activate Handler Note over Handler: Handler receives data as type
declared in handler signature
and performs business logic Handler->>DTO: Data returned from handler deactivate Handler activate DTO Note over DTO: Data returned from the handler
is converted into a type that
Litestar can encode into bytes DTO->>Litestar: Litestar encodable type deactivate DTO activate Litestar Note over Litestar: Data received from DTO
is encoded into bytes Litestar->>Client: Data as encoded bytes deactivate Litestar Data movement ------------- The following is a short summary of the interaction between each of the participants in the above diagram. Data moves between each of the participants in the DTO chart, and as it does so, different actions are performed on the data, and it takes different forms depending on the direction of data transfer, and the participants on either end of the transfer. Lets take a look at each of these data movements: Client → Litestar → DTO ~~~~~~~~~~~~~~~~~~~~~~~~~ - Data is received from the client as encoded bytes - In most cases, the unencoded bytes are passed directly to the DTO - Exception is multipart and URL encoded data, which is decoded into python built-in types before being passed to the DTO DTO → Handler ~~~~~~~~~~~~~~~~ - DTO receives data from client - Performs primitive type validation - Marshals the data into the data type declared in the handler annotation Handler → DTO ~~~~~~~~~~~~~~~~ - Handler receives data as type declared in handler signature - Developer performs business logic and returns data from handler DTO → Litestar ~~~~~~~~~~~~~~~~~~~~~~~~~ - DTO receives data from handler - Marshals the data into a type that can be encoded into bytes by Litestar Litestar → Client ~~~~~~~~~~~~~~~~~~ - Data is received from the DTO as a type that Litestar can encode into bytes - Data is encoded into bytes and sent to the client Contents -------- .. toctree:: 0-basic-use 1-abstract-dto 2-creating-custom-dto-classes litestar-2.16.0/docs/usage/events.rst000066400000000000000000000135111500564371300175260ustar00rootroot00000000000000Events ====== Litestar supports a simple implementation of the event emitter / listener pattern: .. code-block:: python from dataclasses import dataclass from litestar import Request, post from litestar.events import listener from litestar import Litestar from db import user_repository from utils.email import send_welcome_mail @listener("user_created") async def send_welcome_email_handler(email: str) -> None: # do something here to send an email await send_welcome_mail(email) @dataclass class CreateUserDTO: first_name: str last_name: str email: str @post("/users") async def create_user_handler(data: UserDTO, request: Request) -> None: # do something here to create a new user # e.g. insert the user into a database await user_repository.insert(data) # assuming we have now inserted a user, we want to send a welcome email. # To do this in a none-blocking fashion, we will emit an event to a listener, which will send the email, # using a different async block than the one where we are returning a response. request.app.emit("user_created", email=data.email) app = Litestar( route_handlers=[create_user_handler], listeners=[send_welcome_email_handler] ) The above example illustrates the power of this pattern - it allows us to perform async operations without blocking, and without slowing down the response cycle. Listening to Multiple Events ++++++++++++++++++++++++++++ Event listeners can listen to multiple events: .. code-block:: python from litestar.events import listener @listener("user_created", "password_changed") async def send_email_handler(email: str, message: str) -> None: # do something here to send an email await send_email(email, message) Using Multiple Listeners ++++++++++++++++++++++++ You can also listen to the same events using multiple listeners: .. code-block:: python from uuid import UUID from dataclasses import dataclass from litestar import Request, post from litestar.events import listener from db import user_repository from utils.client import client from utils.email import send_farewell_email @listener("user_deleted") async def send_farewell_email_handler(email: str, **kwargs) -> None: # do something here to send an email await send_farewell_email(email) @listener("user_deleted") async def notify_customer_support(reason: str, **kwargs) -> None: # do something here to send an email await client.post("some-url", reason) @dataclass class DeleteUserDTO: email: str reason: str @post("/users") async def delete_user_handler(data: UserDTO, request: Request) -> None: await user_repository.delete({"email": email}) request.app.emit("user_deleted", email=data.email, reason="deleted") In the above example we are performing two side effect for the same event, one sends the user an email, and the other sending an HTTP request to a service management system to create an issue. Passing Arguments to Listeners ++++++++++++++++++++++++++++++ The method :meth:`emit ` has the following signature: .. code-block:: python def emit(self, event_id: str, *args: Any, **kwargs: Any) -> None: ... This means that it expects a string for ``event_id`` following by any number of positional and keyword arguments. While this is highly flexible, it also means you need to ensure the listeners for a given event can handle all the expected args and kwargs. For example, the following would raise an exception in python: .. code-block:: python @listener("user_deleted") async def send_farewell_email_handler(email: str) -> None: await send_farewell_email(email) @listener("user_deleted") async def notify_customer_support(reason: str) -> None: # do something here to send an email await client.post("some-url", reason) @dataclass class DeleteUserDTO: email: str reason: str @post("/users") async def delete_user_handler(data: UserDTO, request: Request) -> None: await user_repository.delete({"email": email}) request.app.emit("user_deleted", email=data.email, reason="deleted") The reason for this is that both listeners will receive two kwargs - ``email`` and ``reason``. To avoid this, the previous example had ``**kwargs`` in both: .. code-block:: python @listener("user_deleted") async def send_farewell_email_handler(email: str, **kwargs) -> None: await send_farewell_email(email) @listener("user_deleted") async def notify_customer_support(reason: str, **kwargs) -> None: await client.post("some-url", reason) Creating Event Emitters ----------------------- An "event emitter" is a class that inherits from :class:`BaseEventEmitterBackend `, which itself inherits from :obj:`contextlib.AbstractAsyncContextManager`. - :meth:`emit `: This is the method that performs the actual emitting logic. Additionally, the abstract ``__aenter__`` and ``__aexit__`` methods from :obj:`contextlib.AbstractAsyncContextManager` must be implemented, allowing the emitter to be used as an async context manager. By default Litestar uses the :class:`SimpleEventEmitter `, which offers an in-memory async queue. This solution works well if the system does not need to rely on complex behaviour, such as a retry mechanism, persistence, or scheduling/cron. For these more complex use cases, users should implement their own backend using either a DB/Key store that supports events (Redis, Postgres, etc.), or a message broker, job queue, or task queue technology. litestar-2.16.0/docs/usage/exceptions.rst000066400000000000000000000144021500564371300204030ustar00rootroot00000000000000Exceptions and exception handling ================================= Litestar define a base exception called :class:`LitestarException ` which serves as a base class for all other exceptions, see :mod:`API Reference `. In general, Litestar has two scenarios for exception handling: - Exceptions that are raised during application configuration, startup, and initialization, which are handled like regular Python exceptions - Exceptions that are raised as part of the request handling, i.e. exceptions in route handlers, dependencies, and middleware, that should be returned as a response to the end user Configuration Exceptions ------------------------ For missing extra dependencies, Litestar will raise either :class:`MissingDependencyException `. For example, if you try to use the :ref:`SQLAlchemyPlugin ` without having SQLAlchemy installed, this will be raised when you start the application. For other configuration issues, Litestar will raise :class:`ImproperlyConfiguredException ` with a message explaining the issue. Application Exceptions ---------------------- For application exceptions, Litestar uses the class :class:`~litestar.exceptions.http_exceptions.HTTPException`, which inherits from :class:`~litestar.exceptions.LitestarException`. This exception will be serialized into a JSON response of the following schema: .. code-block:: json { "status_code": 500, "detail": "Internal Server Error", "extra": {} } Litestar also offers several pre-configured ``HTTPException`` subclasses with pre-set error codes that you can use, such as: .. :currentmodule:: litestar.exceptions.http_exceptions +----------------------------------------+-------------+------------------------------------------+ | Exception | Status code | Description | +========================================+=============+==========================================+ | :class:`ImproperlyConfiguredException` | 500 | Used internally for configuration errors | +----------------------------------------+-------------+------------------------------------------+ | :class:`ValidationException` | 400 | Raised when validation or parsing failed | +----------------------------------------+-------------+------------------------------------------+ | :class:`NotAuthorizedException` | 401 | HTTP status code 401 | +----------------------------------------+-------------+------------------------------------------+ | :class:`PermissionDeniedException` | 403 | HTTP status code 403 | +----------------------------------------+-------------+------------------------------------------+ | :class:`NotFoundException` | 404 | HTTP status code 404 | +----------------------------------------+-------------+------------------------------------------+ | :class:`InternalServerException` | 500 | HTTP status code 500 | +----------------------------------------+-------------+------------------------------------------+ | :class:`ServiceUnavailableException` | 503 | HTTP status code 503 | +----------------------------------------+-------------+------------------------------------------+ .. :currentmodule:: None When a value fails validation, the result will be a :class:`~litestar.exceptions.http_exceptions.ValidationException` with the ``extra`` key set to the validation error message. .. warning:: All validation error messages will be made available for the API consumers by default. If this is not your intent, adjust the exception contents. Exception handling ------------------ Litestar handles all errors by default by transforming them into **JSON responses**. If the errors are **instances of** :class:`~litestar.exceptions.http_exceptions.HTTPException`, the responses will include the appropriate ``status_code``. Otherwise, the responses will default to ``500 - "Internal Server Error"``. The following handler for instance will default to ``MediaType.TEXT`` so the exception will be raised as text. .. literalinclude:: /examples/exceptions/implicit_media_type.py :language: python You can customize exception handling by passing a dictionary, mapping either status codes or exception classes to callables. For example, if you would like to replace the default exception handler with a handler that returns plain-text responses you could do this: .. literalinclude:: /examples/exceptions/override_default_handler.py :language: python The above will define a top level exception handler that will apply the ``plain_text_exception_handler`` function to all exceptions that inherit from ``HTTPException``. You could of course be more granular: .. literalinclude:: /examples/exceptions/per_exception_handlers.py :language: python The choice whether to use a single function that has switching logic inside it, or multiple functions depends on your specific needs. Exception handling layers ^^^^^^^^^^^^^^^^^^^^^^^^^ Since Litestar allows users to define both exception handlers and middlewares in a layered fashion, i.e. on individual route handlers, controllers, routers, or the app layer, multiple layers of exception handlers are required to ensure that exceptions are handled correctly: .. figure:: /images/exception-handlers.jpg :width: 400px Exception Handlers As a result of the above structure, the exceptions raised by the ASGI Router itself, namely ``404 Not Found`` and ``405 Method Not Allowed`` are handled only by exception handlers defined on the app layer. Thus, if you want to affect these exceptions, you will need to pass the exception handlers for them to the Litestar constructor and cannot use other layers for this purpose. Litestar supports defining exception handlers on all layers of the app, with the lower layers overriding layer above them. In the following example, the exception handler for the route handler function will only handle the ``ValidationException`` occurring within that route handler: .. literalinclude:: /examples/exceptions/layered_handlers.py :language: python litestar-2.16.0/docs/usage/htmx.rst000066400000000000000000000162721500564371300172110ustar00rootroot00000000000000HTMX ==== Litestar `HTMX `_ integration. HTMX is a JavaScript library that gives you access to AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext. This section assumes that you have prior knowledge of HTMX. If you want to learn HTMX, we recommend consulting their `official tutorial `_. HTMXPlugin ------------ a Litestar plugin ``HTMXPlugin`` is available to easily configure the default request class for all Litestar routes. .. code-block:: python from litestar.plugins.htmx import HTMXPlugin from litestar import Litestar from litestar.contrib.jinja import JinjaTemplateEngine from litestar.template.config import TemplateConfig from pathlib import Path app = Litestar( route_handlers=[get_form], debug=True, plugins=[HTMXPlugin()], template_config=TemplateConfig( directory=Path("litestar_htmx/templates"), engine=JinjaTemplateEngine, ), ) See :class:`~litestar.plugins.htmx.HTMXDetails` for a full list of available properties. HTMXRequest ------------ A special :class:`~litestar.connection.Request` class, providing interaction with the HTMX client. You can configure this globally by using the ``HTMXPlugin`` or by setting the `request_class` setting on any route, controller, router, or application. .. code-block:: python from litestar.plugins.htmx import HTMXRequest, HTMXTemplate from litestar import get, Litestar from litestar.response import Template from litestar.contrib.jinja import JinjaTemplateEngine from litestar.template.config import TemplateConfig from pathlib import Path @get(path="/form") def get_form(request: HTMXRequest) -> Template: if request.htmx: # if request has "HX-Request" header, then print(request.htmx) # HTMXDetails instance print(request.htmx.current_url) return HTMXTemplate(template_name="partial.html", context=context, push_url="/form") app = Litestar( route_handlers=[get_form], debug=True, request_class=HTMXRequest, template_config=TemplateConfig( directory=Path("litestar_htmx/templates"), engine=JinjaTemplateEngine, ), ) See :class:`~litestar.plugins.htmx.HTMXDetails` for a full list of available properties. HTMX Response Classes --------------------- HTMXTemplate Response Classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The most common use-case for HTMX to render an html page or html snippet. Litestar makes this easy by providing an :class:`~litestar.plugins.htmx.HTMXTemplate` response: .. code-block:: python from litestar.plugins.htmx import HTMXTemplate from litestar.response import Template @get(path="/form") def get_form( request: HTMXRequest, ) -> Template: # Return type is Template and not HTMXTemplate. ... return HTMXTemplate( template_name="partial.html", context=context, # Optional parameters push_url="/form", # update browser history re_swap="outerHTML", # change swapping method re_target="#new-target", # change target element trigger_event="showMessage", # trigger event name params={"alert": "Confirm your Choice."}, # parameter to pass to the event after="receive", # when to trigger event, # possible values 'receive', 'settle', and 'swap' ) .. note:: - Return type is litestar's ``Template`` and not ``HTMXTemplate``. - ``trigger_event``, ``params``, and ``after parameters`` are linked to one another. - If you are triggering an event then ``after`` is required and it must be one of ``receive``, ``settle``, or ``swap``. HTMX provides two types of responses - one that doesn't allow changes to the DOM and one that does. Litestar supports both of these: 1 - Responses that don't make any changes to DOM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Use :class:`~litestar.plugins.htmx.HXStopPolling` to stop polling for a response. .. code-block:: python @get("/") def handler() -> HXStopPolling: ... return HXStopPolling() Use :class:`~litestar.plugins.htmx.ClientRedirect` to redirect with a page reload. .. code-block:: python @get("/") def handler() -> ClientRedirect: ... return ClientRedirect(redirect_to="/contact-us") Use :class:`~litestar.plugins.htmx.ClientRefresh` to force a full page refresh. .. code-block:: python @get("/") def handler() -> ClientRefresh: ... return ClientRefresh() 2 - Responses that may change DOM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Use :class:`~litestar.plugins.htmx.HXLocation` to redirect to a new location without page reload. .. note:: This class provides the ability to change ``target``, ``swapping`` method, the sent ``values``, and the ``headers``. .. code-block:: python @get("/about") def handler() -> HXLocation: ... return HXLocation( redirect_to="/contact-us", # Optional parameters source, # the source element of the request. event, # an event that "triggered" the request. target="#target", # element id to target to. swap="outerHTML", # swapping method to use. hx_headers={"attr": "val"}, # headers to pass to HTMX. values={"val": "one"}, ) # values to submit with response. Use :class:`~litestar.plugins.htmx.PushUrl` to carry a response and push a url to the browser, optionally updating the ``history`` stack. .. note:: If the value for ``push_url`` is set to ``False`` it will prevent updating browser history. .. code-block:: python @get("/about") def handler() -> PushUrl: ... return PushUrl(content="Success!", push_url="/about") Use :class:`~litestar.plugins.htmx.ReplaceUrl` to carry a response and replace the url in the browser's ``location`` bar. .. note:: If the value to ``replace_url`` is set to ``False`` it will prevent updating the browser's location. .. code-block:: python @get("/contact-us") def handler() -> ReplaceUrl: ... return ReplaceUrl(content="Success!", replace_url="/contact-us") Use :class:`~litestar.plugins.htmx.Reswap` to carry a response with a possible swap. .. code-block:: python @get("/contact-us") def handler() -> Reswap: ... return Reswap(content="Success!", method="beforebegin") Use :class:`~litestar.plugins.htmx.Retarget` to carry a response and change the target element. .. code-block:: python @get("/contact-us") def handler() -> Retarget: ... return Retarget(content="Success!", target="#new-target") Use :class:`~litestar.plugins.htmx.TriggerEvent` to carry a response and trigger an event. .. code-block:: python @get("/contact-us") def handler() -> TriggerEvent: ... return TriggerEvent( content="Success!", name="showMessage", params={"attr": "value"}, after="receive", # possible values 'receive', 'settle', and 'swap' ) litestar-2.16.0/docs/usage/index.rst000066400000000000000000000007141500564371300173320ustar00rootroot00000000000000Usage ===== .. toctree:: :titlesonly: applications routing/index requests caching channels cli databases/index debugging dependency-injection dto/index events exceptions htmx lifecycle-hooks logging metrics/index middleware/index openapi/index plugins/index responses security/index static-files custom-types stores templating testing websockets litestar-2.16.0/docs/usage/lifecycle-hooks.rst000066400000000000000000000056431500564371300213110ustar00rootroot00000000000000Life Cycle Hooks ================ Life cycle hooks allow the execution of a callable at a certain point during the request-response cycle. The hooks available are: +--------------------------------------+------------------------------------+ | Name | Runs | +======================================+====================================+ | `before_request`_ | Before the router handler function | +--------------------------------------+------------------------------------+ | `after_request`_ | After the route handler function | +--------------------------------------+------------------------------------+ | `after_response`_ | After the response has been sent | +--------------------------------------+------------------------------------+ .. _before_request: Before Request -------------- The ``before_request`` hook runs immediately before calling the route handler function. It can be any callable accepting a :class:`~litestar.connection.Request` as its first parameter and returns either ``None`` or a value that can be used in a response. If a value is returned, the router handler for this request will be bypassed. .. literalinclude:: /examples/lifecycle_hooks/before_request.py :language: python .. _after_request: After Request ------------- The ``after_request`` hook runs after the route handler returned and the response object has been resolved. It can be any callable which takes a :class:`~litestar.response.Response` instance as its first parameter, and returns a ``Response`` instance. The ``Response`` instance returned does not necessarily have to be the one that was received. .. literalinclude:: /examples/lifecycle_hooks/after_request.py :language: python .. _after_response: After Response -------------- The ``after_response`` hook runs after the response has been returned by the server. It can be any callable accepting a :class:`~litestar.connection.Request` as its first parameter and does not return any value. This hook is meant for data post-processing, transmission of data to third party services, gathering of metrics, etc. .. literalinclude:: /examples/lifecycle_hooks/after_response.py :language: python .. note:: Since the request has already been returned by the time the ``after_response`` is called, the updated state of ``COUNTER`` is not reflected in the response. Layered hooks ------------- .. admonition:: Layered architecture Life cycle hooks are part of Litestar's layered architecture, which means you can set them on every layer of the application. If you set hooks on multiple layers, the layer closest to the route handler will take precedence. You can read more about this here: :ref:`Layered architecture ` .. literalinclude:: /examples/lifecycle_hooks/layered_hooks.py :language: python litestar-2.16.0/docs/usage/logging.rst000066400000000000000000000123051500564371300176500ustar00rootroot00000000000000.. _logging-usage: Logging ======= Application and request level loggers can be configured using the :class:`~litestar.logging.config.LoggingConfig`: .. code-block:: python import logging from litestar import Litestar, Request, get from litestar.logging import LoggingConfig @get("/") def my_router_handler(request: Request) -> None: request.logger.info("inside a request") return None logging_config = LoggingConfig( root={"level": "INFO", "handlers": ["queue_listener"]}, formatters={ "standard": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"} }, log_exceptions="always", ) app = Litestar(route_handlers=[my_router_handler], logging_config=logging_config) .. attention:: Litestar configures a non-blocking ``QueueListenerHandler`` which is keyed as ``queue_listener`` in the logging configuration. The above example is using this handler, which is optimal for async applications. Make sure to use it in your own loggers as in the above example. .. attention:: Exceptions won't be logged by default, except in debug mode. Make sure to use ``log_exceptions="always"`` as in the example above to log exceptions if you need it. Controlling Exception Logging ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ While ``log_exceptions`` controls when exceptions are logged, sometimes you may want to suppress stack traces for specific exception types or HTTP status codes. The ``disable_stack_trace`` parameter allows you to specify a set of exception types or status codes that should not generate stack traces in logs: .. code-block:: python from litestar import Litestar from litestar.logging import LoggingConfig # Don't log stack traces for 404 errors and ValueError exceptions logging_config = LoggingConfig( debug=True, disable_stack_trace={404, ValueError}, ) app = Litestar(logging_config=logging_config) This is particularly useful for common exceptions that you expect in normal operation and don't need detailed stack traces for. Using Python standard library ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `logging `_ is Python's builtin standard logging library and can be configured through ``LoggingConfig``. The ``LoggingConfig.configure()`` method returns a reference to ``logging.getLogger`` which can be used to access a logger instance. Thus, the root logger can retrieved with ``logging_config.configure()()`` as shown in the example below: .. code-block:: python import logging from litestar import Litestar, Request, get from litestar.logging import LoggingConfig logging_config = LoggingConfig( root={"level": "INFO", "handlers": ["queue_listener"]}, formatters={ "standard": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"} }, log_exceptions="always", ) logger = logging_config.configure()() @get("/") def my_router_handler(request: Request) -> None: request.logger.info("inside a request") logger.info("here too") app = Litestar( route_handlers=[my_router_handler], logging_config=logging_config, ) The above example is the same as using logging without the litestar ``LoggingConfig``. .. code-block:: python import logging from litestar import Litestar, Request, get from litestar.logging.config import LoggingConfig def get_logger(mod_name: str) -> logging.Logger: """Return logger object.""" format = "%(asctime)s: %(name)s: %(levelname)s: %(message)s" logger = logging.getLogger(mod_name) # Writes to stdout ch = logging.StreamHandler() ch.setLevel(logging.INFO) ch.setFormatter(logging.Formatter(format)) logger.addHandler(ch) return logger logger = get_logger(__name__) @get("/") def my_router_handler(request: Request) -> None: logger.info("logger inside a request") app = Litestar( route_handlers=[my_router_handler], ) Using Picologging ^^^^^^^^^^^^^^^^^ `Picologging `_ is a high performance logging library that is developed by Microsoft. Litestar will default to using this library automatically if its installed - requiring zero configuration on the part of the user. That is, if ``picologging`` is present the previous example will work with it automatically. Using StructLog ^^^^^^^^^^^^^^^ `StructLog `_ is a powerful structured-logging library. Litestar ships with a dedicated logging plugin and config for using it: .. code-block:: python from litestar import Litestar, Request, get from litestar.plugins.structlog import StructlogPlugin @get("/") def my_router_handler(request: Request) -> None: request.logger.info("inside a request") return None structlog_plugin = StructlogPlugin() app = Litestar(route_handlers=[my_router_handler], plugins=[StructlogPlugin()]) Subclass Logging Configs ^^^^^^^^^^^^^^^^^^^^^^^^ You can easily create you own ``LoggingConfig`` class by subclassing :class:`BaseLoggingConfig <.logging.config.BaseLoggingConfig>` and implementing the ``configure`` method. litestar-2.16.0/docs/usage/metrics/000077500000000000000000000000001500564371300171355ustar00rootroot00000000000000litestar-2.16.0/docs/usage/metrics/index.rst000066400000000000000000000001221500564371300207710ustar00rootroot00000000000000Metrics ======= .. toctree:: :titlesonly: open-telemetry prometheus litestar-2.16.0/docs/usage/metrics/open-telemetry.rst000066400000000000000000000026641500564371300226500ustar00rootroot00000000000000OpenTelemetry ============= Litestar includes optional OpenTelemetry instrumentation that is exported from ``litestar.contrib.opentelemetry``. To use this package, you should first install the required dependencies: .. code-block:: bash :caption: as separate package pip install opentelemetry-instrumentation-asgi .. code-block:: bash :caption: as a Litestar extra pip install litestar[opentelemetry] Once these requirements are satisfied, you can instrument your Litestar application by creating an instance of :class:`OpenTelemetryConfig ` and passing the middleware it creates to the Litestar constructor: .. code-block:: python from litestar import Litestar from litestar.contrib.opentelemetry import OpenTelemetryConfig, OpenTelemetryPlugin open_telemetry_config = OpenTelemetryConfig() app = Litestar(plugins=[OpenTelemetryPlugin(open_telemetry_config)]) The above example will work out of the box if you configure a global ``tracer_provider`` and/or ``metric_provider`` and an exporter to use these (see the `OpenTelemetry Exporter docs `_ for further details). You can also pass con figuration to the ``OpenTelemetryConfig`` telling it which providers to use. Consult :class:`reference docs ` regarding the configuration options you can use. litestar-2.16.0/docs/usage/metrics/prometheus.rst000066400000000000000000000014741500564371300220700ustar00rootroot00000000000000Prometheus ========== Litestar includes optional Prometheus exporter that is exported from ``litestar.plugins.prometheus``. To use this package, you should first install the required dependencies: .. code-block:: bash :caption: as separate package pip install prometheus-client .. code-block:: bash :caption: as a Litestar extra pip install litestar[prometheus] Once these requirements are satisfied, you can instrument your Litestar application: .. literalinclude:: /examples/plugins/prometheus/using_prometheus_exporter.py :language: python :caption: Using the Prometheus Exporter You can also customize the configuration: .. literalinclude:: /examples/plugins/prometheus/using_prometheus_exporter_with_extra_configs.py :language: python :caption: Configuring the Prometheus Exporter litestar-2.16.0/docs/usage/middleware/000077500000000000000000000000001500564371300176045ustar00rootroot00000000000000litestar-2.16.0/docs/usage/middleware/builtin-middleware.rst000066400000000000000000000337761500564371300241370ustar00rootroot00000000000000Built-in middleware =================== CORS ---- `CORS (Cross-Origin Resource Sharing) `_ is a common security mechanism that is often implemented using middleware. To enable CORS in a litestar application simply pass an instance of :class:`~litestar.config.cors.CORSConfig` to :class:`~litestar.app.Litestar`: .. code-block:: python from litestar import Litestar from litestar.config.cors import CORSConfig cors_config = CORSConfig(allow_origins=["https://www.example.com"]) app = Litestar(route_handlers=[...], cors_config=cors_config) CSRF ---- `CSRF (Cross-site request forgery) `_ is a type of attack where unauthorized commands are submitted from a user that the web application trusts. This attack often uses social engineering that tricks the victim into clicking a URL that contains a maliciously crafted, unauthorized request for a particular Web application. The user’s browser then sends this maliciously crafted request to the targeted Web application. If the user is in an active session with the Web application, the application treats this new request as an authorized request submitted by the user. Thus, the attacker can force the user to perform an action the user didn't intend, for example: .. code-block:: text POST /send-money HTTP/1.1 Host: target.web.app Content-Type: application/x-www-form-urlencoded amount=1000usd&to=attacker@evil.com This middleware prevents CSRF attacks by doing the following: 1. On the first "safe" request (e.g GET) - set a cookie with a special token created by the server 2. On each subsequent "unsafe" request (e.g POST) - make sure the request contains either a form field or an additional header that has this token (more on this below) To enable CSRF protection in a Litestar application simply pass an instance of :class:`~litestar.config.csrf.CSRFConfig` to the Litestar constructor: .. code-block:: python from litestar import Litestar, get, post from litestar.config.csrf import CSRFConfig @get() async def get_resource() -> str: # GET is one of the safe methods return "some_resource" @post("{id:int}") async def create_resource(id: int) -> bool: # POST is one of the unsafe methods return True csrf_config = CSRFConfig(secret="my-secret") app = Litestar([get_resource, create_resource], csrf_config=csrf_config) The following snippet demonstrates how to change the cookie name to ``"some-cookie-name"`` and header name to ``"some-header-name"``. .. code-block:: python csrf_config = CSRFConfig(secret="my-secret", cookie_name='some-cookie-name', header_name='some-header-name') A CSRF protected route can be accessed by any client that can make a request with either the header or form-data key. .. note:: The form-data key can not be currently configured. It should only be passed via the key ``"_csrf_token"`` In Python, any client such as `requests `_ or `httpx `_ can be used. The usage of clients or sessions is recommended due to the cookie persistence it offers across requests. The following is an example using `httpx.Client `_. .. code-block:: python import httpx with httpx.Client() as client: get_response = client.get("http://localhost:8000/") # "csrftoken" is the default cookie name csrf = get_response.cookies["csrftoken"] # "x-csrftoken" is the default header name post_response_using_header = client.post("http://localhost:8000/1", headers={"x-csrftoken": csrf}) assert post_response_using_header.status_code == 201 # "_csrf_token" is the default *non* configurable form-data key post_response_using_form_data = client.post("http://localhost:8000/1", data={"_csrf_token": csrf}) assert post_response_using_form_data.status_code == 201 # despite the header being passed, this request will fail as it does not have a cookie in its session # note the usage of ``httpx.post`` instead of ``client.post`` post_response_with_no_persisted_cookie = httpx.post("http://localhost:8000/1", headers={"x-csrftoken": csrf}) assert post_response_with_no_persisted_cookie.status_code == 403 assert "CSRF token verification failed" in post_response_with_no_persisted_cookie.text Routes can be marked as being exempt from the protection offered by this middleware via :ref:`handler opts ` .. code-block:: python @post("/post", exclude_from_csrf=True) def handler() -> None: ... If you need to exempt many routes at once you might want to consider using the :attr:`~litestar.config.csrf.CSRFConfig.exclude` kwarg which accepts list of path patterns to skip in the middleware. .. seealso:: * `Safe and Unsafe (HTTP Methods) `_ * `HTTPX Clients `_ * `Requests Session `_ Allowed Hosts ------------- Another common security mechanism is to require that each incoming request has a ``"Host"`` or ``"X-Forwarded-Host"`` header, and then to restrict hosts to a specific set of domains - what's called "allowed hosts". Litestar includes an :class:`~litestar.middleware.allowed_hosts.AllowedHostsMiddleware` class that can be easily enabled by either passing an instance of :class:`~litestar.config.allowed_hosts.AllowedHostsConfig` or a list of domains to :class:`~litestar.app.Litestar`: .. code-block:: python from litestar import Litestar from litestar.config.allowed_hosts import AllowedHostsConfig app = Litestar( route_handlers=[...], allowed_hosts=AllowedHostsConfig( allowed_hosts=["*.example.com", "www.wikipedia.org"] ), ) .. note:: You can use wildcard prefixes (``*.``) in the beginning of a domain to match any combination of subdomains. Thus, ``*.example.com`` will match ``www.example.com`` but also ``x.y.z.example.com`` etc. You can also simply put ``*`` in trusted hosts, which means allow all. This is akin to turning the middleware off, so in this case it may be better to not enable it in the first place. You should note that a wildcard can only be used only in the prefix of a domain name, not in the middle or end. Doing so will result in a validation exception being raised. Compression ----------- HTML responses can optionally be compressed. Litestar has built in support for gzip and brotli. Gzip support is provided through the built-in Starlette classes, and brotli support can be added by installing the ``brotli`` extras. You can enable either backend by passing an instance of :class:`~litestar.config.compression.CompressionConfig` to ``compression_config`` of :class:`~litestar.app.Litestar`. GZIP ^^^^ You can enable gzip compression of responses by passing an instance of :class:`~litestar.config.compression.CompressionConfig` with the ``backend`` parameter set to ``"gzip"``. You can configure the following additional gzip-specific values: * ``minimum_size``: the minimum threshold for response size to enable compression. Smaller responses will not be compressed. Defaults is ``500``, i.e. half a kilobyte. * ``gzip_compress_level``: a range between 0-9, see the `official python docs `_. Defaults to ``9`` , which is the maximum value. .. code-block:: python from litestar import Litestar from litestar.config.compression import CompressionConfig app = Litestar( route_handlers=[...], compression_config=CompressionConfig(backend="gzip", gzip_compress_level=9), ) Brotli ^^^^^^ The `Brotli `_ package is required to run this middleware. It is available as an extras to litestar with the ``brotli`` extra (``pip install litestar[brotli]``). You can enable brotli compression of responses by passing an instance of :class:`~litestar.config.compression.CompressionConfig` with the ``backend`` parameter set to ``"brotli"``. You can configure the following additional brotli-specific values: * ``minimum_size``: the minimum threshold for response size to enable compression. Smaller responses will not be compressed. Default is 500, i.e. half a kilobyte * ``brotli_quality``: Range [0-11], Controls the compression-speed vs compression-density tradeoff. The higher the quality, the slower the compression. Defaults to 5 * ``brotli_mode``: The compression mode can be ``"generic"`` (for mixed content), ``"text"`` (for UTF-8 format text input), or ``"font"`` (for WOFF 2.0). Defaults to ``"text"`` * ``brotli_lgwin``: Base 2 logarithm of size. Range [10-24]. Defaults to 22. * ``brotli_lgblock``: Base 2 logarithm of the maximum input block size. Range [16-24]. If set to 0, the value will be set based on the quality. Defaults to 0 * ``brotli_gzip_fallback``: a boolean to indicate if gzip should be used if brotli is not supported .. code-block:: python from litestar import Litestar from litestar.config.compression import CompressionConfig app = Litestar( route_handlers=[...], compression_config=CompressionConfig(backend="brotli", brotli_gzip_fallback=True), ) Rate-Limit Middleware --------------------- Litestar includes an optional :class:`~litestar.middleware.rate_limit.RateLimitMiddleware` that follows the `IETF RateLimit draft specification `_. To use the rate limit middleware, use the :class:`~litestar.middleware.rate_limit.RateLimitConfig`: .. literalinclude:: /examples/middleware/rate_limit.py :language: python The only required configuration kwarg is ``rate_limit``, which expects a tuple containing a time-unit (``"second"``, ``"minute"``, ``"hour"``, ``"day"``\ ) and a value for the request quota (integer). Logging Middleware ------------------ Litestar ships with a robust logging middleware that allows logging HTTP request and responses while building on the Litestar's :ref:`logging configuration `: .. literalinclude:: /examples/middleware/logging_middleware.py :language: python The logging middleware uses the logger configuration defined on the application level, which allows for using any supported logging tool, depending on the configuration used (see :ref:`logging configuration ` for more details). Obfuscating Logging Output ^^^^^^^^^^^^^^^^^^^^^^^^^^ Sometimes certain data, e.g. request or response headers, needs to be obfuscated. This is supported by the middleware configuration: .. code-block:: python from litestar.middleware.logging import LoggingMiddlewareConfig logging_middleware_config = LoggingMiddlewareConfig( request_cookies_to_obfuscate={"my-custom-session-key"}, response_cookies_to_obfuscate={"my-custom-session-key"}, request_headers_to_obfuscate={"my-custom-header"}, response_headers_to_obfuscate={"my-custom-header"}, ) The middleware will obfuscate the headers ``Authorization`` and ``X-API-KEY`` , and the cookie ``session`` by default. Compression and Logging of Response Body ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If both :class:`~litestar.config.compression.CompressionConfig` and :class:`~litestar.middleware.logging.LoggingMiddleware` have been defined for the application, the response body will be omitted from response logging if it has been compressed, even if ``"body"`` has been included in :class:`~litestar.middleware.logging.LoggingMiddlewareConfig.response_log_fields`. To force the body of compressed responses to be logged, set :attr:`~litestar.middleware.logging.LoggingMiddlewareConfig.include_compressed_body` to ``True`` , in addition to including ``"body"`` in ``response_log_fields``. Session Middleware ------------------ Litestar includes a :class:`~litestar.middleware.session.base.SessionMiddleware`, offering client- and server-side sessions. Server-side sessions are backed by Litestar's :doc:`stores `, which offer support for: - In memory sessions - File based sessions - Redis based sessions Setting up the middleware ^^^^^^^^^^^^^^^^^^^^^^^^^ To start using sessions in your application all you have to do is create an instance of a :class:`configuration ` object and add its middleware to your application's middleware stack: .. literalinclude:: /examples/middleware/session/cookies_full_example.py :caption: Hello World :language: python .. note:: Since both client- and server-side sessions rely on cookies (one for storing the actual session data, the other for storing the session ID), they share most of the cookie configuration. A complete reference of the cookie configuration can be found at :class:`~litestar.middleware.session.base.BaseBackendConfig`. Client-side sessions ^^^^^^^^^^^^^^^^^^^^ Client side sessions are available through the :class:`~litestar.middleware.session.client_side.ClientSideSessionBackend`, which offers strong AES-CGM encryption security best practices while support cookie splitting. .. important:: ``ClientSideSessionBackend`` requires the `cryptography `_ library, which can be installed together with litestar as an extra using ``pip install litestar[cryptography]`` .. literalinclude:: /examples/middleware/session/cookie_backend.py :caption: ``cookie_backend.py`` :language: python .. seealso:: * :class:`~litestar.middleware.session.client_side.CookieBackendConfig` Server-side sessions ^^^^^^^^^^^^^^^^^^^^ Server side session store data - as the name suggests - on the server instead of the client. They use a cookie containing a session ID which is a randomly generated string to identify a client and load the appropriate data from the store .. literalinclude:: /examples/middleware/session/file_store.py .. seealso:: * :doc:`/usage/stores` * :class:`~litestar.middleware.session.server_side.ServerSideSessionConfig` litestar-2.16.0/docs/usage/middleware/creating-middleware.rst000066400000000000000000000276041500564371300242560ustar00rootroot00000000000000 Creating Middleware =================== As mentioned in :ref:`using middleware `, a middleware in Litestar is **any callable** that takes a kwarg called ``app``, which is the next ASGI handler, i.e. an :class:`~litestar.types.ASGIApp`, and returns an ``ASGIApp``. The example previously given was using a factory function, i.e.: .. code-block:: python from litestar.types import ASGIApp, Scope, Receive, Send def middleware_factory(app: ASGIApp) -> ASGIApp: async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None: # do something here ... await app(scope, receive, send) return my_middleware Extending ``ASGIMiddleware`` ---------------------------- While using functions is a perfectly viable approach, the recommended way to handle this is by using the :class:`~litestar.middleware.ASGIMiddleware` abstract base class, which also includes functionality to dynamically skip the middleware based on ASGI ``scope["type"]``, handler ``opt`` keys or path patterns and a simple way to pass configuration to middlewares; It does not implement an ``__init__`` method, so subclasses are free to use it to customize the middleware's configuration. Modifying Requests and Responses ++++++++++++++++++++++++++++++++ Middlewares can not only be used to execute *around* other ASGI callable, they can also intercept and modify both incoming and outgoing data in a request / response cycle by "wrapping" the respective ``receive`` and ``send`` ASGI callables. The following demonstrates how to add a request timing header with a timestamp to all outgoing responses: .. literalinclude:: /examples/middleware/request_timing.py :language: python Migrating from ``MiddlewareProtocol`` / ``AbstractMiddleware`` ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ :class:`~litestar.middleware.ASGIMiddleware` was introduced in Litestar 2.15. If you've been using ``MiddlewareProtocol`` / ``AbstractMiddleware`` to implement your middlewares before, there's a simple migration path to using ``ASGIMiddleware``. **From MiddlewareProtocol** .. tab-set:: .. tab-item:: MiddlewareProtocol .. literalinclude:: /examples/middleware/middleware_protocol_migration_old.py :language: python .. tab-item:: ASGIMiddleware .. literalinclude:: /examples/middleware/middleware_protocol_migration_new.py :language: python **From AbstractMiddleware** .. tab-set:: .. tab-item:: MiddlewareProtocol .. literalinclude:: /examples/middleware/abstract_middleware_migration_old.py :language: python .. tab-item:: ASGIMiddleware .. literalinclude:: /examples/middleware/abstract_middleware_migration_new.py :language: python Using MiddlewareProtocol ------------------------ The :class:`~litestar.middleware.base.MiddlewareProtocol` class is a `PEP 544 Protocol `_ that specifies the minimal implementation of a middleware as follows: .. code-block:: python from typing import Protocol, Any from litestar.types import ASGIApp, Scope, Receive, Send class MiddlewareProtocol(Protocol): def __init__(self, app: ASGIApp, **kwargs: Any) -> None: ... async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: ... The ``__init__`` method receives and sets "app". *It's important to understand* that app is not an instance of Litestar in this case, but rather the next middleware in the stack, which is also an ASGI app. The ``__call__`` method makes this class into a ``callable``, i.e. once instantiated this class acts like a function, that has the signature of an ASGI app: The three parameters, ``scope, receive, send`` are specified by `the ASGI specification `_, and their values originate with the ASGI server (e.g. ``uvicorn``\ ) used to run Litestar. To use this protocol as a basis, simply subclass it - as you would any other class, and implement the two methods it specifies: .. code-block:: python import logging from litestar.types import ASGIApp, Receive, Scope, Send from litestar import Request from litestar.middleware.base import MiddlewareProtocol logger = logging.getLogger(__name__) class MyRequestLoggingMiddleware(MiddlewareProtocol): def __init__(self, app: ASGIApp) -> None: # can have other parameters as well self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": request = Request(scope) logger.info("Got request: %s - %s", request.method, request.url) await self.app(scope, receive, send) .. important:: Although ``scope`` is used to create an instance of request by passing it to the :class:`~litestar.connection.Request` constructor, which makes it simpler to access because it does some parsing for you already, the actual source of truth remains ``scope`` - not the request. If you need to modify the data of the request you must modify the scope object, not any ephemeral request objects created as in the above. Responding using the MiddlewareProtocol +++++++++++++++++++++++++++++++++++++++ Once a middleware finishes doing whatever its doing, it should pass ``scope``, ``receive``, and ``send`` to an ASGI app and await it. This is what's happening in the above example with: ``await self.app(scope, receive, send)``. Let's explore another example - redirecting the request to a different url from a middleware: .. code-block:: python from litestar.types import ASGIApp, Receive, Scope, Send from litestar.response.redirect import ASGIRedirectResponse from litestar import Request from litestar.middleware.base import MiddlewareProtocol class RedirectMiddleware(MiddlewareProtocol): def __init__(self, app: ASGIApp) -> None: self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if Request(scope).session is None: response = ASGIRedirectResponse(path="/login") await response(scope, receive, send) else: await self.app(scope, receive, send) As you can see in the above, given some condition (``request.session`` being ``None``) we create a :class:`~litestar.response.redirect.ASGIRedirectResponse` and then await it. Otherwise, we await ``self.app`` Modifying ASGI Requests and Responses using the MiddlewareProtocol ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ .. important:: If you'd like to modify a :class:`~litestar.response.Response` object after it was created for a route handler function but before the actual response message is transmitted, the correct place to do this is using the special life-cycle hook called :ref:`after_request `. The instructions in this section are for how to modify the ASGI response message itself, which is a step further in the response process. Using the :class:`~litestar.middleware.base.MiddlewareProtocol` you can intercept and modifying both the incoming and outgoing data in a request / response cycle by "wrapping" that respective ``receive`` and ``send`` ASGI functions. To demonstrate this, let's say we want to append a header with a timestamp to all outgoing responses. We could achieve this by doing the following: .. code-block:: python import time from litestar.datastructures import MutableScopeHeaders from litestar.types import Message, Receive, Scope, Send from litestar.middleware.base import MiddlewareProtocol from litestar.types import ASGIApp class ProcessTimeHeader(MiddlewareProtocol): def __init__(self, app: ASGIApp) -> None: super().__init__(app) self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": start_time = time.monotonic() async def send_wrapper(message: Message) -> None: if message["type"] == "http.response.start": process_time = time.monotonic() - start_time headers = MutableScopeHeaders.from_message(message=message) headers["X-Process-Time"] = str(process_time) await send(message) await self.app(scope, receive, send_wrapper) else: await self.app(scope, receive, send) Inheriting AbstractMiddleware ----------------------------- Litestar offers an :class:`~litestar.middleware.base.AbstractMiddleware` class that can be extended to create middleware: .. code-block:: python import time from litestar.enums import ScopeType from litestar.middleware import AbstractMiddleware from litestar.datastructures import MutableScopeHeaders from litestar.types import Message, Receive, Scope, Send class MyMiddleware(AbstractMiddleware): scopes = {ScopeType.HTTP} exclude = ["first_path", "second_path"] exclude_opt_key = "exclude_from_middleware" async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: start_time = time.monotonic() async def send_wrapper(message: "Message") -> None: if message["type"] == "http.response.start": process_time = time.monotonic() - start_time headers = MutableScopeHeaders.from_message(message=message) headers["X-Process-Time"] = str(process_time) await send(message) await self.app(scope, receive, send_wrapper) The three class variables defined in the above example ``scopes``, ``exclude``, and ``exclude_opt_key`` can be used to fine-tune for which routes and request types the middleware is called: - The scopes variable is a set that can include either or both : ``ScopeType.HTTP`` and ``ScopeType.WEBSOCKET`` , with the default being both. - ``exclude`` accepts either a single string or list of strings that are compiled into a regex against which the request's ``path`` is checked. - ``exclude_opt_key`` is the key to use for in a route handler's :class:`Router.opt ` dict for a boolean, whether to omit from the middleware. Thus, in the following example, the middleware will only run against the handler called ``not_excluded_handler`` for ``/greet`` route: .. literalinclude:: /examples/middleware/base.py :language: python .. danger:: Using ``/`` as an exclude pattern, will disable this middleware for all routes, since, as a regex, it matches *every* path Using DefineMiddleware to pass arguments ---------------------------------------- Litestar offers a simple way to pass positional arguments (``*args``) and keyword arguments (``**kwargs``) to middleware using the :class:`~litestar.middleware.base.DefineMiddleware` class. Let's extend the factory function used in the examples above to take some args and kwargs and then use ``DefineMiddleware`` to pass these values to our middleware: .. code-block:: python from litestar.types import ASGIApp, Scope, Receive, Send from litestar import Litestar from litestar.middleware import DefineMiddleware def middleware_factory(my_arg: int, *, app: ASGIApp, my_kwarg: str) -> ASGIApp: async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None: # here we can use my_arg and my_kwarg for some purpose ... await app(scope, receive, send) return my_middleware app = Litestar( route_handlers=[...], middleware=[DefineMiddleware(middleware_factory, 1, my_kwarg="abc")], ) The ``DefineMiddleware`` is a simple container - it takes a middleware callable as a first parameter, and then any positional arguments, followed by key word arguments. The middleware callable will be called with these values as well as the kwarg ``app`` as mentioned above. litestar-2.16.0/docs/usage/middleware/index.rst000066400000000000000000000011731500564371300214470ustar00rootroot00000000000000Middleware ========== Middlewares in Litestar are ASGI apps that are called "in the middle" between the application entrypoint and the route handler function. Litestar ships with several builtin middlewares that are easy to configure and use. See :doc:`the documentation regarding these
` for more details. .. seealso:: If you're coming from Starlette / FastAPI, take a look at the migration guide: * :ref:`Migration - FastAPI/Starlette - Middlewares ` .. toctree:: :titlesonly: using-middleware builtin-middleware creating-middleware litestar-2.16.0/docs/usage/middleware/using-middleware.rst000066400000000000000000000070611500564371300236020ustar00rootroot00000000000000.. _using-middleware: Using Middleware ================ A middleware in Litestar is any callable that receives at least one kwarg called ``app`` and returns an :class:`ASGIApp `. An ``ASGIApp`` is nothing but an async function that receives the ASGI primitives ``scope`` , ``receive`` and ``send`` , and either calls the next ``ASGIApp`` or returns a response / handles the websocket connection. For example, the following function can be used as a middleware because it receives the ``app`` kwarg and returns an ``ASGIApp``: .. code-block:: python from litestar.types import ASGIApp, Scope, Receive, Send def middleware_factory(app: ASGIApp) -> ASGIApp: async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None: # do something here ... await app(scope, receive, send) return my_middleware We can then pass this middleware to the :class:`Litestar <.app.Litestar>` instance, where it will be called on every request: .. code-block:: python from litestar.types import ASGIApp, Scope, Receive, Send from litestar import Litestar def middleware_factory(app: ASGIApp) -> ASGIApp: async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None: # do something here ... await app(scope, receive, send) return my_middleware app = Litestar(route_handlers=[...], middleware=[middleware_factory]) In the above example, Litestar will call the ``middleware_factory`` function and pass to it ``app``. It's important to understand that this kwarg does not designate the Litestar application but rather the next ``ASGIApp`` in the stack. It will then insert the returned ``my_middleware`` function into the stack of every route in the application - because we declared it on the application level. .. admonition:: Layered architecture :class: seealso Middlewares are part of Litestar's layered architecture* which means you can set them on every layer of the application. You can read more about this here: :ref:`usage/applications:layered architecture` Middleware Call Order --------------------- Since it's also possible to define multiple middlewares on every layer, the call order for middlewares will be **top to bottom** and **left to right**. This means for each layer, the middlewares will be called in the order they have been passed, while the layers will be traversed in the usual order: .. mermaid:: flowchart LR Application --> Router --> Controller --> Handler .. literalinclude:: /examples/middleware/call_order.py :language: python Middlewares and Exceptions -------------------------- When an exception is raised by a route handler or a :doc:`dependency
` it will be transformed into a response by an :ref:`exception handler `. This response will follow the normal "flow" of the application and therefore, middlewares are still applied to it. As with any good rule, there are exceptions to it. In this case they are two exceptions raised by Litestar's ASGI router: * :class:`NotFoundException ` * :class:`MethodNotAllowedException ` They are raised **before the middleware stack is called** and will only be handled by exception handlers defined on the ``Litestar`` instance itself. If you wish to modify error responses generated from these exception, you will have to use an application layer exception handler. litestar-2.16.0/docs/usage/openapi/000077500000000000000000000000001500564371300171225ustar00rootroot00000000000000litestar-2.16.0/docs/usage/openapi/index.rst000066400000000000000000000021761500564371300207710ustar00rootroot00000000000000OpenAPI ======= Litestar has first class OpenAPI support offering the following features: - Automatic `OpenAPI 3.1.0 Schema `_ generation, which is available as both YAML and JSON. - Builtin support for static documentation site generation using several different libraries. - Full configuration using pre-defined type-safe dataclasses. Litestar includes a complete implementation of the `latest version of the OpenAPI specification `_ using Python dataclasses. This implementation is used as a basis for generating OpenAPI specs, supporting :func:`~dataclasses.dataclass`, :class:`~typing.TypedDict`, as well as Pydantic and msgspec models, and any 3rd party entities for which a :ref:`plugin ` is implemented. This is also highly configurable - and users can customize the OpenAPI spec in a variety of ways - ranging from passing configuration globally to setting :ref:`specific kwargs on route ` handler decorators. .. toctree:: schema_generation ui_plugins litestar-2.16.0/docs/usage/openapi/schema_generation.rst000066400000000000000000000256531500564371300233420ustar00rootroot00000000000000Configuring schema generation ----------------------------- OpenAPI schema generation is enabled by default. To configure it you can pass an instance of :class:`OpenAPIConfig <.openapi.OpenAPIConfig>` to the :class:`Litestar ` class using the ``openapi_config`` kwarg: .. code-block:: python from litestar import Litestar from litestar.openapi import OpenAPIConfig app = Litestar( route_handlers=[...], openapi_config=OpenAPIConfig(title="My API", version="1.0.0") ) Disabling schema generation +++++++++++++++++++++++++++ If you wish to disable schema generation and not include the schema endpoints in your API, simply pass ``None`` as the value for ``openapi_config``: .. code-block:: python from litestar import Litestar app = Litestar(route_handlers=[...], openapi_config=None) Configuring schema generation on a route handler ------------------------------------------------- By default, an `operation `_ schema is generated for all route handlers. You can omit a route handler from the schema by setting ``include_in_schema=False``: .. code-block:: python from litestar import get @get(path="/some-path", include_in_schema=False) def my_route_handler() -> None: ... You can also modify the generated schema for the route handler using the following kwargs: ``tags`` A list of strings that correlate to the `tag specification `_. ``security``: A list of dictionaries that correlate to the `security requirements specification `_. The values for this key are string keyed dictionaries with the values being a list of objects. ``summary`` Text used for the route's schema *summary* section. ``description`` Text used for the route's schema *description* section. ``response_description`` Text used for the route's response schema *description* section. ``operation_class`` A subclass of :class:`Operation <.openapi.spec.operation.Operation>` which can be used to fully customize the `operation object `_ for the handler. ``operation_id`` A string or callable that returns a string, which servers as an identifier used for the route's schema *operationId*. ``deprecated`` A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. Defaults to ``False``. ``raises`` A list of exception classes extending from ``litestar.HttpException``. This list should describe all exceptions raised within the route handler's function/method. The Litestar ``ValidationException`` will be added automatically for the schema if any validation is involved (e.g. there are parameters specified in the method/function). For custom exceptions, a `detail` class property should be defined, which will be integrated into the OpenAPI schema. If `detail` isn't specified and the exception's status code matches one from `stdlib status code `_, a generic message will be applied. ``responses`` A dictionary of additional status codes and a description of their expected content. The expected content should be based on a Pydantic model describing its structure. It can also include a description and the expected media type. For example: .. note:: `operation_id` will be prefixed with the method name when function is decorated with `HTTPRouteHandler` and multiple `http_method`. Will also be prefixed with path strings used in `Routers` and `Controllers` to make sure id is unique. .. code-block:: python from datetime import datetime from typing import Optional from pydantic import BaseModel from litestar import get from litestar.openapi.datastructures import ResponseSpec class Item(BaseModel): ... class ItemNotFound(BaseModel): was_removed: bool removed_at: Optional[datetime] @get( path="/items/{pk:int}", responses={ 404: ResponseSpec( data_container=ItemNotFound, description="Item was removed or not found" ) }, ) def retrieve_item(pk: int) -> Item: ... You can also specify ``security`` and ``tags`` on higher level of the application, e.g. on a controller, router, or the app instance itself. For example: .. code-block:: python from litestar import Litestar, get from litestar.openapi import OpenAPIConfig from litestar.openapi.spec import Components, SecurityScheme, Tag @get( "/public", tags=["public"], security=[{}], # this endpoint is marked as having optional security ) def public_path_handler() -> dict[str, str]: return {"hello": "world"} @get("/other", tags=["internal"], security=[{"apiKey": []}]) def internal_path_handler() -> None: ... app = Litestar( route_handlers=[public_path_handler, internal_path_handler], openapi_config=OpenAPIConfig( title="my api", version="1.0.0", tags=[ Tag(name="public", description="This endpoint is for external users"), Tag(name="internal", description="This endpoint is for internal users"), ], security=[{"BearerToken": []}], components=Components( security_schemes={ "BearerToken": SecurityScheme( type="http", scheme="bearer", ) }, ), ), ) Accessing the OpenAPI schema in code ------------------------------------ The OpenAPI schema is generated during the :class:`Litestar ` app's init method. Once init is finished, its accessible as ``app.openapi_schema``. As such you can always access it inside route handlers, dependencies, etc. by access the request instance: .. code-block:: python from litestar import Request, get @get(path="/") def my_route_handler(request: Request) -> dict: schema = request.app.openapi_schema return schema.dict() Customizing Pydantic model schemas ---------------------------------- You can customize the OpenAPI schemas generated for pydantic models by following the guidelines in the `Pydantic docs `_. Additionally, you can affect how pydantic models are translated into OpenAPI ``components`` by settings a special dunder attribute on the model called ``__schema_name__``: .. literalinclude:: /examples/openapi/customize_pydantic_model_name.py :caption: Customize Components Example :language: python The above will result in an OpenAPI schema object that looks like this: .. code-block:: json { "openapi": "3.1.0", "info": {"title": "Litestar API", "version": "1.0.0"}, "servers": [{"url": "/"}], "paths": { "/id": { "get": { "operationId": "Retrieve Id Handler", "responses": { "200": { "description": "Request fulfilled, document follows", "headers": {}, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IdContainer" } } } } }, "deprecated": false } } }, "components": { "schemas": { "IdContainer": { "properties": { "id": {"type": "string", "format": "uuid", "title": "Id"} }, "type": "object", "required": ["id"], "title": "IdContainer" } } } } .. attention:: If you use multiple pydantic models that use the same name in the schema, you will need to use the `__schema_name__` dunder to ensure each has a unique name in the schema, otherwise the schema components will be ambivalent. Customizing ``Operation`` class ------------------------------- You can customize the `operation object `_ used for a path in the generated OpenAPI schemas by creating a subclass of :class:`Operation <.openapi.spec.operation.Operation>`. This option can be helpful in situations where request data needs to be manually parsed as Litestar will not know how to create the OpenAPI operation data by default. .. literalinclude:: /examples/openapi/customize_operation_class.py :caption: Customize Components Example :language: python The above example will result in an OpenAPI schema object that looks like this: .. code-block:: json { "info": { "title": "Litestar API", "version": "1.0.0" }, "openapi": "3.0.3", "servers": [{ "url": "/" }], "paths": { "/": { "post": { "tags": ["ok"], "summary": "Route", "description": "Requires OK, Returns OK", "operationId": "Route", "requestBody": { "content": { "text": { "schema": { "type": "string", "title": "Body", "example": "OK" } } }, "description": "OK is the only accepted value", "required": false }, "responses": { "201": { "description": "Document created, URL follows", "headers": {} } }, "deprecated": false, "x-codeSamples": [ { "lang": "Python", "source": "import requests; requests.get('localhost/example')", "label": "Python" }, { "lang": "cURL", "source": "curl -XGET localhost/example", "label": "curl" } ] } } }, "components": { "schemas": {} } } .. attention:: OpenAPI Vendor Extension fields need to start with `x-` and should not be processed with the default field name converter. To work around this, Litestar will honor an `alias` field provided to the `dataclass.field `_ metadata when generating the field name in the schema. litestar-2.16.0/docs/usage/openapi/ui_plugins.rst000066400000000000000000000262221500564371300220360ustar00rootroot00000000000000OpenAPI UI Plugins ------------------ .. versionadded:: 2.8.0 OpenAPI UI Plugins are designed to allow easy integration with your OpenAPI UI framework of choice. These plugins facilitate the creation of interactive, user-friendly API documentation, making it easier for developers and end-users to understand and interact with your API. Litestar maintains and ships with UI plugins for a range of popular popular OpenAPI documentation tools: - `Scalar `_ - `RapiDoc `_ - `ReDoc `_ - `Stoplight Elements `_ - `Swagger UI `_ - `YAML `_ Each plugin is easily configurable, allowing developers to customize aspects like version, paths, CSS and JavaScript resources. Using OpenAPI UI Plugins ------------------------ Using OpenAPI UI Plugins is as simple as importing the plugin, instantiating it, and adding it to the OpenAPIConfig. .. tab-set:: .. tab-item:: scalar :sync: scalar .. literalinclude:: /examples/openapi/plugins/scalar_simple.py :language: python .. tab-item:: rapidoc :sync: rapidoc .. literalinclude:: /examples/openapi/plugins/rapidoc_simple.py :language: python .. tab-item:: redoc :sync: redoc .. literalinclude:: /examples/openapi/plugins/redoc_simple.py :language: python .. tab-item:: stoplight :sync: stoplight .. literalinclude:: /examples/openapi/plugins/stoplight_simple.py :language: python .. tab-item:: swagger :sync: swagger .. literalinclude:: /examples/openapi/plugins/swagger_ui_simple.py :language: python .. tab-item:: yaml :sync: yaml .. literalinclude:: /examples/openapi/plugins/yaml_simple.py :language: python .. tab-item:: multiple .. literalinclude:: /examples/openapi/plugins/serving_multiple_uis.py :caption: Any combination of UIs can be served. :language: python Configuring OpenAPI UI Plugins ------------------------------ Each plugin can be tailored to meet your unique requirements by passing options at instantiation. For full details on each plugin's options, see the :doc:`API Reference `. All plugins support: - ``path``: Each plugin has its own default, e.g., ``/rapidoc`` for RapiDoc. This can be overridden to serve the UI at a different path. - ``media_type``: The default media type for the plugin, typically the default is ``text/html``. - ``favicon``: A string that should be a valid ```` tag, e.g., ````. - ``style``: A string that should be a valid ``.`` Most plugins support the following additional options: - ``version``: The version of the UIs JS and (in some cases) CSS bundle to use. We use the ``version`` to construct the URL to retrieve the bundle from ``unpkg``, e.g., ``https://unpkg.com/rapidoc@/dist/rapidoc-min.js`` - ``js_url``: The URL to the JS bundle. If provided, this will override the ``version`` option. - ``css_url``: The URL to the CSS bundle. If provided, this will override the ``version`` option. Here's some example plugin configurations: .. tab-set:: .. tab-item:: scalar :sync: scalar .. literalinclude:: /examples/openapi/plugins/scalar_config.py :language: python .. tab-item:: rapidoc :sync: rapidoc .. literalinclude:: /examples/openapi/plugins/rapidoc_config.py :language: python .. tab-item:: redoc :sync: redoc .. literalinclude:: /examples/openapi/plugins/redoc_config.py :language: python .. tab-item:: stoplight :sync: stoplight .. literalinclude:: /examples/openapi/plugins/stoplight_config.py :language: python .. tab-item:: swagger :sync: swagger .. literalinclude:: /examples/openapi/plugins/swagger_ui_config.py :language: python Configuring the OpenAPI Root Path --------------------------------- The OpenAPI root path is the path at which the OpenAPI representations are served. By default, this is ``/schema``. This can be changed by setting the :attr:`OpenAPIConfig.path` attribute. In the following example, we configure the OpenAPI root path to be ``/docs``: .. literalinclude:: /examples/openapi/customize_path.py :language: python This will result in any of the OpenAPI endpoints being served at ``/docs`` instead of ``/schema``, e.g., ``/docs/openapi.json``. Backward Compatibility ---------------------- OpenAPI UI plugins are a new feature introduced in ``v2.8.0``. Providing a subclass of OpenAPIController ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: v2.8.0 The previous method of configuring elements such as the root path and styling was to subclass :class:`OpenAPIController`, and set it on the :attr:`OpenAPIConfig.openapi_controller` attribute. This approach is now deprecated and slated for removal in ``v3.0.0``, but if you are using it, there should be no change in behavior. To maintain backward compatibility with the previous approach, if neither the :attr:`OpenAPIConfig.openapi_controller` or :attr:`OpenAPIConfig.render_plugins` attributes are set, we will automatically add the plugins to respect the also deprecated :attr:`OpenAPIConfig.enabled_endpoints` attribute. By default, this will result in the following endpoints being enabled: - ``/schema/openapi.json`` - ``/schema/redoc`` - ``/schema/rapidoc`` - ``/schema/elements`` - ``/schema/swagger`` - ``/schema/openapi.yml`` - ``/schema/openapi.yaml`` In ``v3.0.0``, the :attr:`OpenAPIConfig.enabled_endpoints` attribute will be removed, and only a single UI plugin will be enabled by default, in addition to the ``openapi.json`` endpoint which will always be enabled. ``Scalar`` will also become the default UI plugin in ``v3.0.0``. To adopt the future behavior, explicitly set the :attr:`OpenAPIConfig.render_plugins` field to an instance of :class:`ScalarRenderPlugin`: .. literalinclude:: /examples/openapi/plugins/scalar_simple.py :language: python :lines: 13-21 Backward compatibility with ``root_schema_site`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Litestar has always supported a ``root_schema_site`` attribute on the :class:`OpenAPIConfig` class. This attribute allows you to elect to serve a UI at the OpenAPI root path, e.g., by default ``redoc`` would be served at both ``/schema`` and ``/schema/redoc``. In ``v3.0.0``, the ``root_schema_site`` attribute will be removed, and the first :class:`OpenAPIRenderPlugin` in the :attr:`OpenAPIConfig.render_plugins` list will be assigned to the ``/schema`` endpoint. As of ``v2.8.0``, if you explicitly use the new :attr:`OpenAPIConfig.render_plugins` attribute, you will be automatically opted in to the new behavior, and the ``root_schema_site`` attribute will be ignored. Building your own OpenAPI UI Plugin ----------------------------------- If Litestar does not have built-in support for your OpenAPI UI framework of choice, you can easily create your own plugin by subclassing :class:`OpenAPIRenderPlugin` and implementing the :meth:`OpenAPIRenderPlugin.render` method. To demonstrate building a custom plugin, we'll look at a plugin very similar to the :class:`ScalarRenderPlugin` that is maintained by Litestar. Here's the finished product: .. literalinclude:: /examples/openapi/plugins/custom_plugin.py :language: python Class definition ~~~~~~~~~~~~~~~~ The class ``ScalarRenderPlugin`` inherits from :class:`OpenAPIRenderPlugin`: .. literalinclude:: /examples/openapi/plugins/custom_plugin.py :language: python :lines: 10 ``__init__`` Constructor ~~~~~~~~~~~~~~~~~~~~~~~~ .. literalinclude:: /examples/openapi/plugins/custom_plugin.py :language: python :lines: 11-22 We support configuration via the following arguments: - ``version``: Specifies the version of RapiDoc to use. - ``js_url``: Custom URL to the RapiDoc JavaScript bundle. - ``css_url``: Custom URL to the RapiDoc CSS bundle. - ``path``: The URL path where the RapiDoc UI will be served. - ``**kwargs``: Captures additional arguments to pass to the superclass. And we construct a url for the Scalar JavaScript bundle if one is not provided: .. literalinclude:: /examples/openapi/plugins/custom_plugin.py :language: python :lines: 20 ``render()`` ~~~~~~~~~~~~ .. literalinclude:: /examples/openapi/plugins/custom_plugin.py :language: python :lines: 24 Finally we define the ``render`` method, which is called by Litestar to render the UI. It receives the a :class:`Request` object and the ``openapi_schema`` as a dictionary. Inside the ``render`` method, we construct the HTML to render the UI, and return it as a string. - ``head``: Defines the HTML ```` section, including the title from ``openapi_schema``, any additional styles (``self.style``), the favicon and custom style sheet if one is provided: .. literalinclude:: /examples/openapi/plugins/custom_plugin.py :language: python :lines: 25-35 - ``body``: Constructs the HTML ````, including a link to the OpenAPI JSON, and the JavaScript bundle: .. literalinclude:: /examples/openapi/plugins/custom_plugin.py :language: python :lines: 37-43 - Finally, returns a complete HTML document (as a byte string), combining head and body. .. literalinclude:: /examples/openapi/plugins/custom_plugin.py :language: python :lines: 45-51 Interacting with the ``Router`` ------------------------------- An instance of :class:`Router` is used to serve the OpenAPI endpoints and is made available to plugins via the :meth:`OpenAPIRenderPlugin.receive_router` method. This can be used for a variety of purposes, including adding additional routes to the ``Router``. .. literalinclude:: /examples/openapi/plugins/receive_router.py :language: python OAuth2 in Swagger UI -------------------- When using Swagger, OAuth2 settings can be configured via :attr:`swagger_ui_init_oauth `, which can be set to a dictionary containing the parameters described in the Swagger UI documentation for OAuth2 `here `_. With that, you can preset your clientId or enable PKCE support. .. literalinclude:: /examples/openapi/plugins/swagger_ui_oauth.py :language: python Customizing the OpenAPI UI -------------------------- Style and behavior of the OpenAPI UI can be customized by overriding the default ``css_url`` and ``js_url`` attributes on the render plugin class, for example: .. literalinclude:: /examples/openapi/plugins/scalar_customized.py :language: python To learn more about customizing the ``Scalar`` UI, see the `Scalar documentation `_. CDN and offline file support ---------------------------- Each plugin supports ``js_url`` and ``css_url`` attributes, which can be used to specify a custom URL to the JavaScript. These can be used to serve the JavaScript and CSS from a CDN, or to serve the files from a local directory. litestar-2.16.0/docs/usage/plugins/000077500000000000000000000000001500564371300171505ustar00rootroot00000000000000litestar-2.16.0/docs/usage/plugins/flash_messages.rst000066400000000000000000000066041500564371300226740ustar00rootroot00000000000000============== Flash Messages ============== .. versionadded:: 2.7.0 Flash messages are a powerful tool for conveying information to the user, such as success notifications, warnings, or errors through one-time messages alongside a response due to some kind of user action. They are typically used to display a message on the next page load and are a great way to enhance user experience by providing immediate feedback on their actions from things like form submissions. Registering the plugin ---------------------- The FlashPlugin can be easily integrated with different templating engines. Below are examples of how to register the ``FlashPlugin`` with ``Jinja2``, ``Mako``, and ``MiniJinja`` templating engines. .. tab-set:: .. tab-item:: Jinja2 :sync: jinja .. literalinclude:: /examples/plugins/flash_messages/jinja.py :language: python :caption: Registering the flash message plugin using the Jinja2 templating engine .. tab-item:: Mako :sync: mako .. literalinclude:: /examples/plugins/flash_messages/mako.py :language: python :caption: Registering the flash message plugin using the Mako templating engine .. tab-item:: MiniJinja :sync: minijinja .. literalinclude:: /examples/plugins/flash_messages/minijinja.py :language: python :caption: Registering the flash message plugin using the MiniJinja templating engine Using the plugin ---------------- After registering the FlashPlugin with your application, you can start using it to add and display flash messages within your application routes. Here is an example showing how to use the FlashPlugin with the Jinja2 templating engine to display flash messages. The same approach applies to Mako and MiniJinja engines as well. .. literalinclude:: /examples/plugins/flash_messages/usage.py :language: python :caption: Using the flash message plugin with Jinja2 templating engine to display flash messages Breakdown +++++++++ #. Here we import the requires classes and functions from the Litestar package and related plugins. #. Flash messages requires a valid session configuration, so we create and enable the ``ServerSideSession`` middleware. #. We then create our ``TemplateConfig`` and ``FlashConfig`` instances, each setting up the configuration for the template engine and flash messages, respectively. #. A single route handler named ``index`` is defined using the ``@get()`` decorator. * Within this handler, the ``flash`` function is called to add a new flash message. This message is stored in the request's context, making it accessible to the template engine for rendering in the response. * The function returns a ``Template`` instance, where ``template_str`` (read more about :ref:`template strings `) contains inline HTML and Jinja2 template code. This template dynamically displays any flash messages by iterating over them with a Jinja2 for loop. Each message is wrapped in a paragraph (``

``) tag, showing the message content and its category. #. Finally, a ``Litestar`` application instance is created, specifying the ``flash_plugin`` and ``index`` route handler in its configuration. The application is also configured with the ``template_config``, which includes the ``Jinja2`` templating engine and the path to the templates directory. litestar-2.16.0/docs/usage/plugins/index.rst000066400000000000000000000136271500564371300210220ustar00rootroot00000000000000.. _plugins: ======= Plugins ======= Litestar supports a plugin system that allows you to extend the functionality of the framework. .. seealso:: * :doc:`/usage/databases/sqlalchemy/plugins/index` Plugins are defined by protocols, and any type that satisfies a protocol can be included in the ``plugins`` argument of the :class:`app `. InitPlugin ---------- ``InitPlugin`` defines an interface that allows for customization of the application's initialization process. Init plugins can define dependencies, add route handlers, configure middleware, and much more! Implementations of these plugins must define a single method: :meth:`on_app_init(self, app_config: AppConfig) -> AppConfig: ` The method accepts and must return an :class:`AppConfig ` instance, which can be modified and is later used to instantiate the application. This method is invoked after any ``on_app_init`` hooks have been called, and each plugin is invoked in the order that they are provided in the ``plugins`` argument of the :class:`app `. Because of this, plugin authors should make it clear in their documentation if their plugin should be invoked before or after other plugins. Example +++++++ The following example shows a simple plugin that adds a route handler, and a dependency to the application. .. literalinclude:: /examples/plugins/init_plugin_protocol.py :language: python :caption: ``InitPlugin`` implementation example The ``MyPlugin`` class is an implementation of the :class:`InitPlugin `. It defines a single method, ``on_app_init()``, which takes an :class:`AppConfig ` instance as an argument and returns same. In the ``on_app_init()`` method, the dependency mapping is updated to include a new dependency named ``"name"``, which is provided by the ``get_name()`` function, and ``route_handlers`` is updated to include the ``route_handler()`` function. The modified :class:`AppConfig ` instance is then returned. SerializationPluginProtocol --------------------------- The SerializationPluginProtocol defines a contract for plugins that provide serialization functionality for data types that are otherwise unsupported by the framework. Implementations of these plugins must define the following methods. 1. :meth:`supports_type(self, field_definition: FieldDefinition) -> bool: ` The method takes a :class:`FieldDefinition ` instance as an argument and returns a :class:`bool` indicating whether the plugin supports serialization for that type. 2. :meth:`create_dto_for_type(self, field_definition: FieldDefinition) -> type[AbstractDTO]: ` This method accepts a :class:`FieldDefinition ` instance as an argument and must return a :class:`AbstractDTO ` implementation that can be used to serialize and deserialize the type. During application startup, if a data or return annotation is encountered that is not a supported type, is supported by the plugin, and doesn't otherwise have a ``dto`` or ``return_dto`` defined, the plugin is used to create a DTO type for that annotation. Example +++++++ The following example shows the actual implementation of the ``SerializationPluginProtocol`` for `SQLAlchemy `_ models that is is provided in ``advanced_alchemy``. .. literalinclude:: ../../../litestar/contrib/sqlalchemy/plugins/serialization.py :language: python :caption: ``SerializationPluginProtocol`` implementation example :meth:`supports_type(self, field_definition: FieldDefinition) -> bool: ` returns a :class:`bool` indicating whether the plugin supports serialization for the given type. Specifically, we return ``True`` if the parsed type is either a collection of SQLAlchemy models or a single SQLAlchemy model. :meth:`create_dto_for_type(self, field_definition: FieldDefinition) -> type[AbstractDTO]: ` takes a :class:`FieldDefinition ` instance as an argument and returns a :class:`SQLAlchemyDTO ` subclass and includes some logic that may be interesting to potential serialization plugin authors. The first thing the method does is check if the parsed type is a collection of SQLAlchemy models or a single SQLAlchemy model, retrieves the model type in either case and assigns it to the ``annotation`` variable. The method then checks if ``annotation`` is already in the ``_type_dto_map`` dictionary. If it is, it returns the corresponding DTO type. This is done to ensure that multiple :class:`SQLAlchemyDTO ` subtypes are not created for the same model. If the annotation is not in the ``_type_dto_map`` dictionary, the method creates a new DTO type for the annotation, adds it to the ``_type_dto_map`` dictionary, and returns it. DIPlugin -------- :class:`~litestar.plugins.DIPlugin` can be used to extend Litestar's dependency injection by providing information about injectable types. Its main purpose it to facilitate the injection of callables with unknown signatures, for example Pydantic's ``BaseModel`` classes; These are not supported natively since, while they are callables, their type information is not contained within their callable signature (their :func:`__init__` method). .. literalinclude:: /examples/plugins/di_plugin.py :language: python :caption: Dynamically generating signature information for a custom type .. toctree:: :titlesonly: flash_messages problem_details litestar-2.16.0/docs/usage/plugins/problem_details.rst000066400000000000000000000032141500564371300230470ustar00rootroot00000000000000=============== Problem Details =============== .. versionadded:: 2.9.0 Problem details are a standardized way of providing machine-readable details of errors in HTTP responses as specified in `RFC 9457`_, the latest RFC at the time of writing. .. _RFC 9457: https://datatracker.ietf.org/doc/html/rfc9457 Usage ----- To send a problem details response, the ``ProblemDetailsPlugin`` should be registered and then a ``ProblemDetailsException`` can be raised anywhere which will automatically be converted into a problem details response. .. literalinclude:: /examples/plugins/problem_details/basic_usage.py :language: python :caption: Basic usage of the problem details plugin. You can convert all ``HTTPExceptions`` into problem details response by enabling the flag in the ``ProblemDetailsConfig.`` .. literalinclude:: /examples/plugins/problem_details/convert_http_exceptions.py :language: python :caption: Converting ``HTTPException`` into problem details response. You can also convert any exception that is not a ``HTTPException`` into a problem details response by providing a mapping of the exception type to a callable that converts the exception into a ``ProblemDetailsException.`` .. tip:: This can used to override how the ``HTTPException`` is converted into a problem details response as well. .. literalinclude:: /examples/plugins/problem_details/convert_exceptions.py :language: python :caption: Converting custom exceptions into problem details response. .. warning:: If the ``extra`` field is a ``Mapping``, then it's merged into the problem details response, otherwise it's included in the response with the key ``extra.`` litestar-2.16.0/docs/usage/requests.rst000066400000000000000000000231421500564371300200760ustar00rootroot00000000000000Requests ======== Request body ------------ The body of HTTP requests can be accessed using the special ``data`` parameter in a handler function. .. literalinclude:: /examples/request_data/request_data_1.py :language: python The type of ``data`` can be any supported type, including * Arbitrary stdlib types * :class:`TypedDicts ` * :func:`dataclasses ` * Types supported via :doc:`plugins ` ie. - `Msgspec Struct `_ - `Pydantic models `_ - `Attrs classes `_ .. literalinclude:: /examples/request_data/request_data_2.py :language: python Validation and customization of OpenAPI documentation ----------------------------------------------------- With the help of :class:`Body `, you have fine-grained control over the validation of the request body, and can also customize the OpenAPI documentation: .. tab-set:: .. tab-item:: example .. literalinclude:: /examples/request_data/request_data_3.py :language: python .. tab-item:: how to test .. literalinclude:: ../../tests/examples/test_request_data.py :language: python :lines: 35-41 Content-type ------------ By default, Litestar will try to parse the request body as JSON. While this may be desired in most cases, you might want to specify a different type. You can do so by passing a :class:`RequestEncodingType ` to ``Body``. This will also help to generate the correct media-type in the OpenAPI schema. URL Encoded Form Data ^^^^^^^^^^^^^^^^^^^^^ To access data sent as `url-encoded form data `_, i.e. ``application/x-www-form-urlencoded`` Content-Type header, use :class:`Body ` and specify :class:`RequestEncodingType.URL_ENCODED ` as the ``media_type``: .. tab-set:: .. tab-item:: example .. literalinclude:: /examples/request_data/request_data_4.py :language: python .. tab-item:: how to test .. literalinclude:: ../../tests/examples/test_request_data.py :language: python :lines: 44-48 .. note:: URL encoded data is inherently less versatile than JSON data - for example, it cannot handle complex dictionaries and deeply nested data. It should only be used for simple data structures. MultiPart Form Data ^^^^^^^^^^^^^^^^^^^ You can access data uploaded using a request with a `multipart/form-data `_ Content-Type header by specifying it in the :class:`Body ` function: .. tab-set:: .. tab-item:: example .. literalinclude:: /examples/request_data/request_data_5.py :language: python .. tab-item:: how to test .. literalinclude:: ../../tests/examples/test_request_data.py :language: python :lines: 51-64 File uploads ------------ In case of files uploaded, Litestar transforms the results into an instance of :class:`UploadFile <.datastructures.upload_file.UploadFile>` class, which offer a convenient interface for working with files. Therefore, you need to type your file uploads accordingly. To access a single file simply type ``data`` as :class:`UploadFile <.datastructures.upload_file.UploadFile>`: .. tab-set:: .. tab-item:: Async .. tab-set:: .. tab-item:: example .. literalinclude:: /examples/request_data/request_data_6.py :language: python .. tab-item:: how to test .. literalinclude:: ../../tests/examples/test_request_data.py :language: python :lines: 67-71 .. tab-item:: Sync .. tab-set:: .. tab-item:: example .. literalinclude:: /examples/request_data/request_data_7.py :language: python .. tab-item:: how to test .. literalinclude:: ../../tests/examples/test_request_data.py :language: python :lines: 74-78 .. admonition:: Technical details :class: info :class:`UploadFile <.datastructures.UploadFile>` wraps :class:`SpooledTemporaryFile ` so it can be used asynchronously. Inside a synchronous function we don't need this wrapper, so we can use its :meth:`read ` method directly. Multiple files ^^^^^^^^^^^^^^ To access multiple files with known filenames, you can use a pydantic model: .. tab-set:: .. tab-item:: example .. literalinclude:: /examples/request_data/request_data_8.py :language: python .. tab-item:: how to test .. literalinclude:: ../../tests/examples/test_request_data.py :language: python :lines: 81-87 Files as a dictionary ^^^^^^^^^^^^^^^^^^^^^ If you do not care about parsing and validation and only want to access the form data as a dictionary, you can use a ``dict`` instead: .. tab-set:: .. tab-item:: example .. literalinclude:: /examples/request_data/request_data_9.py :language: python .. tab-item:: how to test .. literalinclude:: ../../tests/examples/test_request_data.py :language: python :lines: 90-97 Files as a list ^^^^^^^^^^^^^^^ Finally, you can also access the files as a list without the filenames: .. tab-set:: .. tab-item:: example .. literalinclude:: /examples/request_data/request_data_10.py :language: python .. tab-item:: how to test .. literalinclude:: ../../tests/examples/test_request_data.py :language: python :lines: 100-133 MessagePack data ---------------- To receive `MessagePack `_ data, specify the appropriate ``Content-Type`` for ``Body``\ , by using :class:`RequestEncodingType.MESSAGEPACK <.enums.RequestEncodingType>`: .. tab-set:: .. tab-item:: example .. literalinclude:: /examples/request_data/msgpack_request.py :language: python .. tab-item:: how to test .. literalinclude:: ../../tests/examples/test_request_data.py :language: python :lines: 136-141 Custom Request -------------- .. versionadded:: 2.7.0 Litestar supports custom ``request_class`` instances, which can be used to further configure the default :class:`Request`. The example below illustrates how to implement custom request class for the whole application. .. dropdown:: Example of a custom request at the application level .. tab-set:: .. tab-item:: example .. literalinclude:: /examples/request_data/custom_request.py :language: python .. tab-item:: how to test .. literalinclude:: ../../tests/examples/test_request_data.py :language: python :lines: 144-147 .. admonition:: Layered architecture Request classes are part of Litestar's layered architecture, which means you can set a request class on every layer of the application. If you have set a request class on multiple layers, the layer closest to the route handler will take precedence. You can read more about this in the :ref:`usage/applications:layered architecture` section Limits ------- Body size ^^^^^^^^^^ A limit for the allowed request body size can be set on all layers via the ``request_max_body_size`` parameter and defaults to 10MB. If a request body exceeds this limit, a ``413 - Request Entity Too Large`` response will be returned. This limit applies to all methods of consuming the request body, including requesting it via the ``body`` parameter in a route handler and consuming it through a manually constructed :class:`~litestar.connection.Request` instance, e.g. in a middleware. To disable this limit for a specific handler / router / controller, it can be set to :obj:`None`. .. danger:: Setting ``request_max_body_size=None`` is strongly discouraged as it exposes the application to a denial of service (DoS) attack by sending arbitrarily large request bodies to the affected endpoint. Because Litestar has to read the whole body to perform certain actions, such as parsing JSON, it will fill up all the available memory / swap until the application / server crashes, should no outside limits be imposed. This is generally only recommended in environments where the application is running behind a reverse proxy such as NGINX, where a size limit is already set. .. danger:: Since ``request_max_body_size`` is handled on a per-request basis, it won't affect middlewares or ASGI handlers when they try to access the request body via the raw ASGI events. To avoid this, middlewares and ASGI handlers should construct a :class:`~litestar.connection.Request` instance and use the regular :meth:`~litestar.connection.Request.stream` / :meth:`~litestar.connection.Request.body` or content-appropriate method to consume the request body in a safe manner. .. tip:: For requests that define a ``Content-Length`` header, Litestar will not attempt to read the request body should the header value exceed the ``request_max_body_size``. If the header value is within the allowed bounds, Litestar will verify during the streaming of the request body that it does not exceed the size specified in the header. Should the request exceed this size, it will abort the request with a ``400 - Bad Request``. litestar-2.16.0/docs/usage/responses.rst000066400000000000000000000777761500564371300202720ustar00rootroot00000000000000Responses ========= Litestar allows for several ways in which HTTP responses can be specified and handled, each fitting a different use case. The base pattern though is straightforward - simply return a value from a route handler function and let Litestar take care of the rest: .. code-block:: python from pydantic import BaseModel from litestar import get class Resource(BaseModel): id: int name: str @get("/resources") def retrieve_resource() -> Resource: return Resource(id=1, name="my resource") In the example above, the route handler function returns an instance of the ``Resource`` pydantic class. This value will then be used by Litestar to construct an instance of the :class:`Response ` class using defaults values: the response status code will be set to ``200`` and it's ``Content-Type`` header will be set to ``application/json``. The ``Resource`` instance will be serialized into JSON and set as the response body. Media Type ---------- You do not have to specify the ``media_type`` kwarg in the route handler function if the response should be JSON. But if you wish to return a response other than JSON, you should specify this value. You can use the :class:`MediaType ` enum for this purpose: .. code-block:: python from litestar import MediaType, get @get("/resources", media_type=MediaType.TEXT) def retrieve_resource() -> str: return "The rumbling rabbit ran around the rock" The value of the ``media_type`` kwarg affects both the serialization of response data and the generation of OpenAPI docs. The above example will cause Litestar to serialize the response as a simple bytes string with a ``Content-Type`` header value of ``text/plain``. It will also set the corresponding values in the OpenAPI documentation. MediaType has the following members: * MediaType.JSON: ``application/json`` * MediaType.MessagePack: ``application/x-msgpack`` * MediaType.TEXT: ``text/plain`` * MediaType.HTML: ``text/html`` You can also set any `IANA referenced `_ media type string as the ``media_type``. While this will still affect the OpenAPI generation as expected, you might need to handle serialization using either a :ref:`custom response ` with serializer or by serializing the value in the route handler function. JSON responses ++++++++++++++ As previously mentioned, the default ``media_type`` is ``MediaType.JSON``. which supports the following values: * :doc:`dataclasses ` * `pydantic dataclasses `_ * `pydantic models `_ * models from libraries that extend pydantic models * :class:`UUIDs ` * :doc:`datetime objects ` * `msgspec.Struct `_ * container types such as :class:`dict` or :class:`list` containing supported types If you need to return other values and would like to extend serialization you can do this :ref:`custom responses `. You can also set an application media type string with the ``+json`` suffix defined in `RFC 6839 `_ as the ``media_type`` and it will be recognized and serialized as json. For example, you can use ``application/vnd.example.resource+json`` and it will work just like json but have the appropriate content-type header and show up in the generated OpenAPI schema. .. literalinclude:: /examples/responses/json_suffix_responses.py :language: python MessagePack responses +++++++++++++++++++++ In addition to JSON, Litestar offers support for the `MessagePack `_ format which can be a time and space efficient alternative to JSON. It supports all the same types as JSON serialization. To send a ``MessagePack`` response, simply specify the media type as ``MediaType.MESSAGEPACK``\ : .. code-block:: python from typing import Dict from litestar import get, MediaType @get(path="/health-check", media_type=MediaType.MESSAGEPACK) def health_check() -> Dict[str, str]: return {"hello": "world"} Plaintext responses +++++++++++++++++++ For ``MediaType.TEXT``, route handlers should return a :class:`str` or :class:`bytes` value: .. code-block:: python from litestar import get, MediaType @get(path="/health-check", media_type=MediaType.TEXT) def health_check() -> str: return "healthy" HTML responses ++++++++++++++ For ``MediaType.HTML``, route handlers should return a :class:`str` or :class:`bytes` value that contains HTML: .. code-block:: python from litestar import get, MediaType @get(path="/page", media_type=MediaType.HTML) def health_check() -> str: return """

Hello World!
""" .. tip:: It's a good idea to use a :ref:`template engine ` for more complex HTML responses and to write the template itself in a separate file rather than a string. Content Negotiation ------------------- If your handler can return data with different media types and you want to use `Content Negotiation `_ to allow the client to choose which type to return, you can use the :attr:`Request.accept ` property to calculate the best matching return media type. .. literalinclude:: /examples/responses/response_content.py :language: python Status Codes ------------ You can control the response ``status_code`` by setting the corresponding kwarg to the desired value: .. code-block:: python from pydantic import BaseModel from litestar import get from litestar.status_codes import HTTP_202_ACCEPTED class Resource(BaseModel): id: int name: str @get("/resources", status_code=HTTP_202_ACCEPTED) def retrieve_resource() -> Resource: return Resource(id=1, name="my resource") If ``status_code`` is not set by the user, the following defaults are used: * POST: 201 (Created) * DELETE: 204 (No Content) * GET, PATCH, PUT: 200 (Ok) .. attention:: For status codes < 100 or 204, 304 statuses, no response body is allowed. If you specify a return annotation other than ``None``, an :class:`ImproperlyConfiguredException ` will be raised. .. note:: When using the ``route`` decorator with multiple http methods, the default status code is ``200``. The default for ``delete`` is ``204`` because by default it is assumed that delete operations return no data. This though might not be the case in your implementation - so take care of setting it as you see fit. .. tip:: While you can write integers as the value for ``status_code``, e.g. ``200``, it's best practice to use constants (also in tests). Litestar includes easy to use statuses that are exported from ``litestar.status_codes``, e.g. ``HTTP_200_OK`` and ``HTTP_201_CREATED``. Another option is the :class:`http.HTTPStatus` enum from the standard library, which also offers extra functionality. Returning responses ------------------- While the default response handling fits most use cases, in some cases you need to be able to return a response instance directly. Litestar allows you to return any class inheriting from the :class:`Response ` class. Thus, the below example will work perfectly fine: .. literalinclude:: /examples/responses/returning_responses.py :language: python .. attention:: In the case of the builtin :class:`Template `, :class:`File `, :class:`Stream `, and :class:`Redirect ` you should use the response "response containers", otherwise OpenAPI documentation will not be generated correctly. For more details see the respective documentation sections: - `Template responses`_ - `File responses`_ - `Streaming responses`_ - `Redirect responses`_ Annotating responses ++++++++++++++++++++ As you can see above, the :class:`Response ` class accepts a generic argument. This allows Litestar to infer the response body when generating the OpenAPI docs. .. note:: If the generic argument is not provided, and thus defaults to ``Any``, the OpenAPI docs will be imprecise. So make sure to type this argument even when returning an empty or ``null`` body, i.e. use ``None``. Returning ASGI Applications +++++++++++++++++++++++++++ Litestar also supports returning ASGI applications directly, as you would responses. For example: .. code-block:: python from litestar import get from litestar.types import ASGIApp, Receive, Scope, Send @get("/") def handler() -> ASGIApp: async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None: ... return my_asgi_app What is an ASGI Application? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ An ASGI application in this context is any async callable (function, class method or simply a class that implements that special :meth:`object.__call__` dunder method) that accepts the three ASGI arguments: ``scope``, ``receive``, and ``send``. For example, all the following examples are ASGI applications: Function ASGI Application ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python from litestar.types import Receive, Scope, Send async def my_asgi_app_function(scope: Scope, receive: Receive, send: Send) -> None: # do something here ... Method ASGI Application ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python from litestar.types import Receive, Scope, Send class MyClass: async def my_asgi_app_method( self, scope: Scope, receive: Receive, send: Send ) -> None: # do something here ... Class ASGI Application ~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python from litestar.types import Receive, Scope, Send class ASGIApp: async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: # do something here ... Returning responses from third party libraries ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Because you can return any ASGI Application from a route handler, you can also use any ASGI application from other libraries. For example, you can return the response classes from Starlette or FastAPI directly from route handlers: .. code-block:: python from starlette.responses import JSONResponse from litestar import get from litestar.types import ASGIApp @get("/") def handler() -> ASGIApp: return JSONResponse(content={"hello": "world"}) # type: ignore .. attention:: Litestar offers strong typing for the ASGI arguments. Other libraries often offer less strict typing, which might cause type checkers to complain when using ASGI apps from them inside Litestar. For the time being, the only solution is to add ``# type: ignore`` comments in the pertinent places. Nonetheless, the above example will work perfectly fine. Setting Response Headers ------------------------- Litestar allows you to define response headers by using the ``response_headers`` kwarg. This kwarg is available on all layers of the app - individual route handlers, controllers, routers, and the app itself: .. literalinclude:: /examples/responses/response_headers_1.py :language: python In the above example the response returned from ``my_route_handler`` will have headers set from each layer of the application using the given key+value combinations. I.e. it will be a dictionary equal to this: .. code-block:: json { "my-local-header": "local header", "controller-level-header": "controller header", "router-level-header": "router header", "app-level-header": "app header" } The respective descriptions will be used for the OpenAPI documentation. .. tip:: :class:`ResponseHeader ` is a special class that allows to add OpenAPI attributes such as `description` or `documentation_only`. If you don't need those, you can optionally define `response_headers` using a mapping - such as a dictionary - as well: .. code-block:: python @get(response_headers={"my-header": "header-value"}) async def handler() -> str: ... Setting Headers Dynamically +++++++++++++++++++++++++++ The above detailed scheme works great for statically configured headers, but how would you go about handling dynamically setting headers? Litestar allows you to set headers dynamically in several ways and below we will detail the two primary patterns. Using Annotated Responses ^^^^^^^^^^^^^^^^^^^^^^^^^ We can simply return a response instance directly from the route handler and set the headers dictionary manually as you see fit, e.g.: .. literalinclude:: /examples/responses/response_headers_2.py :language: python In the above we use the ``response_headers`` kwarg to pass the ``name`` and ``description`` parameters for the ``Random-Header`` to the OpenAPI documentation, but we set the value dynamically in as part of the :ref:`annotated response ` we return. To this end we do not set a ``value`` for it and we designate it as ``documentation_only=True``. Using the After Request Hook ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ An alternative pattern would be to use an :ref:`after request handler `. We can define the handler on different layers of the application as explained in the pertinent docs. We should take care to document the headers on the corresponding layer: .. literalinclude:: /examples/responses/response_headers_3.py :language: python In the above we set the response header using an ``after_request_handler`` function on the router level. Because the handler function is applied on the router, we also set the documentation for it on the router. We can use this pattern to fine-tune the OpenAPI documentation more granularly by overriding header specification as required. For example, lets say we have a router level header being set and a local header with the same key but a different value range: .. literalinclude:: /examples/responses/response_headers_4.py :language: python Predefined Headers ++++++++++++++++++ Litestar has a dedicated implementation for a few commonly used headers. These headers can be set separately with dedicated keyword arguments or as class attributes on all layers of the app (individual route handlers, controllers, routers, and the app itself). Each layer overrides the layer above it - thus, the headers defined for a specific route handler will override those defined on its router, which will in turn override those defined on the app level. These header implementations allow easy creating, serialization and parsing according to the associated header specifications. Cache Control ^^^^^^^^^^^^^ :class:`CacheControlHeader <.datastructures.headers.CacheControlHeader>` represents a `Cache-Control Header `_. Here is a simple example that shows how to use it: .. literalinclude:: /examples/datastructures/headers/cache_control.py :caption: Cache Control Header :language: python In this example we have a ``cache-control`` with ``max-age`` of 1 month for the whole app, a ``max-age`` of 1 day for all routes within ``MyController``, and ``no-store`` for one specific route ``get_server_time``. Here are the cache control values that will be returned from each endpoint: * When calling ``/population`` the response will have ``cache-control`` with ``max-age=2628288`` (1 month). * When calling ``/chance_of_rain`` the response will have ``cache-control`` with ``max-age=86400`` (1 day). * When calling ``/timestamp`` the response will have ``cache-control`` with ``no-store`` which means don't store the result in any cache. ETag ^^^^ :class:`ETag <.datastructures.headers.ETag>` represents an `ETag header `_. Here are some usage examples: .. literalinclude:: /examples/datastructures/headers/etag.py :caption: Returning ETag headers :language: python .. literalinclude:: /examples/datastructures/headers/etag_parsing.py :caption: Parsing ETag headers :language: python Setting Response Cookies ------------------------- Litestar allows you to define response cookies by using the ``response_cookies`` kwarg. This kwarg is available on all layers of the app - individual route handlers, controllers, routers, and the app itself: .. literalinclude:: /examples/responses/response_cookies_1.py :language: python In the above example, the response returned by ``my_route_handler`` will have cookies set by each layer of the application. Cookies are set using the `Set-Cookie header `_ and with above resulting in: .. code-block:: text Set-Cookie: local-cookie=local value; Path=/; SameSite=lax Set-Cookie: controller-cookie=controller value; Path=/; SameSite=lax Set-Cookie: router-cookie=router value; Path=/; SameSite=lax Set-Cookie: app-cookie=app value; Path=/; SameSite=lax You can easily override cookies declared in higher levels by redeclaring a cookie with the same key in a lower level, e.g.: .. literalinclude:: /examples/responses/response_cookies_2.py :language: python Of the two declarations of ``my-cookie`` only the route handler one will be used, because its lower level: .. code-block:: text Set-Cookie: my-cookie=456; Path=/; SameSite=lax .. tip:: If all you need for your cookies are key and value, you can supply them using a :class:`Mapping[str, str] ` - like a :class:`dict` - instead: .. code-block:: python @get(response_cookies={"my-cookie": "cookie-value"}) async def handler() -> str: ... .. seealso:: * :class:`Cookie reference <.datastructures.cookie.Cookie>` Setting Cookies dynamically ++++++++++++++++++++++++++++ While the above scheme works great for static cookie values, it doesn't allow for dynamic cookies. Because cookies are fundamentally a type of response header, we can utilize the same patterns we use to setting :ref:`set headers headers `. Using Annotated Responses ^^^^^^^^^^^^^^^^^^^^^^^^^ We can simply return a response instance directly from the route handler and set the cookies list manually as you see fit, e.g.: .. literalinclude:: /examples/responses/response_cookies_3.py :language: python In the above we use the ``response_cookies`` kwarg to pass the ``key`` and ``description`` parameters for the ``Random-Cookie`` to the OpenAPI documentation, but we set the value dynamically in as part of the :ref:`annotated response ` we return. To this end we do not set a ``value`` for it and we designate it as ``documentation_only=True``. Using the After Request Hook ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ An alternative pattern would be to use an :ref:`after request handler `. We can define the handler on different layers of the application as explained in the pertinent docs. We should take care to document the cookies on the corresponding layer: .. literalinclude:: /examples/responses/response_cookies_4.py :language: python In the above we set the cookie using an ``after_request_handler`` function on the router level. Because the handler function is applied on the router, we also set the documentation for it on the router. We can use this pattern to fine-tune the OpenAPI documentation more granular by overriding cookie specification as required. For example, lets say we have a router level cookie being set and a local cookie with the same key but a different value range: .. literalinclude:: /examples/responses/response_cookies_5.py :language: python Redirect Responses ------------------ Redirect responses are `special HTTP responses `_ with a status code in the 30x range. In Litestar, a redirect response looks like this: .. code-block:: python from litestar.status_codes import HTTP_302_FOUND from litestar import get from litestar.response import Redirect @get(path="/some-path", status_code=HTTP_302_FOUND) def redirect() -> Redirect: # do some stuff here # ... # finally return redirect return Redirect(path="/other-path") To return a redirect response you should do the following: - optionally: set an appropriate status code for the route handler (301, 302, 303, 307, 308). If not set the default of 302 will be used. - annotate the return value of the route handler as returning :class:`Redirect <.response.Redirect>` - return an instance of the :class:`Redirect <.response.Redirect>` class with the desired redirect path File Responses -------------- File responses send a file: .. code-block:: python from pathlib import Path from litestar import get from litestar.response import File @get(path="/file-download") def handle_file_download() -> File: return File( path=Path(Path(__file__).resolve().parent, "report").with_suffix(".pdf"), filename="report.pdf", ) The :class:`File <.response.File>` class expects two kwargs: * ``path``: path of the file to download. * ``filename``: the filename to set in the response `Content-Disposition `_ attachment. .. attention:: When a route handler's return value is annotated with :class:`File <.response.File>`, the default ``media_type`` for the route_handler is switched from :class:`MediaType.JSON <.enums.MediaType>` to :class:`MediaType.TEXT <.enums.MediaType>` (i.e. ``"text/plain"``). If the file being sent has an `IANA media type `_, you should set it as the value for ``media_type`` instead. For example: .. code-block:: python from pathlib import Path from litestar import get from litestar.response import File @get(path="/file-download", media_type="application/pdf") def handle_file_download() -> File: return File( path=Path(Path(__file__).resolve().parent, "report").with_suffix(".pdf"), filename="report.pdf", ) Streaming Responses ------------------- To return a streaming response use the :class:`Stream <.response.Stream>` class. The class receives a single positional arg, that must be an iterator delivering the stream: .. literalinclude:: /examples/responses/streaming_responses.py :language: python .. note:: You can use different kinds of values for the iterator. It can be a callable returning a sync or async generator, a generator itself, a sync or async iterator class, or an instance of a sync or async iterator class. Server Sent Event Responses --------------------------- To send `server-sent-events` or SSEs to the frontend, use the :class:`ServerSentEvent <.response.ServerSentEvent>` class. The class receives a content arg. You can additionally specify ``event_type``, which is the name of the event as declared in the browser, the ``event_id``, which sets the event source property, ``comment_message``, which is used in for sending pings, and ``retry_duration``, which dictates the duration for retrying. .. literalinclude:: /examples/responses/sse_responses.py :language: python .. note:: You can use different kinds of values for the iterator. It can be a callable returning a sync or async generator, a generator itself, a sync or async iterator class, or an instance of a sync or async iterator class. In your iterator function you can yield integers, strings or bytes, the message sent in that case will have ``message`` as the ``event_type`` if the ServerSentEvent has no ``event_type`` set, otherwise it will use the ``event_type`` specified, and the data will be the yielded value. If you want to send a different event type, you can use a dictionary with the keys ``event_type`` and ``data`` or the :class:`ServerSentEventMessage <.response.ServerSentEventMessage>` class. .. note:: You can further customize all the sse parameters, add comments, and set the retry duration by using the :class:`ServerSentEvent <.response.ServerSentEvent>` class directly or by using the :class:`ServerSentEventMessage <.response.ServerSentEventMessage>` or dictionaries with the appropriate keys. Template Responses ------------------ Template responses are used to render templates into HTML. To use a template response you must first :ref:`register a template engine ` on the application level. Once an engine is in place, you can use a template response like so: .. code-block:: python from litestar import Request, get from litestar.response import Template @get(path="/info") def info(request: Request) -> Template: return Template(template_name="info.html", context={"user": request.user}) In the above example, :class:`Template <.response.Template>` is passed the template name, which is a path like value, and a context dictionary that maps string keys into values that will be rendered in the template. Custom Responses ---------------- While Litestar supports the serialization of many types by default, sometimes you want to return something that's not supported. In those cases it's convenient to make use of a custom response class. The example below illustrates how to deal with :class:`MultiDict <.datastructures.MultiDict>` instances. .. literalinclude:: /examples/responses/custom_responses.py :language: python .. admonition:: Layered architecture :class: seealso Response classes are part of Litestar's layered architecture, which means you can set a response class on every layer of the application. If you have set a response class on multiple layers, the layer closest to the route handler will take precedence. You can read more about this here: :ref:`usage/applications:layered architecture` Background Tasks ---------------- All Litestar responses allow passing in a ``background`` kwarg. This kwarg accepts either an instance of :class:`BackgroundTask <.background_tasks.BackgroundTask>` or an instance of :class:`BackgroundTasks <.background_tasks.BackgroundTasks>`, which wraps an iterable of :class:`BackgroundTask <.background_tasks.BackgroundTask>` instances. A background task is a sync or async callable (function, method, or class that implements the :meth:`object.__call__` dunder method) that will be called after the response finishes sending the data. Thus, in the following example the passed in background task will be executed after the response sends: .. literalinclude:: /examples/responses/background_tasks_1.py :caption: Background Task Passed into Response :language: python When the ``greeter`` handler is called, the logging task will be called with any ``*args`` and ``**kwargs`` passed into the :class:`BackgroundTask <.background_tasks.BackgroundTask>`. .. note:: In the above example ``"greeter"`` is an arg and ``message=f"was called with name {name}"`` is a kwarg. The function signature of ``logging_task`` allows for this, so this should pose no problem. :class:`BackgroundTask <.background_tasks.BackgroundTask>` is typed with :class:`ParamSpec `, enabling correct type checking for arguments and keyword arguments passed to it. Route decorators (e.g. ``@get``, ``@post``, etc.) also allow passing in a background task with the ``background`` kwarg: .. literalinclude:: /examples/responses/background_tasks_2.py :caption: Background Task Passed into Decorator :language: python .. note:: Route handler arguments cannot be passed into background tasks when they are passed into decorators. Executing Multiple Background Tasks +++++++++++++++++++++++++++++++++++ You can also use the :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` class and pass to it an iterable (:class:`list`, :class:`tuple`, etc.) of :class:`BackgroundTask <.background_tasks.BackgroundTask>` instances: .. literalinclude:: /examples/responses/background_tasks_3.py :caption: Multiple Background Tasks :language: python :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` class accepts an optional keyword argument ``run_in_task_group`` with a default value of ``False``. Setting this to ``True`` allows background tasks to run concurrently, using an `anyio.task_group `_. .. note:: Setting ``run_in_task_group`` to ``True`` will not preserve execution order. Pagination ----------- When you need to return a large number of items from an endpoint it is common practice to use pagination to ensure clients can request a specific subset or "page" from the total dataset. Litestar supports three types of pagination out of the box: * classic pagination * limit / offset pagination * cursor pagination Classic Pagination ++++++++++++++++++ In classic pagination the dataset is divided into pages of a specific size and the consumer then requests a specific page. .. literalinclude:: /examples/pagination/using_classic_pagination.py :caption: Classic Pagination :language: python The data container for this pagination is called :class:`ClassicPagination <.pagination.ClassicPagination>`, which is what will be returned by the paginator in the above example This will also generate the corresponding OpenAPI documentation. If you require async logic, you can implement the :class:`AbstractAsyncClassicPaginator <.pagination.AbstractAsyncClassicPaginator>` instead of the :class:`AbstractSyncClassicPaginator <.pagination.AbstractSyncClassicPaginator>`. Offset Pagination +++++++++++++++++ In offset pagination the consumer requests a number of items specified by ``limit`` and the ``offset`` from the beginning of the dataset. For example, given a list of 50 items, you could request ``limit=10``, ``offset=39`` to request items 40-50. .. literalinclude:: /examples/pagination/using_offset_pagination.py :caption: Offset Pagination :language: python The data container for this pagination is called :class:`OffsetPagination <.pagination.OffsetPagination>`, which is what will be returned by the paginator in the above example This will also generate the corresponding OpenAPI documentation. If you require async logic, you can implement the :class:`AbstractAsyncOffsetPaginator <.pagination.AbstractAsyncOffsetPaginator>` instead of the :class:`AbstractSyncOffsetPaginator <.pagination.AbstractSyncOffsetPaginator>`. Offset Pagination With SQLAlchemy ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When retrieving paginated data from the database using SQLAlchemy, the Paginator instance requires an SQLAlchemy session instance to make queries. This can be achieved with :doc:`/usage/dependency-injection` .. literalinclude:: /examples/pagination/using_offset_pagination_with_sqlalchemy.py :caption: Offset Pagination With SQLAlchemy :language: python See :ref:`SQLAlchemy plugin ` for sqlalchemy integration. Cursor Pagination +++++++++++++++++ In cursor pagination the consumer requests a number of items specified by ``results_per_page`` and a ``cursor`` after which results are given. Cursor is unique identifier within the dataset that serves as a way to point the starting position. .. literalinclude:: /examples/pagination/using_cursor_pagination.py :caption: Cursor Pagination :language: python The data container for this pagination is called :class:`CursorPagination <.pagination.CursorPagination>`, which is what will be returned by the paginator in the above example This will also generate the corresponding OpenAPI documentation. If you require async logic, you can implement the :class:`AbstractAsyncCursorPaginator <.pagination.AbstractAsyncCursorPaginator>` instead of the :class:`AbstractSyncCursorPaginator <.pagination.AbstractSyncCursorPaginator>`. litestar-2.16.0/docs/usage/routing/000077500000000000000000000000001500564371300171565ustar00rootroot00000000000000litestar-2.16.0/docs/usage/routing/handlers.rst000066400000000000000000000607001500564371300215130ustar00rootroot00000000000000Route handlers ============== Route handlers are the core of Litestar. They are constructed by decorating a function or class method with one of the handler :term:`decorators ` exported from Litestar. For example: .. code-block:: python :caption: Defining a route handler by decorating a function with the :class:`@get() <.handlers.get>` :term:`decorator` from litestar import get @get("/") def greet() -> str: return "hello world" In the above example, the :term:`decorator` includes all the information required to define the endpoint operation for the combination of the path ``"/"`` and the HTTP verb ``GET``. In this case it will be a HTTP response with a ``Content-Type`` header of ``text/plain``. .. include:: /admonitions/sync-to-thread-info.rst Declaring paths --------------- All route handler :term:`decorators ` accept an optional path :term:`argument`. This :term:`argument` can be declared as a :term:`kwarg ` using the :paramref:`~.handlers.base.BaseRouteHandler.path` parameter: .. code-block:: python :caption: Defining a route handler by passing the path as a keyword argument from litestar import get @get(path="/some-path") async def my_route_handler() -> None: ... It can also be passed as an :term:`argument` without the keyword: .. code-block:: python :caption: Defining a route handler but not using the keyword argument from litestar import get @get("/some-path") async def my_route_handler() -> None: ... And the value for this :term:`argument` can be either a string path, as in the above examples, or a :class:`list` of :class:`string ` paths: .. code-block:: python :caption: Defining a route handler with multiple paths from litestar import get @get(["/some-path", "/some-other-path"]) async def my_route_handler() -> None: ... This is particularly useful when you want to have optional :ref:`path parameters `: .. code-block:: python :caption: Defining a route handler with a path that has an optional path parameter from litestar import get @get( ["/some-path", "/some-path/{some_id:int}"], ) async def my_route_handler(some_id: int = 1) -> None: ... .. _handler-function-kwargs: "reserved" keyword arguments ---------------------------- Route handler functions or methods access various data by declaring these as annotated function :term:`kwargs `. The annotated :term:`kwargs ` are inspected by Litestar and then injected into the request handler. The following sources can be accessed using annotated function :term:`kwargs `: - :ref:`path, query, header, and cookie parameters ` - :doc:`requests ` - :doc:`injected dependencies ` Additionally, you can specify the following special :term:`kwargs `, (known as "reserved keywords"): * ``cookies``: injects the request :class:`cookies <.datastructures.cookie.Cookie>` as a parsed :class:`dictionary `. * ``headers``: injects the request headers as a parsed :class:`dictionary `. * ``query`` : injects the request ``query_params`` as a parsed :class:`dictionary `. * ``request``: injects the :class:`Request <.connection.Request>` instance. Available only for `HTTP route handlers`_ * ``scope`` : injects the ASGI scope :class:`dictionary `. * ``socket``: injects the :class:`WebSocket <.connection.WebSocket>` instance. Available only for `websocket route handlers`_ * ``state`` : injects a copy of the application :class:`State <.datastructures.state.State>`. * ``body`` : the raw request body. Available only for `HTTP route handlers`_ Note that if your parameters collide with any of the reserved :term:`keyword arguments ` above, you can :ref:`provide an alternative name `. For example: .. code-block:: python :caption: Providing an alternative name for a reserved keyword argument from typing import Any, Dict from litestar import Request, get from litestar.datastructures import Headers, State @get(path="/") async def my_request_handler( state: State, request: Request, headers: Dict[str, str], query: Dict[str, Any], cookies: Dict[str, Any], ) -> None: ... .. tip:: You can define a custom typing for your application state and then use it as a type instead of just using the :class:`~.datastructures.state.State` class from Litestar Type annotations ---------------- Litestar enforces strict :term:`type annotations `. Functions decorated by a route handler **must** have all their :term:`arguments ` and return value type annotated. If a type annotation is missing, an :exc:`~.exceptions.ImproperlyConfiguredException` will be raised during the application boot-up process. There are several reasons for why this limitation is enforced: #. To ensure best practices #. To ensure consistent OpenAPI schema generation #. To allow Litestar to compute the :term:`arguments ` required by a function during application bootstrap HTTP route handlers ------------------- The most commonly used route handlers are those that handle HTTP requests and responses. These route handlers all inherit from the :class:`~.handlers.HTTPRouteHandler` class, which is aliased as the :term:`decorator` called :func:`~.handlers.route`: .. code-block:: python :caption: Defining a route handler by decorating a function with the :class:`@route() <.handlers.route>` :term:`decorator` from litestar import HttpMethod, route @route(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST]) async def my_endpoint() -> None: ... As mentioned above, :func:`@route() <.handlers.route>` is merely an alias for ``HTTPRouteHandler``, thus the below code is equivalent to the one above: .. code-block:: python :caption: Defining a route handler by decorating a function with the :class:`HTTPRouteHandler <.handlers.HTTPRouteHandler>` class from litestar import HttpMethod from litestar.handlers.http_handlers import HTTPRouteHandler @HTTPRouteHandler(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST]) async def my_endpoint() -> None: ... Semantic handler :term:`decorators ` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Litestar also includes "semantic" :term:`decorators `, that is, :term:`decorators ` the pre-set the :paramref:`~litestar.handlers.HTTPRouteHandler.http_method` :term:`kwarg ` to a specific HTTP verb, which correlates with their name: * :func:`@delete() <.handlers.delete>` * :func:`@get() <.handlers.get>` * :func:`@head() <.handlers.head>` * :func:`@patch() <.handlers.patch>` * :func:`@post() <.handlers.post>` * :func:`@put() <.handlers.put>` These are used exactly like :func:`@route() <.handlers.route>` with the sole exception that you cannot configure the :paramref:`~.handlers.HTTPRouteHandler.http_method` :term:`kwarg `: .. dropdown:: Click to see the predefined route handlers .. code-block:: python :caption: Predefined :term:`decorators ` for HTTP route handlers from litestar import delete, get, patch, post, put, head from litestar.dto import DTOConfig, DTOData from litestar.plugins.pydantic import PydanticDTO from pydantic import BaseModel class Resource(BaseModel): ... class PartialResourceDTO(PydanticDTO[Resource]): config = DTOConfig(partial=True) @get(path="/resources") async def list_resources() -> list[Resource]: ... @post(path="/resources") async def create_resource(data: Resource) -> Resource: ... @get(path="/resources/{pk:int}") async def retrieve_resource(pk: int) -> Resource: ... @head(path="/resources/{pk:int}") async def retrieve_resource_head(pk: int) -> None: ... @put(path="/resources/{pk:int}") async def update_resource(data: Resource, pk: int) -> Resource: ... @patch(path="/resources/{pk:int}", dto=PartialResourceDTO) async def partially_update_resource( data: DTOData[PartialResourceDTO], pk: int ) -> Resource: ... @delete(path="/resources/{pk:int}") async def delete_resource(pk: int) -> None: ... Although these :term:`decorators ` are merely subclasses of :class:`~.handlers.HTTPRouteHandler` that pre-set the :paramref:`~.handlers.HTTPRouteHandler.http_method`, using :func:`@get() <.handlers.get>`, :func:`@patch() <.handlers.patch>`, :func:`@put() <.handlers.put>`, :func:`@delete() <.handlers.delete>`, or :func:`@post() <.handlers.post>` instead of :func:`@route() <.handlers.route>` makes the code clearer and simpler. Furthermore, in the OpenAPI specification each unique combination of HTTP verb (e.g. ``GET``, ``POST``, etc.) and path is regarded as a distinct `operation `_\ , and each operation should be distinguished by a unique :paramref:`~.handlers.HTTPRouteHandler.operation_id` and optimally also have a :paramref:`~.handlers.HTTPRouteHandler.summary` and :paramref:`~.handlers.HTTPRouteHandler.description` sections. As such, using the :func:`@route() <.handlers.route>` :term:`decorator` is discouraged. Instead, the preferred pattern is to share code using secondary class methods or by abstracting code to reusable functions. Websocket route handlers ------------------------ A WebSocket connection can be handled with a :func:`@websocket() <.handlers.WebsocketRouteHandler>` route handler. .. note:: The websocket handler is a low level approach, requiring to handle the socket directly, and dealing with keeping it open, exceptions, client disconnects, and content negotiation. For a more high level approach to handling WebSockets, see :doc:`/usage/websockets` .. code-block:: python :caption: Using the :func:`@websocket() <.handlers.WebsocketRouteHandler>` route handler :term:`decorator` from litestar import WebSocket, websocket @websocket(path="/socket") async def my_websocket_handler(socket: WebSocket) -> None: await socket.accept() await socket.send_json({...}) await socket.close() The :func:`@websocket() <.handlers.WebsocketRouteHandler>` :term:`decorator` is an alias of the :class:`~.handlers.WebsocketRouteHandler` class. Thus, the below code is equivalent to the one above: .. code-block:: python :caption: Using the :class:`~.handlers.WebsocketRouteHandler` class directly from litestar import WebSocket from litestar.handlers.websocket_handlers import WebsocketRouteHandler @WebsocketRouteHandler(path="/socket") async def my_websocket_handler(socket: WebSocket) -> None: await socket.accept() await socket.send_json({...}) await socket.close() In difference to HTTP routes handlers, websocket handlers have the following requirements: #. They **must** declare a ``socket`` :term:`kwarg `. #. They **must** have a return :term:`annotation` of ``None``. #. They **must** be :ref:`async functions `. These requirements are enforced using inspection, and if any of them is unfulfilled an informative exception will be raised. OpenAPI currently does not support websockets. As such no schema will be generated for these route handlers. .. seealso:: * :class:`~.handlers.WebsocketRouteHandler` * :doc:`/usage/websockets` ASGI route handlers ------------------- If you need to write your own ASGI application, you can do so using the :func:`@asgi() <.handlers.asgi>` :term:`decorator`: .. code-block:: python :caption: Using the :func:`@asgi() <.handlers.asgi>` route handler :term:`decorator` from litestar.types import Scope, Receive, Send from litestar.status_codes import HTTP_400_BAD_REQUEST from litestar import Response, asgi @asgi(path="/my-asgi-app") async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": if scope["method"] == "GET": response = Response({"hello": "world"}) await response(scope=scope, receive=receive, send=send) return response = Response( {"detail": "unsupported request"}, status_code=HTTP_400_BAD_REQUEST ) await response(scope=scope, receive=receive, send=send) Like other route handlers, the :func:`@asgi() <.handlers.asgi>` :term:`decorator` is an alias of the :class:`~.handlers.ASGIRouteHandler` class. Thus, the code below is equivalent to the one above: .. code-block:: python :caption: Using the :class:`~.handlers.ASGIRouteHandler` class directly from litestar import Response from litestar.handlers.asgi_handlers import ASGIRouteHandler from litestar.status_codes import HTTP_400_BAD_REQUEST from litestar.types import Scope, Receive, Send @ASGIRouteHandler(path="/my-asgi-app") async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": if scope["method"] == "GET": response = Response({"hello": "world"}) await response(scope=scope, receive=receive, send=send) return response = Response( {"detail": "unsupported request"}, status_code=HTTP_400_BAD_REQUEST ) await response(scope=scope, receive=receive, send=send) Limitations of ASGI route handlers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In difference to the other route handlers, the :func:`@asgi() <.handlers.asgi>` route handler accepts only three :term:`kwargs ` that **must** be defined: * ``scope``, a mapping of values describing the ASGI connection. It always includes a ``type`` key, with the values being either ``http`` or ``websocket``, and a ``path`` key. If the type is ``http``, the scope dictionary will also include a ``method`` key with the value being one of ``DELETE``, ``GET``, ``POST``, ``PATCH``, ``PUT``, ``HEAD``. * ``receive``, an injected function by which the ASGI application receives messages. * ``send``, an injected function by which the ASGI application sends messages. You can read more about these in the `ASGI specification `_. Additionally, ASGI route handler functions **must** be :ref:`async functions `. This is enforced using inspection, and if the function is not an :ref:`async functions `, an informative exception will be raised. See the :class:`ASGIRouteHandler API reference documentation <.handlers.asgi_handlers.ASGIRouteHandler>` for full details on the :func:`@asgi() <.handlers.asgi>` :term:`decorator` and the :term:`kwargs ` it accepts. Route handler indexing ---------------------- You can provide a :paramref:`~.handlers.base.BaseRouteHandler.name` :term:`kwarg ` in all route handler :term:`decorators `. The value for this :term:`kwarg ` **must be unique**, otherwise :exc:`~.exceptions.ImproperlyConfiguredException` exception will be raised. The default value for :paramref:`~.handlers.base.BaseRouteHandler.name` is value returned by the handler's :meth:`~object.__str__` method which should be the full dotted path to the handler (e.g., ``app.controllers.projects.list`` for ``list`` function residing in ``app/controllers/projects.py`` file). :paramref:`~.handlers.base.BaseRouteHandler.name` can be used to dynamically retrieve (i.e. during runtime) a mapping containing the route handler instance and paths, also it can be used to build a URL path for that handler: .. code-block:: python :caption: Using the :paramref:`~.handlers.base.BaseRouteHandler.name` :term:`kwarg ` to retrieve a route handler instance and paths from litestar import Litestar, Request, get from litestar.exceptions import NotFoundException from litestar.response import Redirect @get("/abc", name="one") def handler_one() -> None: pass @get("/xyz", name="two") def handler_two() -> None: pass @get("/def/{param:int}", name="three") def handler_three(param: int) -> None: pass @get("/{handler_name:str}", name="four") def handler_four(request: Request, name: str) -> Redirect: handler_index = request.app.get_handler_index_by_name(name) if not handler_index: raise NotFoundException(f"no handler matching the name {name} was found") # handler_index == { "paths": ["/"], "handler": ..., "qualname": ... } # do something with the handler index below, e.g. send a redirect response to the handler, or access # handler.opt and some values stored there etc. return Redirect(path=handler_index[0]) @get("/redirect/{param_value:int}", name="five") def handler_five(request: Request, param_value: int) -> Redirect: path = request.app.route_reverse("three", param=param_value) return Redirect(path=path) app = Litestar(route_handlers=[handler_one, handler_two, handler_three]) :meth:`~.app.Litestar.route_reverse` will raise :exc:`~.exceptions.NoRouteMatchFoundException` if route with given name was not found or if any of path :term:`parameters ` is missing or if any of passed path :term:`parameters ` types do not match types in the respective route declaration. However, :class:`str` is accepted in place of :class:`~datetime.datetime`, :class:`~datetime.date`, :class:`~datetime.time`, :class:`~datetime.timedelta`, :class:`float`, and :class:`~pathlib.Path` parameters, so you can apply custom formatting and pass the result to :meth:`~.app.Litestar.route_reverse`. If handler has multiple paths attached to it :meth:`~.app.Litestar.route_reverse` will return the path that consumes the most number of :term:`keyword arguments ` passed to the function. .. code-block:: python :caption: Using the :meth:`~.app.Litestar.route_reverse` method to build a URL path for a route handler from litestar import get, Request @get( ["/some-path", "/some-path/{id:int}", "/some-path/{id:int}/{val:str}"], name="handler_name", ) def handler(id: int = 1, val: str = "default") -> None: ... @get("/path-info") def path_info(request: Request) -> str: path_optional = request.app.route_reverse("handler_name") # /some-path` path_partial = request.app.route_reverse("handler_name", id=100) # /some-path/100 path_full = request.app.route_reverse("handler_name", id=100, val="value") # /some-path/100/value` return f"{path_optional} {path_partial} {path_full}" When a handler is associated with multiple routes having identical path :term:`parameters ` (e.g., an indexed handler registered across multiple routers), the output of :meth:`~.app.Litestar.route_reverse` is unpredictable. This :term:`callable` will return a formatted path; however, its selection may appear arbitrary. Therefore, reversing URLs under these conditions is **strongly** advised against. If you have access to :class:`~.connection.Request` instance you can make reverse lookups using :meth:`~.connection.ASGIConnection.url_for` method which is similar to :meth:`~.app.Litestar.route_reverse` but returns an absolute URL. .. _handler_opts: Adding arbitrary metadata to handlers -------------------------------------- All route handler :term:`decorators ` accept a key called ``opt`` which accepts a :term:`dictionary ` of arbitrary values, e.g., .. code-block:: python :caption: Adding arbitrary metadata to a route handler through the ``opt`` :term:`kwarg ` from litestar import get @get("/", opt={"my_key": "some-value"}) def handler() -> None: ... This dictionary can be accessed by a :doc:`route guard `, or by accessing the :attr:`~.connection.ASGIConnection.route_handler` property on a :class:`~.connection.request.Request` object, or using the :class:`ASGI scope ` object directly. Building on ``opt``, you can pass any arbitrary :term:`kwarg ` to the route handler :term:`decorator`, and it will be automatically set as a key in the ``opt`` dictionary: .. code-block:: python :caption: Adding arbitrary metadata to a route handler through the ``opt`` :term:`kwarg ` from litestar import get @get("/", my_key="some-value") def handler() -> None: ... assert handler.opt["my_key"] == "some-value" You can specify the ``opt`` :term:`dictionary ` at all layers of your application. On specific route handlers, on a controller, a router, and even on the app instance itself as described in :ref:`layered architecture ` The resulting :term:`dictionary ` is constructed by merging ``opt`` dictionaries of all layers. If multiple layers define the same key, the value from the closest layer to the response handler will take precedence. .. _signature_namespace: Signature :term:`namespace` --------------------------- Litestar produces a model of the arguments to any handler or dependency function, called a "signature model" which is used for parsing and validation of raw data to be injected into the function. Building the model requires inspection of the names and types of the signature parameters at runtime, and so it is necessary for the types to be available within the scope of the module - something that linting tools such as ``ruff`` or ``flake8-type-checking`` will actively monitor, and suggest against. For example, the name ``Model`` is *not* available at runtime in the following snippet: .. code-block:: python :caption: A route handler with a type that is not available at runtime from __future__ import annotations from typing import TYPE_CHECKING from litestar import Controller, post if TYPE_CHECKING: from domain import Model class MyController(Controller): @post() def create_item(data: Model) -> Model: return data In this example, Litestar will be unable to generate the signature model because the type ``Model`` does not exist in the module scope at runtime. We can address this on a case-by-case basis by silencing our linters, for example: .. code-block:: python :no-upgrade: :caption: Silencing linters for a type that is not available at runtime from __future__ import annotations from typing import TYPE_CHECKING from litestar import Controller, post # Choose the appropriate noqa directive according to your linter from domain import Model # noqa: TCH002 However, this approach can get tedious; as an alternative, Litestar accepts a ``signature_types`` sequence at every :ref:`layer ` of the application, as demonstrated in the following example: .. literalinclude:: /examples/signature_namespace/domain.py :language: python :caption: This module defines our domain type in some central place. This module defines our controller, note that we do not import ``Model`` into the runtime :term:`namespace`, nor do we require any directives to control behavior of linters. .. literalinclude:: /examples/signature_namespace/controller.py :language: python :caption: This module defines our controller without importing ``Model`` into the runtime namespace. Finally, we ensure that our application knows that when it encounters the name "Model" when parsing signatures, that it should reference our domain ``Model`` type. .. literalinclude:: /examples/signature_namespace/app.py :language: python :caption: Ensuring the application knows how to resolve the ``Model`` type when parsing signatures. .. tip:: If you want to map your type to a name that is different from its ``__name__`` attribute, you can use the :paramref:`~.handlers.base.BaseRouteHandler.signature_namespace` parameter, e.g., ``app = Litestar(signature_namespace={"FooModel": Model})``. This enables import patterns like ``from domain.foo import Model as FooModel`` inside ``if TYPE_CHECKING`` blocks. Default signature :term:`namespace` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Litestar automatically adds some names to the signature :term:`namespace` when parsing signature models in order to support injection of the :ref:`handler-function-kwargs`. These names are: * ``Headers`` * ``ImmutableState`` * ``Receive`` * ``Request`` * ``Scope`` * ``Send`` * ``State`` * ``WebSocket`` * ``WebSocketScope`` The import of any of these names can be safely left inside an ``if TYPE_CHECKING:`` block without any configuration required. litestar-2.16.0/docs/usage/routing/index.rst000066400000000000000000000006411500564371300210200ustar00rootroot00000000000000======= Routing ======= Routing is a fundamental aspect of building web applications with Litestar. It involves mapping URLs to the appropriate request handlers that process and respond to client requests. Litestar provides a flexible and intuitive routing system that allows you to define routes at various layers of your application. .. toctree:: :caption: Articles overview handlers parameters litestar-2.16.0/docs/usage/routing/overview.rst000066400000000000000000000265461500564371300215730ustar00rootroot00000000000000======== Overview ======== Registering Routes ------------------- At the root of every Litestar application there is an instance of the :class:`Litestar ` class, on which the root level :class:`controllers <.controller.Controller>`, :class:`routers <.router.Router>`, and :class:`route handler <.handlers.BaseRouteHandler>` functions are registered using the :paramref:`~litestar.config.app.AppConfig.route_handlers` :term:`kwarg `: .. code-block:: python :caption: Registering route handlers from litestar import Litestar, get @get("/sub-path") def sub_path_handler() -> None: ... @get() def root_handler() -> None: ... app = Litestar(route_handlers=[root_handler, sub_path_handler]) Components registered on the app are appended to the root path. Thus, the ``root_handler`` function will be called for the path ``"/"``, whereas the ``sub_path_handler`` will be called for ``"/sub-path"``. You can also declare a function to handle multiple paths, e.g.: .. code-block:: python :caption: Registering a route handler for multiple paths from litestar import get, Litestar @get(["/", "/sub-path"]) def handler() -> None: ... app = Litestar(route_handlers=[handler]) To handle more complex path schemas you should use :class:`controllers <.controller.Controller>` and :class:`routers <.router.Router>` Registering routes dynamically ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Occasionally there is a need for dynamic route registration. Litestar supports this via the :paramref:`~.app.Litestar.register` method exposed by the Litestar app instance: .. code-block:: python :caption: Registering a route handler dynamically with the :paramref:`~.app.Litestar.register` method from litestar import Litestar, get @get() def root_handler() -> None: ... app = Litestar(route_handlers=[root_handler]) @get("/sub-path") def sub_path_handler() -> None: ... app.register(sub_path_handler) Since the app instance is attached to all instances of :class:`~.connection.base.ASGIConnection`, :class:`~.connection.request.Request`, and :class:`~.connection.websocket.WebSocket` objects, you can in effect call the :meth:`~.router.Router.register` method inside route handler functions, middlewares, and even injected dependencies. For example: .. code-block:: python :caption: Call the :meth:`~.router.Router.register` method from inside a route handler function from typing import Any from litestar import Litestar, Request, get @get("/some-path") def route_handler(request: Request[Any, Any]) -> None: @get("/sub-path") def sub_path_handler() -> None: ... request.app.register(sub_path_handler) app = Litestar(route_handlers=[route_handler]) In the above we dynamically created the ``sub_path_handler`` and registered it inside the ``route_handler`` function. .. caution:: Although Litestar exposes the :meth:`register <.router.Router.register>` method, it should not be abused. Dynamic route registration increases the application complexity and makes it harder to reason about the code. It should therefore be used only when absolutely required. :class:`Routers <.router.Router>` --------------------------------- :class:`Routers <.router.Router>` are instances of the :class:`~.router.Router`, class which is the base class for the :class:`Litestar app <.app.Litestar>` itself. A :class:`~.router.Router` can register :class:`Controllers <.controller.Controller>`, :class:`route handler <.handlers.BaseRouteHandler>` functions, and other routers, similarly to the Litestar constructor: .. code-block:: python :caption: Registering a :class:`~.router.Router` from litestar import Litestar, Router, get @get("/{order_id:int}") def order_handler(order_id: int) -> None: ... order_router = Router(path="/orders", route_handlers=[order_handler]) base_router = Router(path="/base", route_handlers=[order_router]) app = Litestar(route_handlers=[base_router]) Once ``order_router`` is registered on ``base_router``, the handler function registered on ``order_router`` will become available on ``/base/orders/{order_id}``. :class:`Controllers <.controller.Controller>` --------------------------------------------- :class:`Controllers <.controller.Controller>` are subclasses of the :class:`Controller <.controller.Controller>` class. They are used to organize endpoints under a specific sub-path, which is the controller's path. Their purpose is to allow users to utilize Python OOP for better code organization and organize code by logical concerns. .. dropdown:: Click to see an example of registering a controller .. code-block:: python :caption: Registering a :class:`~.controller.Controller` from litestar.plugins.pydantic import PydanticDTO from litestar.controller import Controller from litestar.dto import DTOConfig, DTOData from litestar.handlers import get, post, patch, delete from pydantic import BaseModel, UUID4 class UserOrder(BaseModel): user_id: int order: str class PartialUserOrderDTO(PydanticDTO[UserOrder]): config = DTOConfig(partial=True) class UserOrderController(Controller): path = "/user-order" @post() async def create_user_order(self, data: UserOrder) -> UserOrder: ... @get(path="/{order_id:uuid}") async def retrieve_user_order(self, order_id: UUID4) -> UserOrder: ... @patch(path="/{order_id:uuid}", dto=PartialUserOrderDTO) async def update_user_order( self, order_id: UUID4, data: DTOData[PartialUserOrderDTO] ) -> UserOrder: ... @delete(path="/{order_id:uuid}") async def delete_user_order(self, order_id: UUID4) -> None: ... The above is a simple example of a "CRUD" controller for a model called ``UserOrder``. You can place as many :doc:`route handler methods ` on a controller, as long as the combination of path+http method is unique. The ``path`` that is defined on the :class:`controller <.controller.Controller>` is appended before the path that is defined for the route handlers declared on it. Thus, in the above example, ``create_user_order`` has the path of the :class:`controller <.controller.Controller>` - ``/user-order/``, while ``retrieve_user_order`` has the path ``/user-order/{order_id:uuid}"``. .. note:: If you do not declare a ``path`` class variable on the controller, it will default to the root path of ``"/"``. Registering components multiple times -------------------------------------- You can register both standalone route handler functions and controllers multiple times. Controllers ^^^^^^^^^^^ .. code-block:: python :caption: Registering a controller multiple times from litestar import Router, Controller, get class MyController(Controller): path = "/controller" @get() def handler(self) -> None: ... internal_router = Router(path="/internal", route_handlers=[MyController]) partner_router = Router(path="/partner", route_handlers=[MyController]) consumer_router = Router(path="/consumer", route_handlers=[MyController]) In the above, the same ``MyController`` class has been registered on three different routers. This is possible because what is passed to the :class:`router <.router.Router>` is not a class instance but rather the class itself. The :class:`router <.router.Router>` creates its own instance of the :class:`controller <.controller.Controller>`, which ensures encapsulation. Therefore, in the above example, three different instances of ``MyController`` will be created, each mounted on a different sub-path, e.g., ``/internal/controller``, ``/partner/controller``, and ``/consumer/controller``. Route handlers ^^^^^^^^^^^^^^ You can also register standalone route handlers multiple times: .. code-block:: python :caption: Registering a route handler multiple times from litestar import Litestar, Router, get @get(path="/handler") def my_route_handler() -> None: ... internal_router = Router(path="/internal", route_handlers=[my_route_handler]) partner_router = Router(path="/partner", route_handlers=[my_route_handler]) consumer_router = Router(path="/consumer", route_handlers=[my_route_handler]) Litestar(route_handlers=[internal_router, partner_router, consumer_router]) When the handler function is registered, it's actually copied. Thus, each router has its own unique instance of the route handler. Path behaviour is identical to that of controllers above, namely, the route handler function will be accessible in the following paths: ``/internal/handler``, ``/partner/handler``, and ``/consumer/handler``. .. attention:: You can nest routers as you see fit - but be aware that once a router has been registered it cannot be re-registered or an exception will be raised. Mounting ASGI Apps ------------------- Litestar support "mounting" ASGI applications on sub-paths, i.e., specifying a handler function that will handle all requests addressed to a given path. .. dropdown:: Click to see an example of mounting an ASGI app .. literalinclude:: /examples/routing/mount_custom_app.py :language: python :caption: Mounting an ASGI App The handler function will receive all requests with an url that begins with ``/some/sub-path``, e.g, ``/some/sub-path``, ``/some/sub-path/abc``, ``/some/sub-path/123/another/sub-path``, etc. .. admonition:: Technical Details :class: info If we are sending a request to the above with the url ``/some/sub-path``, the handler will be invoked and the value of ``scope["path"]`` will equal ``"/"``. If we send a request to ``/some/sub-path/abc``, it will also be invoked,and ``scope["path"]`` will equal ``"/abc"``. Mounting is especially useful when you need to combine components of other ASGI applications - for example, for third party libraries. The following example is identical in principle to the one above, but it uses `Starlette `_: .. dropdown:: Click to see an example of mounting a Starlette app .. literalinclude:: /examples/routing/mounting_starlette_app.py :language: python :caption: Mounting a Starlette App .. admonition:: Why Litestar uses radix based routing The regex matching used by popular frameworks such as Starlette, FastAPI or Flask is very good at resolving path parameters fast, giving it an advantage when a URL has a lot of path parameters - what we can think of as ``vertical`` scaling. On the other hand, it is not good at scaling horizontally - the more routes, the less performant it becomes. Thus, there is an inverse relation between performance and application size with this approach that strongly favors very small microservices. The **trie** based approach used by Litestar is agnostic to the number of routes of the application giving it better horizontal scaling characteristics at the expense of somewhat slower resolution of path parameters. Litestar implements its routing solution that is based on the concept of a `radix tree `_ or *trie*. .. seealso:: If you are interested in the technical aspects of the implementation, refer to `this GitHub issue `_ - it includes an indepth discussion of the pertinent code. litestar-2.16.0/docs/usage/routing/parameters.rst000066400000000000000000000246211500564371300220600ustar00rootroot00000000000000Parameters =========== Path Parameters --------------- Path :term:`parameters ` are parameters declared as part of the ``path`` component of the URL. They are declared using a simple syntax ``{param_name:param_type}`` : .. literalinclude:: /examples/parameters/path_parameters_1.py :language: python :caption: Defining a path parameter in a route handler In the above there are two components: 1. The path :term:`parameter` is defined in the :class:`@get() <.handlers.get>` :term:`decorator`, which declares both the parameter's name (``user_id``) and type (:class:`int`). 2. The :term:`decorated ` function ``get_user`` defines a parameter with the same name as the parameter defined in the ``path`` :term:`kwarg `. The correlation of parameter name ensures that the value of the path parameter will be injected into the function when it is called. Supported Path Parameter Types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Currently, the following types are supported: * ``date``: Accepts date strings and time stamps. * ``datetime``: Accepts date-time strings and time stamps. * ``decimal``: Accepts decimal values and floats. * :class:`float`: Accepts ints and floats. * :class:`int`: Accepts ints and floats. * :class:`path`: Accepts valid POSIX paths. * :class:`str`: Accepts all string values. * ``time``: Accepts time strings with optional timezone compatible with standard (Pydantic/Msgspec) datetime formats. * ``timedelta``: Accepts duration strings compatible with the standard (Pydantic/Msgspec) timedelta formats. * ``uuid``: Accepts all uuid values. The types declared in the path :term:`parameter` and the function do not need to match 1:1 - as long as parameter inside the function declaration is typed with a "higher" type to which the lower type can be coerced, this is fine. For example, consider this: .. literalinclude:: /examples/parameters/path_parameters_2.py :language: python :caption: Coercing path parameters into different types The :term:`parameter` defined inside the ``path`` :term:`kwarg ` is typed as :class:`int` , because the value passed as part of the request will be a timestamp in milliseconds without any decimals. The parameter in the function declaration though is typed as :class:`datetime.datetime`. This works because the int value will be automatically coerced from an :class:`int` into a :class:`~datetime.datetime`. Thus, when the function is called it will be called with a :class:`~datetime.datetime`-typed parameter. .. note:: You only need to define the :term:`parameter` in the function declaration if it is actually used inside the function. If the path parameter is part of the path, but the function does not use it, it is fine to omit it. It will still be validated and added to the OpenAPI schema correctly. The Parameter function ---------------------- :func:`~.params.Parameter` is a helper function wrapping a :term:`parameter` with extra information to be added to the OpenAPI schema. Extra validation and documentation for path params ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you want to add validation or enhance the OpenAPI documentation generated for a given path :term:`parameter`, you can do so using the `the parameter function`_: .. literalinclude:: /examples/parameters/path_parameters_3.py :language: python :caption: Adding extra validation and documentation to a path parameter In the above example, :func:`~.params.Parameter` is used to restrict the value of :paramref:`~.params.Parameter.version` to a range between 1 and 10, and then set the :paramref:`~.params.Parameter.title`, :paramref:`~.params.Parameter.description`, :paramref:`~.params.Parameter.examples`, and :paramref:`externalDocs <.params.Parameter.external_docs>` sections of the OpenAPI schema. Query Parameters ---------------- Query :term:`parameters ` are defined as :term:`keyword arguments ` to handler functions. Every :term:`keyword argument ` that is not otherwise specified (for example as a :ref:`path parameter `) will be interpreted as a query parameter. .. literalinclude:: /examples/parameters/query_params.py :language: python :caption: Defining query parameters in a route handler .. admonition:: Technical details :class: info These :term:`parameters ` will be parsed from the function signature and used to generate an internal data model. This model in turn will be used to validate the parameters and generate the OpenAPI schema. This ability allows you to use any number of schema/modelling libraries, including Pydantic, Msgspec, Attrs, and Dataclasses, and it will follow the same kind of validation and parsing as you would get from these libraries. Query :term:`parameters ` come in three basic types: - Required - Required with a default value - Optional with a default value Query parameters are **required** by default. If one such a parameter has no value, a :exc:`~.exceptions.http_exceptions.ValidationException` will be raised. Default values ~~~~~~~~~~~~~~ In this example, ``param`` will have the value ``"hello"`` if it is not specified in the request. If it is passed as a query :term:`parameter` however, it will be overwritten: .. literalinclude:: /examples/parameters/query_params_default.py :language: python :caption: Defining a default value for a query parameter Optional :term:`parameters ` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Instead of only setting a default value, it is also possible to make a query parameter entirely optional. Here, we give a default value of ``None`` , but still declare the type of the query parameter to be a :class:`string `. This means that this parameter is not required. If it is given, it has to be a :class:`string `. If it is not given, it will have a default value of ``None`` .. literalinclude:: /examples/parameters/query_params_optional.py :language: python :caption: Defining an optional query parameter Type coercion ------------- It is possible to coerce query :term:`parameters ` into different types. A query starts out as a :class:`string `, but its values can be parsed into all kinds of types. .. literalinclude:: /examples/parameters/query_params_types.py :language: python :caption: Coercing query parameters into different types Alternative names and constraints --------------------------------- Sometimes you might want to "remap" query :term:`parameters ` to allow a different name in the URL than what is being used in the handler function. This can be done by making use of :func:`~.params.Parameter`. .. literalinclude:: /examples/parameters/query_params_remap.py :language: python :caption: Remapping query parameters to different names Here, we remap from ``snake_case`` in the handler function to ``camelCase`` in the URL. This means that for the URL ``http://127.0.0.1:8000?camelCase=foo`` , the value of ``camelCase`` will be used for the value of the ``snake_case`` parameter. :func:`~.params.Parameter` also allows us to define additional constraints: .. literalinclude:: /examples/parameters/query_params_constraints.py :language: python :caption: Constraints on query parameters In this case, ``param`` is validated to be an *integer larger than 5*. Header and Cookie Parameters ---------------------------- Unlike *Query* :term:`parameters `, *Header* and *Cookie* parameters have to be declared using `the parameter function`_ , for example: .. literalinclude:: /examples/parameters/header_and_cookie_parameters.py :language: python :caption: Defining header and cookie parameters As you can see in the above, header parameters are declared using the ``header`` :term:`kwargs ` and cookie parameters using the ``cookie`` :term:`kwarg `. Aside form this difference they work the same as query parameters. Layered Parameters ------------------ As part of Litestar's :ref:`layered architecture `, you can declare :term:`parameters ` not only as part of individual route handler functions, but also on other layers of the application: .. literalinclude:: /examples/parameters/layered_parameters.py :language: python :caption: Declaring parameters on different layers of the application In the above we declare :term:`parameters ` on the :class:`Litestar app <.app.Litestar>`, :class:`router <.router.Router>`, and :class:`controller <.controller.Controller>` layers in addition to those declared in the route handler. Now, examine these more closely. * ``app_param`` is a cookie parameter with the key ``special-cookie``. We type it as :class:`str` by passing this as an arg to the :func:`~.params.Parameter` function. This is required for us to get typing in the OpenAPI doc. Additionally, this parameter is assumed to be required because it is not explicitly set as ``False`` on :paramref:`~.params.Parameter.required`. This is important because the route handler function does not declare a parameter called ``app_param`` at all, but it will still require this param to be sent as part of the request of validation will fail. * ``router_param`` is a header parameter with the key ``MyHeader``. Because it is set as ``False`` on :paramref:`~.params.Parameter.required`, it will not fail validation if not present unless explicitly declared by a route handler - and in this case it is. Thus, it is actually required for the router handler function that declares it as an :class:`str` and not an ``str | None``. If a :class:`string ` value is provided, it will be tested against the provided regex. * ``controller_param`` is a query param with the key ``controller_param``. It has an :paramref:`~.params.Parameter.lt` set to ``100`` defined on the controller, which means the provided value must be less than 100. Yet the route handler redeclares it with an :paramref:`~.params.Parameter.lt` set to ``50``, which means for the route handler this value must be less than 50. * ``local_param`` is a route handler local :ref:`query parameter `, and ``path_param`` is a :ref:`path parameter `. .. note:: You cannot declare path :term:`parameters ` in different application layers. The reason for this is to ensure simplicity - otherwise parameter resolution becomes very difficult to do correctly. litestar-2.16.0/docs/usage/security/000077500000000000000000000000001500564371300173365ustar00rootroot00000000000000litestar-2.16.0/docs/usage/security/abstract-authentication-middleware.rst000066400000000000000000000100211500564371300270150ustar00rootroot00000000000000================================== Implementing Custom Authentication ================================== Litestar exports :class:`~.middleware.authentication.AbstractAuthenticationMiddleware`, which is an :term:`abstract base class` (ABC) that implements the :class:`~.middleware.base.MiddlewareProtocol`. To add authentication to your app using this class as a basis, subclass it and implement the abstract method :meth:`~.middleware.authentication.AbstractAuthenticationMiddleware.authenticate_request`: .. code-block:: python :caption: Adding authentication to your app by subclassing :class:`~.middleware.authentication.AbstractAuthenticationMiddleware` from litestar.middleware import ( AbstractAuthenticationMiddleware, AuthenticationResult, ) from litestar.connection import ASGIConnection class MyAuthenticationMiddleware(AbstractAuthenticationMiddleware): async def authenticate_request( self, connection: ASGIConnection ) -> AuthenticationResult: # do something here. ... As you can see, ``authenticate_request`` is an async function that receives a connection instance and is supposed to return an :class:`~.middleware.authentication.AuthenticationResult` instance, which is a :doc:`dataclass ` that has two attributes: 1. ``user``: a non-optional value representing a user. It is typed as ``Any`` so it receives any value, including ``None``. 2. ``auth``: an optional value representing the authentication scheme. Defaults to ``None``. These values are then set as part of the ``scope`` dictionary, and they are made available as :attr:`Request.user <.connection.ASGIConnection.user>` and :attr:`Request.auth <.connection.ASGIConnection.auth>` respectively, for HTTP route handlers, and :attr:`WebSocket.user <.connection.ASGIConnection.user>` and :attr:`WebSocket.auth <.connection.ASGIConnection.auth>` for websocket route handlers. Creating a Custom Authentication Middleware ----------------------------------------------- Since the above is quite hard to grasp in the abstract, let us see an example. We start off by creating a user model. It can be implemented using msgspec, Pydantic, an ODM, ORM, etc. For the sake of this example here let us say it is a dataclass: .. literalinclude:: /examples/security/using_abstract_authentication_middleware.py :lines: 19-26 :language: python :caption: user and token models We can now create our authentication middleware: .. literalinclude:: /examples/security/using_abstract_authentication_middleware.py :lines: 29-43 :language: python :caption: authentication_middleware.py Finally, we need to pass our middleware to the Litestar constructor: .. literalinclude:: /examples/security/using_abstract_authentication_middleware.py :lines: 80-88 :language: python :caption: main.py That is it. ``CustomAuthenticationMiddleware`` will now run for every request, and we would be able to access these in a http route handler in the following way: .. literalinclude:: /examples/security/using_abstract_authentication_middleware.py :lines: 46-51 :language: python :caption: Accessing the user and auth in a http route handler with ``CustomAuthenticationMiddleware`` Or for a websocket route: .. literalinclude:: /examples/security/using_abstract_authentication_middleware.py :lines: 54-59 :language: python :caption: Accessing the user and auth in a websocket route handler with ``CustomAuthenticationMiddleware`` And if you would like to exclude individual routes outside those configured: .. literalinclude:: /examples/security/using_abstract_authentication_middleware.py :lines: 62-70 :language: python :caption: Excluding individual routes from ``CustomAuthenticationMiddleware`` And of course use the same kind of mechanism for dependencies: .. literalinclude:: /examples/security/using_abstract_authentication_middleware.py :lines: 73-77 :language: python :caption: Using ``CustomAuthenticationMiddleware`` in a dependency litestar-2.16.0/docs/usage/security/excluding-and-including-endpoints.rst000066400000000000000000000073411500564371300265720ustar00rootroot00000000000000Excluding and including endpoints ================================= Please make sure you read the :doc:`security backends documentation ` first for learning how to set up a security backend. This section focuses on configuring the ``exclude`` rule for those backends. There are multiple ways for including or excluding endpoints in the authentication flow. The default rules are configured in the ``Auth`` object used (subclass of :class:`~.security.base.AbstractSecurityConfig`). The examples below use :class:`~.security.session_auth.auth.SessionAuth` but it is the same for :class:`~.security.jwt.auth.JWTAuth` and :class:`~.security.jwt.auth.JWTCookieAuth`. Excluding routes -------------------- The ``exclude`` argument takes a :class:`string ` or :class:`list` of :class:`strings ` that are interpreted as regex patterns. For example, the configuration below would apply authentication to all endpoints except those where the route starts with ``/login``, ``/signup``, or ``/schema``. Thus, one does not have to exclude ``/schema/swagger`` as well - it is included in the ``/schema`` pattern. .. danger:: Passing ``/`` will disable authentication for all routes, since, as a regex, it matches *every* path. .. code-block:: python session_auth = SessionAuth[User, ServerSideSessionBackend]( retrieve_user_handler=retrieve_user_handler, # we must pass a config for a session backend. # all session backends are supported session_backend_config=ServerSideSessionConfig(), # exclude any URLs that should not have authentication. # We exclude the documentation URLs, signup and login. exclude=["/login", "/signup", "/schema"], ) ... Including routes ---------------- Since the exclusion rules are evaluated as regex, it is possible to pass a rule that inverts exclusion - meaning, no path but the one specified in the pattern will be protected by authentication. In the example below, only endpoints under the ``/secured`` route will require authentication - all other routes do not. .. code-block:: python ... session_auth = SessionAuth[User, ServerSideSessionBackend]( retrieve_user_handler=retrieve_user_handler, # we must pass a config for a session backend. # all session backends are supported session_backend_config=ServerSideSessionConfig(), # exclude any URLs that should not have authentication. # We exclude the documentation URLs, signup and login. exclude=[r"^(?!.*\/secured$).*$"], ) ... Exclude from auth -------------------- Sometimes, you might want to apply authentication to all endpoints under a route but a few selected. In this case, you can pass ``exclude_from_auth=True`` to the route handler as shown below. .. code-block:: python ... @get("/secured") def secured_route() -> Any: ... @get("/unsecured", exclude_from_auth=True) def unsecured_route() -> Any: ... ... You can set an alternative option key in the security configuration, e.g., you can use ``no_auth`` instead of ``exclude_from_auth``. .. code-block:: python ... @get("/secured") def secured_route() -> Any: ... @get("/unsecured", no_auth=True) def unsecured_route() -> Any: ... session_auth = SessionAuth[User, ServerSideSessionBackend]( retrieve_user_handler=retrieve_user_handler, # we must pass a config for a session backend. # all session backends are supported session_backend_config=ServerSideSessionConfig(), # exclude any URLs that should not have authentication. # We exclude the documentation URLs, signup and login. exclude=["/login", "/signup", "/schema"], exclude_opt_key="no_auth" # default value is `exclude_from_auth` ) ... litestar-2.16.0/docs/usage/security/guards.rst000066400000000000000000000076271500564371300213710ustar00rootroot00000000000000Guards ====== Guards are :term:`callables ` that receive two arguments - ``connection``, which is the :class:`Request <.connection.Request>` or :class:`WebSocket <.connection.WebSocket>` instance (both sub-classes of :class:`~.connection.ASGIConnection`), and ``route_handler``, which is a copy of the :class:`~.handlers.BaseRouteHandler`. Their role is to *authorize* the request by verifying that the connection is allowed to reach the endpoint handler in question. If verification fails, the guard should raise an :exc:`HTTPException`, usually a :class:`~.exceptions.NotAuthorizedException` with a ``status_code`` of ``401``. To illustrate this we will implement a rudimentary role based authorization system in our Litestar app. As we have done for ``authentication``, we will assume that we added some sort of persistence layer without actually specifying it in the example. We begin by creating an :class:`~enum.Enum` with two roles - ``consumer`` and ``admin``: .. literalinclude:: /examples/security/guards.py :language: python :lines: 12-14 :caption: Defining the enum ``UserRole`` Our ``User`` model will now look like this: .. literalinclude:: /examples/security/guards.py :language: python :lines: 17-24 :caption: User model for role based authorization Given that the ``User`` model has a ``role`` property we can use it to authorize a request. Let us create a guard that only allows admin users to access certain route handlers and then add it to a route handler function: .. literalinclude:: /examples/security/guards.py :language: python :lines: 27-29, 32-33 :caption: Defining the guard ``admin_user_guard`` used to authorize certain route handlers Here, the ``admin_user_guard`` guard checks if the user is an admin. The connection has a `user` object attached to it thanks to the JWT middleware, see :doc:`authentication ` and in particular the :meth:`JWTAuth.retrieve_user_handler` method. Thus, only an admin user would be able to send a post request to the ``create_user`` handler. Guard scopes ------------ Guards are part of Litestar's :ref:`layered architecture ` and can be declared on all layers of the app - the Litestar instance, routers, controllers, and individual route handlers: .. literalinclude:: /examples/security/guards.py :language: python :lines: 36-49 :caption: Declaring guards on different layers of the app The placement of guards within the Litestar application depends on the scope and level of access control needed: - Should restrictions apply to individual route handlers? - Is the access control intended for all actions within a controller? - Are you aiming to secure all routes managed by a specific router? - Or do you need to enforce access control across the entire application? As you can see in the above examples - ``guards`` is a :class:`list`. This means you can add **multiple** guards at every layer. Unlike :doc:`dependencies ` , guards do not override each other but are rather *cumulative*. This means that you can define guards on different layers of your app, and they will combine. .. caution:: If guards are placed at the controller or the app level, they **will** be executed on all ``OPTIONS`` requests as well. For more details, including a workaround, refer https://github.com/litestar-org/litestar/issues/2314. The route handler "opt" key --------------------------- Occasionally there might be a need to set some values on the route handler itself - these can be permissions, or some other flag. This can be achieved with :ref:`the opts kwarg ` of route handler To illustrate this let us say we want to have an endpoint that is guarded by a "secret" token, to which end we create the following guard: .. literalinclude:: /examples/security/guards.py :language: python :lines: 52-61 litestar-2.16.0/docs/usage/security/index.rst000066400000000000000000000007371500564371300212060ustar00rootroot00000000000000Security ======== While Litestar is agnostic to the security scheme used - allowing users to use any standard and non-standard security scheme they deem necessary, it does include several builtin components that allow for easy implementation of authentication and authorization. .. toctree:: :titlesonly: :caption: Articles abstract-authentication-middleware security-backends guards excluding-and-including-endpoints jwt secret-datastructures litestar-2.16.0/docs/usage/security/jwt.rst000066400000000000000000000107541500564371300207030ustar00rootroot00000000000000JWT Security Backends ===================== Litestar offers optional JWT based security backends. To use these make sure to install the `pyjwt `_ and `cryptography `_ packages, or simply install Litestar with the ``jwt`` `extra `_: .. code-block:: shell :caption: Install Litestar with JWT extra pip install litestar[jwt] :class:`JWT Auth <.security.jwt.JWTAuth>` Backend ------------------------------------------------- This is the base JWT Auth backend. You can read about its particular API in the :class:`~.security.jwt.JWTAuth`. It sends the JWT token using a header - and it expects requests to send the JWT token using the same header key. .. dropdown:: Click to see the code .. literalinclude:: /examples/security/jwt/using_jwt_auth.py :language: python :caption: Using JWT Auth :class:`JWT Cookie Auth <.security.jwt.JWTCookieAuth>` Backend -------------------------------------------------------------- This backend inherits from the :class:`~.security.jwt.JWTAuth` backend, with the difference being that instead of using a header for the JWT Token, it uses a cookie. .. dropdown:: Click to see the code .. literalinclude:: /examples/security/jwt/using_jwt_cookie_auth.py :language: python :caption: Using JWT Cookie Auth :class:`OAuth2 Bearer <.security.jwt.auth.OAuth2PasswordBearerAuth>` Password Flow ---------------------------------------------------------------------------------- The :class:`~.security.jwt.auth.OAuth2PasswordBearerAuth` backend inherits from the :class:`~.security.jwt.JWTCookieAuth` backend. It works similarly to the :class:`~.security.jwt.JWTCookieAuth` backend, but is meant to be used for OAuth 2.0 Bearer password flows. .. dropdown:: Click to see the code .. literalinclude:: /examples/security/jwt/using_oauth2_password_bearer.py :language: python :caption: Using OAUTH2 Bearer Password Using a custom token class -------------------------- The token class used can be customized with arbitrary fields, by creating a subclass of :class:`~.security.jwt.Token`, and specifying it on the backend: .. literalinclude:: /examples/security/jwt/custom_token_cls.py :language: python :caption: Using a custom token The token will be converted from JSON into the appropriate type, including basic type conversions. .. important:: Complex type conversions, especially those including third libraries such as Pydantic or attrs, as well as any custom ``type_decoders`` are not available for converting the token. To support more complex conversions, the :meth:`~.security.jwt.Token.encode` and :meth:`~.security.jwt.Token.decode` methods must be overwritten in the subclass. Verifying issuer and audience ----------------------------- To verify the JWT ``iss`` (*issuer*) and ``aud`` (*audience*) claim, a list of accepted issuers or audiences can bet set on the authentication backend. When a JWT is decoded, the issuer or audience on the token is compared to the list of accepted issuers / audiences. If the value in the token does not match any value in the respective list, a :exc:`NotAuthorizedException` will be raised, returning a response with a ``401 Unauthorized`` status. .. literalinclude:: /examples/security/jwt/verify_issuer_audience.py :language: python :caption: Verifying issuer and audience Customizing token validation ---------------------------- Token decoding / validation can be further customized by overriding the :meth:`~.security.jwt.Token.decode_payload` method. It will be called by :meth:`~.security.jwt.Token.decode` with the encoded token string, and must return a dictionary representing the decoded payload, which will then used by :meth:`~.security.jwt.Token.decode` to construct an instance of the token class. .. literalinclude:: /examples/security/jwt/custom_decode_payload.py :language: python :caption: Customizing payload decoding Using token revocation ---------------------- Token revocation can be implemented by maintaining a list of revoked tokens and checking against this list during authentication. When a token is revoked, it should be added to the list, and any subsequent requests with that token should be denied. .. dropdown:: Click to see the code .. literalinclude:: /examples/security/jwt/using_token_revocation.py :language: python :caption: Implementing token revocation litestar-2.16.0/docs/usage/security/secret-datastructures.rst000066400000000000000000000044701500564371300244350ustar00rootroot00000000000000Handling Secrets ================ Overview -------- Two data structures are available to assist in handling secrets in web services: :class:`SecretString ` and :class:`SecretBytes `. These are containers for holding sensitive data within your application. Secret Parameters ----------------- The following example demonstrates how to use :class:`~datastructures.SecretString` to accept a secret value as a parameter in a GET request: .. literalinclude:: /examples/datastructures/secrets/secret_header.py :language: python :caption: Example of using ``SecretString`` for a Header Parameter .. note:: When storing and comparing secrets, use secure practices to prevent unauthorized access. For example, use environment variables, secret management services, or encrypted databases to store secrets securely. When comparing secrets, use :func:`secrets.compare_digest` or similar to mitigate the risk of timing attacks. .. note:: The :func:`headers ` attribute of the :class:`~connection.ASGIConnection` object stores the headers exactly as they are parsed from the ASGI message. Care should be taken to ensure that these headers are not logged or otherwise exposed in a way that could compromise the security of the application. Secret Body ----------- This example demonstrates use of a data structure with a :class:`~datastructures.SecretString` field to accept a secret within the HTTP body of a request: .. literalinclude:: /examples/datastructures/secrets/secret_body.py :language: python :caption: Example of using ``SecretString`` for a Request Body Security Considerations ----------------------- While :class:`SecretString` and :class:`SecretBytes` can help in securely transferring secret data through the framework, it's vital to adopt secure practices for storing and comparing secrets within your application. Here are a few guidelines: - Store secrets securely, using environment variables, secret management services, or encrypted databases. - Always use constant time comparison functions such as :func:`secrets.compare_digest` for comparing secret values to mitigate the risk of timing attacks. - Implement access controls and logging to monitor and restrict who can access sensitive information. litestar-2.16.0/docs/usage/security/security-backends.rst000066400000000000000000000021651500564371300235130ustar00rootroot00000000000000================= Security Backends ================= :class:`~.security.base.AbstractSecurityConfig` ----------------------------------------------- :doc:`litestar.security ` includes an :class:`~.security.base.AbstractSecurityConfig` class that serves as a basis for all the security backends offered by Litestar, and is also meant to be used as a basis for custom security backends created by users which you can read more about here: :doc:`/usage/security/abstract-authentication-middleware` Session Auth Backend -------------------- Litestar offers a builtin session auth backend that can be used out of the box with any of the :ref:`session backends ` supported by the Litestar session middleware. .. dropdown:: Click to see an example of using the session auth backend .. literalinclude:: /examples/security/using_session_auth.py :language: python :caption: Using Session Auth JWT Auth -------- Litestar includes several JWT security backends. Check out the :doc:`JWT documentation ` for more details. litestar-2.16.0/docs/usage/static-files.rst000066400000000000000000000116121500564371300206110ustar00rootroot00000000000000Static files ============ To serve static files (i.e., serve arbitrary files from a given directory), the :func:`~litestar.static_files.create_static_files_router` can be used to create a :class:`Router ` to handle this task. .. literalinclude:: /examples/static_files/full_example.py :language: python :caption: Serving static files using :func:`create_static_files_router ` In this example, files from the directory ``assets`` will be served on the path ``/static``. A file ``assets/hello.txt`` would now be available on ``/static/hello.txt`` .. attention:: Directories are interpreted as relative to the working directory from which the application is started Sending files as attachments ---------------------------- By default, files are sent "inline", meaning they will have a ``Content-Disposition: inline`` header. Setting :paramref:`~litestar.static_files.create_static_files_router.params.send_as_attachment` to ``True`` will send them with a ``Content-Disposition: attachment`` instead: .. literalinclude:: /examples/static_files/send_as_attachment.py :language: python :caption: Sending files as attachments using the the :paramref:`~litestar.static_files.create_static_files_router.params.send_as_attachment` parameter of :func:`create_static_files_router` HTML mode --------- "HTML mode" can be enabled by setting :paramref:`~litestar.static_files.create_static_files_router.params.html_mode` to ``True``. This will: - Serve and ``/index.html`` when the path ``/`` is requested - Attempt to serve ``/404.html`` when a requested file is not found .. literalinclude:: /examples/static_files/html_mode.py :language: python :caption: Serving HTML files using the :paramref:`~litestar.static_files.create_static_files_router.params.html_mode` parameter of :func:`create_static_files_router` Passing options to the generated router --------------------------------------- Options available on :class:`~litestar.router.Router` can be passed to directly :func:`~litestar.static_files.create_static_files_router`: .. literalinclude:: /examples/static_files/passing_options.py :language: python :caption: Passing options to the router generated by :func:`create_static_files_router` Using a custom router class --------------------------- The router class used can be customized with the :paramref:`~.static_files.create_static_files_router.params.router_class` parameter: .. literalinclude:: /examples/static_files/custom_router.py :language: python :caption: Using a custom router class with :func:`create_static_files_router` Retrieving paths to static files -------------------------------- :meth:`~litestar.app.Litestar.route_reverse` and :meth:`~litestar.connection.ASGIConnection.url_for` can be used to retrieve the path under which a specific file will be available: .. literalinclude:: /examples/static_files/route_reverse.py :language: python :caption: Retrieving paths to static files using :meth:`~.app.Litestar.route_reverse` .. tip:: The ``name`` parameter has to match the ``name`` parameter passed to :func:`create_static_files_router`, which defaults to ``static``. (Remote) file systems --------------------- To customize how Litestar interacts with the file system, a class implementing the :class:`~litestar.types.FileSystemProtocol` can be passed to ``file_system``. An example of this are the file systems provided by `fsspec `_, which includes support for FTP, SFTP, Hadoop, SMB, GitHub and `many more `_, with support for popular cloud providers available via 3rd party implementations such as - S3 via `S3FS `_ - Google Cloud Storage via `GCFS `_ - Azure Blob Storage via `adlfs `_ .. literalinclude:: /examples/static_files/file_system.py :language: python :caption: Using a custom file system with :func:`create_static_files_router` Upgrading from legacy StaticFilesConfig --------------------------------------- .. deprecated:: v3.0 :class:`StaticFilesConfig` is deprecated and will be removed in Litestar 3.0 Existing code can be upgraded to :func:`create_static_files_router` by replacing :class:`StaticFilesConfig` instances with this function call and passing the result to ``route_handlers`` instead of ``static_files_config``: .. literalinclude:: /examples/static_files/upgrade_from_static_1.py :language: python :caption: Using the deprecated :class:`~.static_files.config.StaticFilesConfig` .. literalinclude:: /examples/static_files/upgrade_from_static_2.py :language: python :caption: Upgrading from :class:`~.static_files.config.StaticFilesConfig` to :func:`create_static_files_router` litestar-2.16.0/docs/usage/stores.rst000066400000000000000000000307371500564371300175520ustar00rootroot00000000000000:tocdepth: 4 Stores ====== .. py:currentmodule:: litestar.stores When developing applications, oftentimes a simply storage mechanism is needed, for example when :doc:`caching response data` or storing data for :ref:`server-side sessions `. In cases like these a traditional database is often not needed, and a simple key/value store suffices. Litestar provides several low level key value stores, offering an asynchronous interface to store data in a thread- and process-safe manner. These stores are centrally managed via a :class:`registry `, allowing easy access throughout the whole application and third party integration (for example plugins). Built-in stores --------------- :class:`MemoryStore ` A simple in-memory store, using a dictionary to hold data. This store offers no persistence and is not thread or multiprocess safe, but it is suitable for basic applications such as caching and has generally the lowest overhead. This is the default store used by Litestar internally. If you plan to enable :doc:`multiple web workers ` and you need inter-process communication across multiple worker processes, you should use one of the other non-memory stores instead. :class:`FileStore ` A store that saves data as files on disk. Persistence is built in, and data is easy to extract and back up. It is slower compared to in-memory solutions, and primarily suitable for situations when larger amounts of data need to be stored, is particularly long-lived, or persistence has a very high importance. Offers `namespacing`_. :class:`RedisStore ` A store backend by `redis `_. It offers all the guarantees and features of Redis, making it suitable for almost all applications. Offers `namespacing`_. :class:`ValkeyStore ` A store backed by `valkey `_, a fork of Redis created as the result of Redis' license changes. Similarly to the RedisStore, it is suitable for almost all applications and supports `namespacing`_. At the time of writing, :class:`Valkey ` is equivalent to :class:`redis.asyncio.Redis`, and all notes pertaining to Redis also apply to Valkey. .. admonition:: Why not memcached? :class: info Memcached is not a supported backend, and will likely also not be added in the future. The reason for this is simply that it's hard to support memcached properly, since it's missing a lot of basic functionality like checking a key's expiry time, or something like Redis' `SCAN `_ command, which allows to implement pattern-based deletion of keys. Interacting with a store ------------------------ The most fundamental operations of a store are: - :meth:`get <.base.Store.get>`: To retrieve a stored value - :meth:`set <.base.Store.set>`: To set a value in the store - :meth:`delete <.base.Store.delete>`: To delete a stored value Getting and setting values ++++++++++++++++++++++++++ .. literalinclude:: /examples/stores/get_set.py :language: python Setting an expiry time ++++++++++++++++++++++ The :meth:`set <.base.Store.set>` method has an optional parameter ``expires_in``, allowing to specify a time after which a stored value should expire. .. literalinclude:: /examples/stores/expiry.py :language: python .. note:: It is up to the individual store to decide how to handle expired values, and implementations may differ. The :class:`redis based store <.redis.RedisStore>` for example uses Redis' native expiry mechanism to handle this, while the :class:`FileStore <.file.FileStore>` only deletes expired values when they're trying to be accessed, or explicitly deleted via the :meth:`delete_expired <.file.FileStore.delete_expired>` method. It is also possible to extend the expiry time on each access, which is useful for applications such as server side sessions or LRU caches: .. literalinclude:: /examples/stores/expiry_renew_on_get.py :language: python Deleting expired values ####################### When using a :class:`MemoryStore <.memory.MemoryStore>` or :class:`FileStore <.file.FileStore>`, expired data won't be deleted automatically. Instead, it will only happen when the data is being accessed, or if this process is invoked explicitly via :meth:`MemoryStore.delete_expired <.memory.MemoryStore.delete_expired>` or :meth:`FileStore.delete_expired <.file.FileStore.delete_expired>` respectively. It's a good practice to call ``delete_expired`` periodically, to ensure the size of the stored values does not grow indefinitely. In this example, an :ref:`after_response ` handler is used to delete expired items at most every 30 second: .. literalinclude:: /examples/stores/delete_expired_after_response.py :language: python When using the :class:`FileStore <.file.FileStore>`, expired items may also be deleted on startup: .. literalinclude:: /examples/stores/delete_expired_on_startup.py :language: python .. note:: For the :class:`MemoryStore <.memory.MemoryStore>`, this is not needed as the data is simply stored in a dictionary. This means that every time a new instance of this store is created, it will start out empty. What can be stored ++++++++++++++++++ Stores generally operate on :class:`bytes`; They accept bytes to store, and will return bytes. For convenience, the :meth:`set <.base.Store.set>` method also allows to pass in strings, which will be UTF-8 encoded before being stored. This means that :meth:`get <.base.Store.get>` will return bytes even when a string has been passed to :meth:`set <.base.Store.set>`. The reason for this limitation is simple: Different backends used to store the data offer vastly different encoding, storage, and (de)serialization capacities. Since stores are designed to be interchangeable, this means settling for a common denominator, a type that all backends will support. :class:`bytes` meet these requirements and make it possible to store a very wide variety of data. .. admonition:: Technical details :class:`MemoryStore <.memory.MemoryStore>` differs from this, because it does not do any encoding before storing the value. This means that it's technically possible to store arbitrary objects in this store, and get the same object back. However, this is not reflected in the store's typing, as the underlying :class:`Store <.base.Store>` interface does not guarantee this behaviour, and it is not guaranteed that :class:`MemoryStore <.memory.MemoryStore>` will always behave in this case. Namespacing +++++++++++ When stores are being used for more than one purpose, some extra bookkeeping is required to safely perform bulk operations such as :class:`delete_all <.base.Store.delete_all>`. If for example a :class:`RedisStore <.redis.RedisStore>` was used, simply issuing a `FLUSHALL `_ command might have unforeseen consequences. To help with this, some stores offer namespacing capabilities, allowing to build a simple hierarchy of stores. These come with the additional :meth:`with_namespace <.base.NamespacedStore.with_namespace>` method, which returns a new :class:`NamespacedStore <.base.NamespacedStore>` instance. Once a namespaced store is created, operations on it will only affect itself and its child namespaces. When using the :class:`RedisStore <.redis.RedisStore>`, this allows to reuse the same underlying :class:`Redis ` instance and connection, while ensuring isolation. .. note:: :class:`RedisStore <.redis.RedisStore>` uses the ``LITESTAR`` namespace by default; all keys created by this store, will use the ``LITESTAR`` prefix when storing data in redis. :meth:`RedisStore.delete_all <.redis.RedisStore.delete_all>` is implemented in such a way that it will only delete keys matching the current namespace, making it safe and side-effect free. This can be turned off by explicitly passing ``namespace=None`` to the store when creating a new instance. .. literalinclude:: /examples/stores/namespacing.py :language: python Even though all three stores defined here use the same Redis instance, calling ``delete_all`` on the ``cache_store`` will not affect data within the ``session_store``. Defining stores hierarchically like this still allows to easily clear everything, by simply calling :meth:`delete_all <.base.Store.delete_all>` on the root store. Managing stores with the registry --------------------------------- The :class:`StoreRegistry ` is a central place through which stores can be configured and managed, and can help to easily access stores set up and used by other parts of the application, Litestar internals or third party integrations. It is available throughout the whole application context via the :class:`Litestar.stores ` attribute. It operates on a few basic principles: - An initial mapping of stores can be provided to the registry - Registered stores can be requested with :meth:`get <.registry.StoreRegistry.get>` - If a store has been requested that has not been registered yet, a store of that name will be created and registered using the `the default factory`_ .. literalinclude:: /examples/stores/registry.py :language: python This pattern offers isolation of stores, and an easy way to configure stores used by middlewares and other Litestar features or third party integrations. In the following example, the store set up by the :class:`RateLimitMiddleware ` is accessed via the registry: .. literalinclude:: /examples/stores/registry_access_integration.py :language: python This works because :class:`RateLimitMiddleware ` will request its store internally via ``app.stores.get`` as well. The default factory +++++++++++++++++++ The pattern above is made possible by using the registry's default factory; A callable that gets invoked every time a store is requested that hasn't been registered yet. It's similar to the ``default`` argument to :meth:`dict.get`. By default, the default factory is a function that returns a new :class:`MemoryStore ` instance. This behaviour can be changed by supplying a custom ``default_factory`` method to the registry. To make use of this, a registry instance can be passed directly to the application: .. literalinclude:: /examples/stores/registry_default_factory.py :language: python The registry will now return the same :class:`MemoryStore ` every time an undefined store is being requested. Using the registry to configure integrations ++++++++++++++++++++++++++++++++++++++++++++ This mechanism also allows to control the stores used by various integrations, such as middlewares: .. literalinclude:: /examples/stores/registry_configure_integrations.py :language: python In this example, the registry is being set up with stores using the ``sessions`` and ``response_cache`` keys. These are not magic constants, but instead configuration values that can be changed. Those names just happen to be their default values. Adjusting those default values allows to easily reuse stores, without the need for a more complex setup: .. literalinclude:: /examples/stores/configure_integrations_set_names.py :language: python Now the rate limit middleware and response caching will use the ``redis`` store, while sessions will be store in the ``file`` store. Setting up the default factory with namespacing +++++++++++++++++++++++++++++++++++++++++++++++ The default factory can be used in conjunction with `namespacing`_ to create isolated, hierarchically organized stores, with minimal boilerplate: .. literalinclude:: /examples/stores/registry_default_factory_namespacing.py :language: python Without any extra configuration, every call to ``app.stores.get`` with a unique name will return a namespace for this name only, while re-using the underlying Redis instance. Store lifetime ++++++++++++++ Stores may not be automatically closed when the application is shut down. This is the case in particular for the RedisStore if you are not using the class method :meth:`RedisStore.with_client <.redis.RedisStore.with_client>` and passing in your own Redis instance. In this case you're responsible to close the Redis instance yourself. litestar-2.16.0/docs/usage/templating.rst000066400000000000000000000341501500564371300203700ustar00rootroot00000000000000Templating ========== Litestar has built-in support for `Jinja2 `_ , `Mako `_ and `Minijinja `_ template engines, as well as abstractions to make use of any template engine you wish. Template engines ---------------- To stay lightweight, a Litestar installation does not include the *Jinja*, *Mako* or *Minijinja* libraries themselves. Before you can start using them, you have to install it via the respective extra: .. tab-set:: .. tab-item:: Jinja :sync: jinja .. code-block:: shell pip install litestar[jinja] .. tab-item:: Mako :sync: mako .. code-block:: shell pip install litestar[mako] .. tab-item:: MiniJinja :sync: minijinja .. code-block:: shell pip install litestar[minijinja] .. tip:: *Jinja* is included in the ``standard`` extra. If you installed Litestar using ``litestar[standard]``, you do not need to explicitly add the ``jinja`` extra. Registering a template engine ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To register one of the built-in template engines you simply need to pass it to the Litestar constructor: .. tab-set:: .. tab-item:: Jinja :sync: jinja .. literalinclude:: /examples/templating/template_engine_jinja.py :language: python .. tab-item:: Mako :sync: mako .. literalinclude:: /examples/templating/template_engine_mako.py :language: python .. tab-item:: MiniJinja :sync: minijinja .. literalinclude:: /examples/templating/template_engine_minijinja.py :language: python .. note:: The ``directory`` parameter passed to :class:`TemplateConfig ` can be either a directory or list of directories to use for loading templates. Registering a Custom Template Engine ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The above example will create a jinja Environment instance, but you can also pass in your own instance. .. code-block:: python from litestar import Litestar from litestar.contrib.jinja import JinjaTemplateEngine from litestar.template import TemplateConfig from jinja2 import Environment, DictLoader my_custom_env = Environment(loader=DictLoader({"index.html": "Hello {{name}}!"})) app = Litestar( template_config=TemplateConfig( instance=JinjaTemplateEngine.from_environment(my_custom_env) ) ) .. note:: The ``instance`` parameter passed to :class:`TemplateConfig ` can not be used in conjunction with the ``directory`` parameter, if you choose to use instance you're fully responsible on the engine creation. Defining a custom template engine ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you wish to use another templating engine, you can easily do so by implementing :class:`TemplateEngineProtocol `. This class accepts a generic argument which should be the template class, and it specifies two methods: .. code-block:: python from typing import Protocol, Union, List from pydantic import DirectoryPath # the template class of the respective library from some_lib import SomeTemplate class TemplateEngineProtocol(Protocol[SomeTemplate]): def __init__(self, directory: Union[DirectoryPath, List[DirectoryPath]]) -> None: """Builds a template engine.""" ... def get_template(self, template_name: str) -> SomeTemplate: """Loads the template with template_name and returns it.""" ... Once you have your custom engine you can register it as you would the built-in engines. Accessing the template engine instance ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you need to access the template engine instance, you can do so via the :class:`TemplateConfig.engine ` attribute: .. tab-set:: .. tab-item:: Jinja :sync: jinja .. literalinclude:: /examples/templating/engine_instance_jinja.py :language: python .. tab-item:: Mako :sync: mako .. literalinclude:: /examples/templating/engine_instance_mako.py :language: python .. tab-item:: MiniJinja :sync: minijinja .. literalinclude:: /examples/templating/engine_instance_minijinja.py :language: python Template responses ------------------ Once you have a template engine registered you can return :class:`templates responses <.response.Template>` from your route handlers: .. tab-set:: .. tab-item:: Jinja :sync: jinja .. literalinclude:: /examples/templating/returning_templates_jinja.py :language: python .. tab-item:: Mako :sync: mako .. literalinclude:: /examples/templating/returning_templates_mako.py :language: python .. tab-item:: MiniJinja :sync: minijinja .. literalinclude:: /examples/templating/returning_templates_minijinja.py :language: python * ``name`` is the name of the template file within on of the specified directories. If no file with that name is found, a :class:`TemplateNotFoundException <.exceptions.TemplateNotFoundException>` exception will be raised. * ``context`` is a dictionary containing arbitrary data that will be passed to the template engine's ``render`` method. For Jinja and Mako, this data will be available in the `template context <#template-context>`_ Template Files vs. Strings -------------------------- When you define a template response, you can either pass a template file name or a string containing the template. The latter is useful if you want to define the template inline for small templates or :doc:`HTMX ` responses for example. .. tab-set:: .. tab-item:: File name .. code-block:: python :caption: Template via file @get() async def example() -> Template: return Template(template_name="test.html", context={"hello": "world"}) .. tab-item:: String .. code-block:: python :caption: Template via string @get() async def example() -> Template: template_string = "{{ hello }}" return Template(template_str=template_string, context={"hello": "world"}) Template context ---------------- Both `Jinja2 `_ and `Mako `_ support passing a context object to the template as well as defining callables that will be available inside the template. Accessing the request instance ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The current :class:`Request ` is available within the template context under ``request``, which also provides access to the :doc:`app instance `. Accessing ``app.state.key`` for example would look like this: check_context_key: {{ check_context_key() }} .. tab-set:: .. tab-item:: Jinja :sync: jinja .. code-block:: html
My state value: {{request.app.state.some_key}}
.. tab-item:: Mako :sync: mako .. code-block:: html html
My state value: ${request.app.state.some_key}
.. tab-item:: MiniJinja :sync: minijinja .. code-block:: html
My state value: {{request.app.state.some_key}}
Adding CSRF inputs ^^^^^^^^^^^^^^^^^^ If you want to add a hidden ```` tag containing a `CSRF token `_, you first need to configure :ref:`CSRF protection `. With that in place, you can now insert the CSRF input field inside an HTML form: .. tab-set:: .. tab-item:: Jinja :sync: jinja .. code-block:: html
{{ csrf_input | safe }}

.. tab-item:: Mako :sync: mako .. code-block:: html
${csrf_input | n}

.. tab-item:: MiniJinja :sync: minijinja .. code-block:: html
{{ csrf_input | safe}}

The input holds a CSRF token as its value and is hidden so users cannot see or interact with it. The token is sent back to the server when the form is submitted, and is checked by the CSRF middleware. .. note:: The `csrf_input` must be marked as safe in order to ensure that it does not get escaped. Passing template context ^^^^^^^^^^^^^^^^^^^^^^^^ Passing context to the template is very simple - its one of the kwargs expected by the :class:`Template ` container, so simply pass a string keyed dictionary of values: .. code-block:: python from litestar import get from litestar.response import Template @get(path="/info") def info() -> Template: return Template(template_name="info.html", context={"numbers": "1234567890"}) Template callables ------------------ Both `Jinja2 `_ and `Mako `_ allow users to define custom callables that are ran inside the template. Litestar builds on this and offers some functions out of the box. Built-in callables ^^^^^^^^^^^^^^^^^^ ``url_for`` To access urls for route handlers you can use the ``url_for`` function. Its signature and behaviour matches :meth:`route_reverse ` behaviour. More details about route handler indexing can be found :ref:`here `. ``csrf_token`` This function returns the request's unique :ref:`CSRF token ` You can use this if you wish to insert the ``csrf_token`` into non-HTML based templates, or insert it into HTML templates not using a hidden input field but by some other means, for example inside a special ```` tag. ``url_for_static_asset`` URLs for static files can be created using the ``url_for_static_asset`` function. It's signature and behaviour are identical to :meth:`app.url_for_static_asset `. Registering template callables ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The :class:`TemplateEngineProtocol ` specifies the method ``register_template_callable`` that allows defining a custom callable on a template engine. This method is implemented for the two built in engines, and it can be used to register callables that will be injected into the template. The callable should expect one argument - the context dictionary. It can be any callable - a function, method, or class that defines the call method. For example: .. tab-set:: .. tab-item:: Jinja :sync: jinja .. literalinclude:: /examples/templating/template_functions_jinja.py :caption: ``template_functions.py`` :language: python .. literalinclude:: /examples/templating/templates/index.html.jinja2 :language: html :caption: ``templates/index.html.jinja2`` .. tab-item:: Mako :sync: mako .. literalinclude:: /examples/templating/template_functions_mako.py :caption: ``template_functions.py`` :language: python .. literalinclude:: /examples/templating/templates/index.html.mako :language: html :caption: ``templates/index.html.mako`` .. tab-item:: Minijinja :sync: minijinja .. literalinclude:: /examples/templating/template_functions_minijinja.py :caption: ``template_functions.py`` :language: python .. literalinclude:: /examples/templating/templates/index.html.minijinja :language: html :caption: ``templates/index.html.minijinja`` Run the example with ``uvicorn template_functions:app`` , visit http://127.0.0.1:8000, and you'll see .. image:: /images/examples/template_engine_callable.png :alt: Template engine callable example litestar-2.16.0/docs/usage/testing.rst000066400000000000000000000363031500564371300177030ustar00rootroot00000000000000Testing ======= Testing is a first class citizen in Litestar, which offers several powerful testing utilities out of the box. Test Client ----------- Litestar's test client is built on top of the `httpx `_ library. To use the test client you should pass to it an instance of Litestar as the ``app`` kwarg. Let's say we have a very simple app with a health check endpoint: .. code-block:: python :caption: ``my_app/main.py`` from litestar import Litestar, MediaType, get @get(path="/health-check", media_type=MediaType.TEXT) def health_check() -> str: return "healthy" app = Litestar(route_handlers=[health_check]) We would then test it using the test client like so: .. tab-set:: .. tab-item:: Sync :sync: sync .. code-block:: python :caption: ``tests/test_health_check.py`` from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient from my_app.main import app app.debug = True def test_health_check(): with TestClient(app=app) as client: response = client.get("/health-check") assert response.status_code == HTTP_200_OK assert response.text == "healthy" .. tab-item:: Async :sync: async .. code-block:: python :caption: ``tests/test_health_check.py`` from litestar.status_codes import HTTP_200_OK from litestar.testing import AsyncTestClient from my_app.main import app app.debug = True async def test_health_check(): async with AsyncTestClient(app=app) as client: response = await client.get("/health-check") assert response.status_code == HTTP_200_OK assert response.text == "healthy" Since we would probably need to use the client in multiple places, it's better to make it into a pytest fixture: .. tab-set:: .. tab-item:: Sync :sync: sync .. code-block:: python :caption: ``tests/conftest.py`` from typing import TYPE_CHECKING, Iterator import pytest from litestar.testing import TestClient from my_app.main import app if TYPE_CHECKING: from litestar import Litestar app.debug = True @pytest.fixture(scope="function") def test_client() -> Iterator[TestClient[Litestar]]: with TestClient(app=app) as client: yield client .. tab-item:: Async :sync: async .. code-block:: python :caption: ``tests/conftest.py`` from typing import TYPE_CHECKING, AsyncIterator import pytest from litestar.testing import AsyncTestClient from my_app.main import app if TYPE_CHECKING: from litestar import Litestar app.debug = True @pytest.fixture(scope="function") async def test_client() -> AsyncIterator[AsyncTestClient[Litestar]]: async with AsyncTestClient(app=app) as client: yield client We would then be able to rewrite our test like so: .. tab-set:: .. tab-item:: Sync :sync: sync .. literalinclude:: /examples/testing/test_health_check_sync.py :caption: ``tests/test_health_check.py`` :language: python .. tab-item:: Async :sync: async .. literalinclude:: /examples/testing/test_health_check_async.py :caption: ``tests/test_health_check.py`` :language: python Testing websockets ++++++++++++++++++ Litestar's test client enhances the httpx client to support websockets. To test a websocket endpoint, you can use the :meth:`websocket_connect ` method on the test client. The method returns a websocket connection object that you can use to send and receive messages, see an example below for json: For more information, see also the :class:`WebSocket ` class in the API documentation and the :ref:`websocket ` documentation. .. literalinclude:: /examples/testing/test_websocket.py :language: python Using sessions ++++++++++++++ If you are using :ref:`session middleware ` for session persistence across requests, then you might want to inject or inspect session data outside a request. For this, :class:`TestClient <.testing.TestClient>` provides two methods: * :meth:`set_session_data ` * :meth:`get_session_data ` .. attention:: - The Session Middleware must be enabled in Litestar app provided to the TestClient to use sessions. - If you are using the :class:`ClientSideSessionBackend ` you need to install the ``cryptography`` package. You can do so by installing ``litestar``: .. tab-set:: .. tab-item:: pip .. code-block:: bash :caption: Using pip python3 -m pip install litestar[cryptography] .. tab-item:: pipx .. code-block:: bash :caption: Using `pipx `_ pipx install litestar[cryptography] .. tab-item:: pdm .. code-block:: bash :caption: Using `PDM `_ pdm add litestar[cryptography] .. tab-item:: Poetry .. code-block:: bash :caption: Using `Poetry `_ poetry add litestar[cryptography] .. tab-set:: .. tab-item:: Sync :sync: sync .. literalinclude:: /examples/testing/test_set_session_data.py :caption: Setting session data :language: python .. literalinclude:: /examples/testing/test_get_session_data.py :caption: Getting session data :language: python .. tab-item:: Async :sync: async .. literalinclude:: /examples/testing/test_set_session_data_async.py :caption: Setting session data :language: python .. literalinclude:: /examples/testing/test_get_session_data_async.py :caption: Getting session data :language: python Using a blocking portal +++++++++++++++++++++++ The :class:`TestClient <.testing.TestClient>` uses a feature of `anyio `_ called a **Blocking Portal**. The :class:`anyio.abc.BlockingPortal` allows :class:`TestClient <.testing.TestClient>` to execute asynchronous functions using a synchronous call. ``TestClient`` creates a blocking portal to manage ``Litestar``'s async logic, and it allows ``TestClient``'s API to remain fully synchronous. Any tests that are using an instance of ``TestClient`` can also make use of the blocking portal to execute asynchronous functions without the test itself being asynchronous. .. literalinclude:: /examples/testing/test_with_portal.py :caption: Using a blocking portal :language: python Creating a test app ------------------- Litestar also offers a helper function called :func:`create_test_client ` which first creates an instance of Litestar and then a test client using it. There are multiple use cases for this helper - when you need to check generic logic that is decoupled from a specific Litestar app, or when you want to test endpoints in isolation. You can pass to this helper all the kwargs accepted by the litestar constructor, with the ``route_handlers`` kwarg being **required**. Yet unlike the Litestar app, which expects ``route_handlers`` to be a list, here you can also pass individual values. For example, you can do this: .. code-block:: python :caption: ``my_app/tests/test_health_check.py`` from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from my_app.main import health_check def test_health_check(): with create_test_client(route_handlers=[health_check]) as client: response = client.get("/health-check") assert response.status_code == HTTP_200_OK assert response.text == "healthy" But also this: .. code-block:: python :caption: ``my_app/tests/test_health_check.py`` from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from my_app.main import health_check def test_health_check(): with create_test_client(route_handlers=health_check) as client: response = client.get("/health-check") assert response.status_code == HTTP_200_OK assert response.text == "healthy" Running a live server --------------------- The test clients make use of HTTPX's ability to directly call into an ASGI app, without having to run an actual server. In most cases this is sufficient but there are some exceptions where this won't work, due to the limitations of the emulated client-server communication. For example, when using server-sent events with an infinite generator, it will lock up the test client, since HTTPX tries to consume the full response before returning a request. Litestar offers two helper functions, :func:`litestar.testing.subprocess_sync_client` and :func:`litestar.testing.subprocess_async_client` that will launch a Litestar instance with in a subprocess and set up an httpx client for running tests. You can either load your actual app file or create subsets from it as you would with the regular test client setup: .. literalinclude:: /examples/testing/subprocess_sse_app.py :language: python .. literalinclude:: /examples/testing/test_subprocess_sse.py :language: python RequestFactory -------------- Another helper is the :class:`RequestFactory ` class, which creates instances of :class:`litestar.connection.request.Request `. The use case for this helper is when you need to test logic that expects to receive a request object. For example, lets say we wanted to unit test a *guard* function in isolation, to which end we'll reuse the examples from the :doc:`route guards ` documentation: .. code-block:: python :caption: ``my_app/guards.py`` from litestar import Request from litestar.exceptions import NotAuthorizedException from litestar.handlers.base import BaseRouteHandler def secret_token_guard(request: Request, route_handler: BaseRouteHandler) -> None: if ( route_handler.opt.get("secret") and not request.headers.get("Secret-Header", "") == route_handler.opt["secret"] ): raise NotAuthorizedException() We already have our route handler in place: .. code-block:: python :caption: ``my_app/secret.py`` from os import environ from litestar import get from my_app.guards import secret_token_guard @get(path="/secret", guards=[secret_token_guard], opt={"secret": environ.get("SECRET")}) def secret_endpoint() -> None: ... We could thus test the guard function like so: .. code-block:: python :caption: ``tests/guards/test_secret_token_guard.py`` import pytest from litestar.exceptions import NotAuthorizedException from litestar.testing import RequestFactory from my_app.guards import secret_token_guard from my_app.secret import secret_endpoint request = RequestFactory().get("/") def test_secret_token_guard_failure_scenario(): copied_endpoint_handler = secret_endpoint.copy() copied_endpoint_handler.opt["secret"] = None with pytest.raises(NotAuthorizedException): secret_token_guard(request=request, route_handler=copied_endpoint_handler) def test_secret_token_guard_success_scenario(): copied_endpoint_handler = secret_endpoint.copy() copied_endpoint_handler.opt["secret"] = "super-secret" secret_token_guard(request=request, route_handler=copied_endpoint_handler) Using polyfactory ------------------------ `Polyfactory `__ offers an easy and powerful way to generate mock data from pydantic models and dataclasses. Let's say we have an API that talks to an external service and retrieves some data: .. code-block:: python :caption: ``main.py`` from typing import Protocol, runtime_checkable from polyfactory.factories.pydantic import BaseModel from litestar import get class Item(BaseModel): name: str @runtime_checkable class Service(Protocol): def get(self) -> Item: ... @get(path="/item") def get_item(service: Service) -> Item: return service.get() We could test the ``/item`` route like so: .. code-block:: python :caption: ``tests/conftest.py`` import pytest from litestar.di import Provide from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from my_app.main import Service, Item, get_item @pytest.fixture() def item(): return Item(name="Chair") def test_get_item(item: Item): class MyService(Service): def get_one(self) -> Item: return item with create_test_client( route_handlers=get_item, dependencies={"service": Provide(lambda: MyService())} ) as client: response = client.get("/item") assert response.status_code == HTTP_200_OK assert response.json() == item.dict() While we can define the test data manually, as is done in the above, this can be quite cumbersome. That's where `polyfactory `_ library comes in. It generates mock data for pydantic models and dataclasses based on type annotations. With it, we could rewrite the above example like so: .. code-block:: python :caption: ``main.py`` from typing import Protocol, runtime_checkable import pytest from pydantic import BaseModel from polyfactory.factories.pydantic_factory import ModelFactory from litestar.status_codes import HTTP_200_OK from litestar import get from litestar.di import Provide from litestar.testing import create_test_client class Item(BaseModel): name: str @runtime_checkable class Service(Protocol): def get_one(self) -> Item: ... @get(path="/item") def get_item(service: Service) -> Item: return service.get_one() class ItemFactory(ModelFactory[Item]): model = Item @pytest.fixture() def item(): return ItemFactory.build() def test_get_item(item: Item): class MyService(Service): def get_one(self) -> Item: return item with create_test_client( route_handlers=get_item, dependencies={"service": Provide(lambda: MyService())} ) as client: response = client.get("/item") assert response.status_code == HTTP_200_OK assert response.json() == item.dict() litestar-2.16.0/docs/usage/websockets.rst000066400000000000000000000325361500564371300204030ustar00rootroot00000000000000WebSockets ========== There are three ways to handle WebSockets in Litestar: 1. The low-level :class:`~litestar.handlers.websocket` route handler, providing basic abstractions over the ASGI WebSocket interface 2. :class:`~litestar.handlers.websocket_listener` and :class:`~litestar.handlers.WebsocketListener`\ : Reactive, event-driven WebSockets with full serialization and DTO support and support for a synchronous interface 3. :class:`~litestar.handlers.websocket_stream`: Proactive, stream oriented WebSockets with full serialization and DTO support 4. :func:`~litestar.handlers.send_websocket_stream`: Proactive, stream oriented WebSockets The main difference between the low and high level interfaces is that, dealing with low level interface requires, setting up a loop and listening for incoming data, handling exceptions, client disconnects, and parsing incoming and serializing outgoing data. WebSocket Listeners -------------------- WebSocket Listeners can be used to interact with a WebSocket in an event-driven manner, using a callback style interface. They treat a WebSocket handler like any other route handler: A callable that takes in incoming data in an already pre-processed form and returns data to be serialized and sent over the connection. The low level details will be handled behind the curtains. .. code-block:: python from litestar import Litestar from litestar.handlers.websocket_handlers import websocket_listener @websocket_listener("/") async def handler(data: str) -> str: return data app = Litestar([handler]) This handler will accept connections on ``/``, and wait to receive data. Once a message has been received, it will be passed into the handler function defined, via the ``data`` parameter. This works like a regular route handler, so it's possible to specify the type of data which should be received, and it will be converted accordingly. .. note:: Contrary to WebSocket route handlers, functions decorated with :class:`websocket_listener <.handlers.websocket_listener>` don't have to be asynchronous. Receiving data ++++++++++++++ Data can be received in the listener via the ``data`` parameter. The data passed to this will be converted / parsed according to the given type annotation and supports :class:`str`, :class:`bytes`, or arbitrary :class:`dict` / or :class:`list` in the form of JSON. .. important:: The listeners will default to JSON unless `data` is annotated with `str` or `bytes` .. tab-set:: .. tab-item:: JSON .. literalinclude:: /examples/websockets/receive_json.py :language: python .. tab-item:: Text .. literalinclude:: /examples/websockets/receive_str.py :language: python .. tab-item:: Bytes .. literalinclude:: /examples/websockets/receive_bytes.py :language: python .. important:: Contrary to route handlers, JSON data will only be parsed but not validated. This is a limitation of the current implementation and will change in future versions. Sending data +++++++++++++ Sending data is done by simply returning the value to be sent from the handler function. Similar to receiving data, type annotations configure how the data is being handled. Values that are not :class:`str` or :class:`bytes` are assumed to be JSON encodable and will be serialized accordingly before being sent. This serialization is available for all data types currently supported by Litestar ( :doc:`dataclasses `\ , :class:`TypedDict `, :class:`NamedTuple `, :class:`msgspec.Struct`, etc.), including DTOs. .. tab-set:: .. tab-item:: Text .. literalinclude:: /examples/websockets/sending_str.py :language: python .. tab-item:: Bytes .. literalinclude:: /examples/websockets/sending_bytes.py :language: python .. tab-item:: Dict as JSON .. literalinclude:: /examples/websockets/sending_json_dict.py :language: python .. tab-item:: Dataclass as JSON .. literalinclude:: /examples/websockets/sending_json_dataclass.py :language: python Setting transport modes +++++++++++++++++++++++ Receive mode ~~~~~~~~~~~~ .. tab-set:: .. tab-item:: Text mode ``text`` is the default mode and is appropriate for most messages, including structured data such as JSON. .. literalinclude:: /examples/websockets/mode_receive_text.py :language: python .. tab-item:: Binary mode .. literalinclude:: /examples/websockets/mode_receive_binary.py :language: python .. important:: Once configured with a mode, a listener will only listen to socket events of the appropriate type. This means if a listener is configured to use ``binary`` mode, it will not respond to WebSocket events sending data in the text channel. Send mode ~~~~~~~~~ .. tab-set:: .. tab-item:: Text mode ``text`` is the default mode and is appropriate for most messages, including structured data such as JSON. .. literalinclude:: /examples/websockets/mode_send_text.py :language: python .. tab-item:: Binary mode .. literalinclude:: /examples/websockets/mode_send_binary.py :language: python Dependency injection ++++++++++++++++++++ :doc:`dependency-injection` is available and generally works the same as in regular route handlers: .. literalinclude:: /examples/websockets/dependency_injection_simple.py :language: python .. important:: Injected dependencies work on the level of the underlying **route handler**. This means they won't be re-evaluated every time the listener function is called. The following example makes use of :ref:`yield dependencies ` and the fact that dependencies are only evaluated once for every connection; The step after the ``yield`` will only be executed after the connection has been closed. .. literalinclude:: /examples/websockets/dependency_injection_yield.py :language: python Interacting with the WebSocket directly +++++++++++++++++++++++++++++++++++++++ Sometimes access to the socket instance is needed, in which case the :class:`WebSocket <.connection.WebSocket>` instance can be injected into the handler function via the ``socket`` argument: .. literalinclude:: /examples/websockets/socket_access.py :language: python .. important:: Since WebSockets are inherently asynchronous, to interact with the asynchronous methods on :class:`WebSocket <.connection.WebSocket>`, the handler function needs to be asynchronous. Customising connection acceptance +++++++++++++++++++++++++++++++++ By default, Litestar will accept all incoming connections by awaiting ``WebSocket.accept()`` without arguments. This behavior can be customized by passing a custom ``connection_accept_handler`` function. Litestar will await this function to accept the connection. .. literalinclude:: /examples/websockets/setting_custom_connection_headers.py :language: python Class based WebSocket handling ++++++++++++++++++++++++++++++ In addition to using a simple function as in the examples above, a class based approach is made possible by extending the :class:`WebSocketListener <.handlers.WebsocketListener>`. This provides convenient access to socket events such as connect and disconnect, and can be used to encapsulate more complex logic. .. tab-set:: .. tab-item:: Sync .. literalinclude:: /examples/websockets/listener_class_based.py :language: python .. tab-item:: Async .. literalinclude:: /examples/websockets/listener_class_based_async.py :language: python Custom WebSocket ++++++++++++++++ .. versionadded:: 2.7.0 Litestar supports custom ``websocket_class`` instances, which can be used to further configure the default :class:`WebSocket`. The example below illustrates how to implement a custom WebSocket class for the whole application. .. dropdown:: Example of a custom websocket at the application level .. literalinclude:: /examples/websockets/custom_websocket.py :language: python .. admonition:: Layered architecture WebSocket classes are part of Litestar's layered architecture, which means you can set a WebSocket class on every layer of the application. If you have set a WebSocket class on multiple layers, the layer closest to the route handler will take precedence. You can read more about this in the :ref:`usage/applications:layered architecture` section WebSocket Streams ----------------- WebSocket streams can be used to proactively push data to a client, using an asynchronous generator function. Data will be sent via the socket every time the generator ``yield``\ s, until it is either exhausted or the client disconnects. .. literalinclude:: /examples/websockets/stream_basic.py :language: python :caption: Streaming the current time in 0.5 second intervals Serialization +++++++++++++ Just like with route handlers, type annotations configure how the data is being handled. :class:`str` or :class:`bytes` will be sent as-is, while everything else will be encoded as JSON before being sent. This serialization is available for all data types currently supported by Litestar (:doc:`dataclasses `, :class:`TypedDict `, :class:`NamedTuple `, :class:`msgspec.Struct`, etc.), including DTOs. Dependency Injection ++++++++++++++++++++ Dependency injection is available and works analogous to regular route handlers. .. important:: One thing to keep in mind, especially for long-lived streams, is that dependencies are scoped to the lifetime of the handler. This means that if for example a database connection is acquired in a dependency, it will be held until the generator stops. This may not be desirable in all cases, and acquiring resources ad-hoc inside the generator itself preferable .. literalinclude:: /examples/websockets/stream_di_hog.py :language: python :caption: Bad: The lock will be held until the client disconnects .. literalinclude:: /examples/websockets/stream_di_hog_fix.py :language: python :caption: Good: The lock will only be acquired when it's needed Interacting with the WebSocket directly +++++++++++++++++++++++++++++++++++++++ To interact with the :class:`WebSocket <.connection.WebSocket>` directly, it can be injected into the generator function via the ``socket`` argument: .. literalinclude:: /examples/websockets/stream_socket_access.py :language: python Receiving data while streaming ++++++++++++++++++++++++++++++ By default, a stream will listen for a client disconnect in the background, and stop the generator once received. Since this requires receiving data from the socket, it can lead to data loss if the application is attempting to read from the same socket simultaneously. .. tip:: To prevent data loss, by default, ``websocket_stream`` will raise an exception if it receives any data while listening for client disconnects. If incoming data should be ignored, ``allow_data_discard`` should be set to ``True`` If receiving data while streaming is desired, :func:`~litestar.handlers.send_websocket_stream` can be configured to not listen for disconnects by setting ``listen_for_disconnect=False``. .. important:: When using ``listen_for_disconnect=False``, the application needs to ensure the disconnect event is received elsewhere, otherwise the stream will only terminate when the generator is exhausted Combining streaming and receiving data --------------------------------------- To stream and receive data concurrently, the stream can be set up manually using :func:`~litestar.handlers.send_websocket_stream` in combination with either a regular :class:`~litestar.handlers.websocket` handler or a WebSocket listener. .. tab-set:: .. tab-item:: websocket_listener .. tab-set:: .. tab-item:: example .. literalinclude:: /examples/websockets/stream_and_receive_listener.py :language: python .. tab-item:: how to test .. literalinclude:: ../../tests/examples/test_websockets.py :language: python :lines: 18-25 .. tab-item:: websocket handler .. tab-set:: .. tab-item:: example .. literalinclude:: /examples/websockets/stream_and_receive_raw.py :language: python .. tab-item:: how to test .. literalinclude:: ../../tests/examples/test_websockets.py :language: python :lines: 28-35 Transport modes --------------- WebSockets have two transport modes: ``text`` and ``binary``. They dictate how bytes are transferred over the wire and can be set independently from another, i.e. a socket can send ``binary`` and receive ``text`` It may seem intuitive that ``text`` and ``binary`` should map to :class:`str` and :class:`bytes` respectively, but this is not the case. WebSockets can receive and send data in any format, independently of the mode. The mode only affects how the bytes are handled during transport (i.e. on the protocol level). In most cases the default mode - ``text`` - is all that's needed. Binary transport is usually employed when sending binary blobs that don't have a meaningful string representation, such as images. litestar-2.16.0/litestar/000077500000000000000000000000001500564371300152625ustar00rootroot00000000000000litestar-2.16.0/litestar/__init__.py000066400000000000000000000015031500564371300173720ustar00rootroot00000000000000from litestar.app import Litestar from litestar.connection import Request, WebSocket from litestar.controller import Controller from litestar.enums import HttpMethod, MediaType from litestar.handlers import ( asgi, delete, get, head, patch, post, put, route, websocket, websocket_listener, websocket_stream, ) from litestar.response import Response from litestar.router import Router from litestar.utils.version import get_version __version__ = get_version() __all__ = ( "Controller", "HttpMethod", "Litestar", "MediaType", "Request", "Response", "Router", "WebSocket", "__version__", "asgi", "delete", "get", "head", "patch", "post", "put", "route", "websocket", "websocket_listener", "websocket_stream", ) litestar-2.16.0/litestar/__main__.py000066400000000000000000000002501500564371300173510ustar00rootroot00000000000000from litestar.cli.main import litestar_group def run_cli() -> None: """Application Entrypoint.""" litestar_group() if __name__ == "__main__": run_cli() litestar-2.16.0/litestar/_asgi/000077500000000000000000000000001500564371300163445ustar00rootroot00000000000000litestar-2.16.0/litestar/_asgi/__init__.py000066400000000000000000000001151500564371300204520ustar00rootroot00000000000000from litestar._asgi.asgi_router import ASGIRouter __all__ = ("ASGIRouter",) litestar-2.16.0/litestar/_asgi/asgi_router.py000066400000000000000000000157361500564371300212550ustar00rootroot00000000000000from __future__ import annotations import re from collections import defaultdict from functools import lru_cache from traceback import format_exc from typing import TYPE_CHECKING, Any, Pattern from litestar._asgi.routing_trie import validate_node from litestar._asgi.routing_trie.mapping import add_route_to_trie from litestar._asgi.routing_trie.traversal import parse_path_to_route from litestar._asgi.routing_trie.types import create_node from litestar._asgi.utils import get_route_handlers from litestar.exceptions import ImproperlyConfiguredException from litestar.utils import normalize_path from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from litestar._asgi.routing_trie.types import RouteTrieNode from litestar.app import Litestar from litestar.routes import ASGIRoute, HTTPRoute, WebSocketRoute from litestar.routes.base import BaseRoute from litestar.types import ( ASGIApp, ExceptionHandlersMap, LifeSpanReceive, LifeSpanSend, LifeSpanShutdownCompleteEvent, LifeSpanShutdownFailedEvent, LifeSpanStartupCompleteEvent, LifeSpanStartupFailedEvent, Method, Receive, RouteHandlerType, Scope, Send, ) __all__ = ("ASGIRouter",) class ASGIRouter: """Litestar ASGI router. Handling both the ASGI lifespan events and routing of connection requests. """ __slots__ = ( "_app_exception_handlers", "_mount_paths_regex", "_mount_routes", "_plain_routes", "_registered_routes", "_static_routes", "app", "root_route_map_node", "route_handler_index", "route_mapping", ) def __init__(self, app: Litestar) -> None: """Initialize ``ASGIRouter``. Args: app: The Litestar app instance """ self._app_exception_handlers: ExceptionHandlersMap = app.exception_handlers self._mount_paths_regex: Pattern | None = None self._mount_routes: dict[str, RouteTrieNode] = {} self._plain_routes: set[str] = set() self._registered_routes: set[HTTPRoute | WebSocketRoute | ASGIRoute] = set() self.app = app self.root_route_map_node: RouteTrieNode = create_node() self.route_handler_index: dict[str, RouteHandlerType] = {} self.route_mapping: dict[str, list[BaseRoute]] = defaultdict(list) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI callable. The main entry point to the Router class. """ scope.setdefault("path_params", {}) path = scope["path"] if root_path := scope.get("root_path", ""): path = path.split(root_path, maxsplit=1)[-1] normalized_path = normalize_path(path) try: asgi_app, route_handler, scope["path"], scope["path_params"], path_template = self.handle_routing( path=normalized_path, method=scope.get("method") ) except Exception: ScopeState.from_scope(scope).exception_handlers = self._app_exception_handlers raise else: ScopeState.from_scope(scope).exception_handlers = route_handler.resolve_exception_handlers() scope["route_handler"] = route_handler scope["path_template"] = path_template await asgi_app(scope, receive, send) @lru_cache(1024) # noqa: B019 def handle_routing( self, path: str, method: Method | None ) -> tuple[ASGIApp, RouteHandlerType, str, dict[str, Any], str]: """Handle routing for a given path / method combo. This method is meant to allow easy caching. Args: path: The path of the request. method: The scope's method, if any. Returns: A tuple composed of the ASGIApp of the route, the route handler instance, the resolved and normalized path and any parsed path params. """ return parse_path_to_route( mount_paths_regex=self._mount_paths_regex, mount_routes=self._mount_routes, path=path, plain_routes=self._plain_routes, root_node=self.root_route_map_node, method=method, ) def _store_handler_to_route_mapping(self, route: BaseRoute) -> None: """Store the mapping of route handlers to routes and to route handler names. Args: route: A Route instance. Returns: None """ for handler in get_route_handlers(route): if handler.name in self.route_handler_index and str(self.route_handler_index[handler.name]) != str(handler): raise ImproperlyConfiguredException( f"route handler names must be unique - {handler.name} is not unique." ) identifier = handler.name or str(handler) self.route_mapping[identifier].append(route) self.route_handler_index[identifier] = handler def construct_routing_trie(self) -> None: """Create a map of the app's routes. This map is used in the asgi router to route requests. """ new_routes = [route for route in self.app.routes if route not in self._registered_routes] for route in new_routes: add_route_to_trie( app=self.app, mount_routes=self._mount_routes, plain_routes=self._plain_routes, root_node=self.root_route_map_node, route=route, ) self._store_handler_to_route_mapping(route) self._registered_routes.add(route) validate_node(node=self.root_route_map_node) if self._mount_routes: self._mount_paths_regex = re.compile("|".join(sorted(set(self._mount_routes)))) # pyright: ignore async def lifespan(self, receive: LifeSpanReceive, send: LifeSpanSend) -> None: """Handle the ASGI "lifespan" event on application startup and shutdown. Args: receive: The ASGI receive function. send: The ASGI send function. Returns: None. """ shutdown_event: LifeSpanShutdownCompleteEvent = {"type": "lifespan.shutdown.complete"} startup_event: LifeSpanStartupCompleteEvent = {"type": "lifespan.startup.complete"} await receive() started = False try: async with self.app.lifespan(): await send(startup_event) started = True await receive() except BaseException as e: formatted_exception = format_exc() failure_message: LifeSpanStartupFailedEvent | LifeSpanShutdownFailedEvent if started: failure_message = {"type": "lifespan.shutdown.failed", "message": formatted_exception} else: failure_message = {"type": "lifespan.startup.failed", "message": formatted_exception} await send(failure_message) raise e await send(shutdown_event) litestar-2.16.0/litestar/_asgi/routing_trie/000077500000000000000000000000001500564371300210565ustar00rootroot00000000000000litestar-2.16.0/litestar/_asgi/routing_trie/__init__.py000066400000000000000000000005351500564371300231720ustar00rootroot00000000000000from litestar._asgi.routing_trie.mapping import add_route_to_trie from litestar._asgi.routing_trie.traversal import parse_path_to_route from litestar._asgi.routing_trie.types import RouteTrieNode from litestar._asgi.routing_trie.validate import validate_node __all__ = ("RouteTrieNode", "add_route_to_trie", "parse_path_to_route", "validate_node") litestar-2.16.0/litestar/_asgi/routing_trie/mapping.py000066400000000000000000000206031500564371300230640ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING, Any, cast from litestar._asgi.routing_trie.types import ( ASGIHandlerTuple, PathParameterSentinel, create_node, ) from litestar._asgi.utils import wrap_in_exception_handler from litestar.types.internal_types import PathParameterDefinition __all__ = ("add_mount_route", "add_route_to_trie", "build_route_middleware_stack", "configure_node") if TYPE_CHECKING: from litestar._asgi.routing_trie.types import RouteTrieNode from litestar.app import Litestar from litestar.routes import ASGIRoute, HTTPRoute, WebSocketRoute from litestar.types import ASGIApp, RouteHandlerType def add_mount_route( current_node: RouteTrieNode, mount_routes: dict[str, RouteTrieNode], root_node: RouteTrieNode, route: ASGIRoute, ) -> RouteTrieNode: """Add a node for a mount route. Args: current_node: The current trie node that is being mapped. mount_routes: A dictionary mapping static routes to trie nodes. root_node: The root trie node. route: The route that is being added. Returns: A trie node. """ # we need to ensure that we can traverse the map both through the full path key, e.g. "/my-route/sub-path" and # via the components keys ["my-route, "sub-path"] if route.path not in current_node.children: root_node = current_node for component in route.path_components: if component not in current_node.children: current_node.children[component] = create_node() # type: ignore[index] current_node = current_node.children[component] # type: ignore[index] current_node.is_mount = True current_node.is_static = route.route_handler.is_static if route.path != "/": mount_routes[route.path] = root_node.children[route.path] = current_node else: mount_routes[route.path] = current_node return current_node def add_route_to_trie( app: Litestar, mount_routes: dict[str, RouteTrieNode], plain_routes: set[str], root_node: RouteTrieNode, route: HTTPRoute | WebSocketRoute | ASGIRoute, ) -> RouteTrieNode: """Add a new route path (e.g. '/foo/bar/{param:int}') into the route_map tree. Inserts non-parameter paths ('plain routes') off the tree's root node. For paths containing parameters, splits the path on '/' and nests each path segment under the previous segment's node (see prefix tree / trie). Args: app: The Litestar app instance. mount_routes: A dictionary mapping static routes to trie nodes. plain_routes: A set of routes that do not have path parameters. root_node: The root trie node. route: The route that is being added. Returns: A RouteTrieNode instance. """ current_node = root_node has_path_parameters = bool(route.path_parameters) if (route_handler := getattr(route, "route_handler", None)) and getattr(route_handler, "is_mount", False): current_node = add_mount_route( current_node=current_node, mount_routes=mount_routes, root_node=root_node, route=cast("ASGIRoute", route), ) elif not has_path_parameters: plain_routes.add(route.path) if route.path not in root_node.children: current_node.children[route.path] = create_node() current_node = root_node.children[route.path] else: for component in route.path_components: if isinstance(component, PathParameterDefinition): current_node.is_path_param_node = True next_node_key: type[PathParameterSentinel] | str = PathParameterSentinel else: next_node_key = component if next_node_key not in current_node.children: current_node.children[next_node_key] = create_node() current_node.child_keys = set(current_node.children.keys()) current_node = current_node.children[next_node_key] if isinstance(component, PathParameterDefinition) and component.type is Path: current_node.is_path_type = True configure_node(route=route, app=app, node=current_node) return current_node def configure_node( app: Litestar, route: HTTPRoute | WebSocketRoute | ASGIRoute, node: RouteTrieNode, ) -> None: """Set required attributes and route handlers on route_map tree node. Args: app: The Litestar app instance. route: The route that is being added. node: The trie node being configured. Returns: None """ from litestar.routes import HTTPRoute, WebSocketRoute node.path_template = route.path_format if not node.path_parameters: node.path_parameters = {} if isinstance(route, HTTPRoute): for method, handler_mapping in route.route_handler_map.items(): handler, _ = handler_mapping node.asgi_handlers[method] = ASGIHandlerTuple( asgi_app=build_route_middleware_stack(app=app, route=route, route_handler=handler), handler=handler, ) node.path_parameters[method] = tuple(route.path_parameters.values()) elif isinstance(route, WebSocketRoute): node.asgi_handlers["websocket"] = ASGIHandlerTuple( asgi_app=build_route_middleware_stack(app=app, route=route, route_handler=route.route_handler), handler=route.route_handler, ) node.path_parameters["websocket"] = tuple(route.path_parameters.values()) else: node.asgi_handlers["asgi"] = ASGIHandlerTuple( asgi_app=build_route_middleware_stack(app=app, route=route, route_handler=route.route_handler), handler=route.route_handler, ) node.path_parameters["asgi"] = tuple(route.path_parameters.values()) node.is_asgi = True def build_route_middleware_stack( app: Litestar, route: HTTPRoute | WebSocketRoute | ASGIRoute, route_handler: RouteHandlerType, ) -> ASGIApp: """Construct a middleware stack that serves as the point of entry for each route. Args: app: The Litestar app instance. route: The route that is being added. route_handler: The route handler that is being wrapped. Returns: An ASGIApp that is composed of a "stack" of middlewares. """ from litestar.middleware.allowed_hosts import AllowedHostsMiddleware from litestar.middleware.compression import CompressionMiddleware from litestar.middleware.csrf import CSRFMiddleware from litestar.middleware.response_cache import ResponseCacheMiddleware from litestar.routes import HTTPRoute asgi_handler: ASGIApp = route.handle # type: ignore[assignment] handler_middleware = route_handler.resolve_middleware() has_cached_route = isinstance(route, HTTPRoute) and any(r.cache for r in route.route_handlers) has_middleware = ( app.csrf_config or app.compression_config or has_cached_route or app.allowed_hosts or handler_middleware ) if has_middleware: # If there is an exception raised from the handler, the first ExceptionHandlerMiddleware that catches the # exception will create the response and call send(). As middleware may wrap the send() callable, we need there # to be an instance of ExceptionHandlerMiddleware in between the handler and the middleware so that any send # wrappers instated by middleware are called. If there is no middleware, we can skip this step. asgi_handler = wrap_in_exception_handler(app=asgi_handler) if app.csrf_config: asgi_handler = CSRFMiddleware(app=asgi_handler, config=app.csrf_config) if app.compression_config: asgi_handler = CompressionMiddleware(app=asgi_handler, config=app.compression_config) if has_cached_route: asgi_handler = ResponseCacheMiddleware(app=asgi_handler, config=app.response_cache_config) if app.allowed_hosts: asgi_handler = AllowedHostsMiddleware(app=asgi_handler, config=app.allowed_hosts) for middleware in handler_middleware: if hasattr(middleware, "__iter__"): handler, kwargs = cast("tuple[Any, dict[str, Any]]", middleware) asgi_handler = handler(app=asgi_handler, **kwargs) else: asgi_handler = middleware(app=asgi_handler) # type: ignore[call-arg] return asgi_handler litestar-2.16.0/litestar/_asgi/routing_trie/traversal.py000066400000000000000000000137741500564371300234470ustar00rootroot00000000000000from __future__ import annotations from functools import lru_cache from typing import TYPE_CHECKING, Any, Pattern from litestar._asgi.routing_trie.types import PathParameterSentinel from litestar.exceptions import MethodNotAllowedException, NotFoundException from litestar.utils import normalize_path __all__ = ("parse_node_handlers", "parse_path_params", "parse_path_to_route", "traverse_route_map") if TYPE_CHECKING: from litestar._asgi.routing_trie.types import ASGIHandlerTuple, RouteTrieNode from litestar.types import ASGIApp, Method, RouteHandlerType from litestar.types.internal_types import PathParameterDefinition def traverse_route_map( root_node: RouteTrieNode, path: str, ) -> tuple[RouteTrieNode, list[str], str]: """Traverses the application route mapping and retrieves the correct node for the request url. Args: root_node: The root trie node. path: The request's path. Raises: NotFoundException: If no correlating node is found. Returns: A tuple containing the target RouteMapNode and a list containing all path parameter values. """ current_node = root_node path_params: list[str] = [] path_components = [p for p in path.split("/") if p] for i, component in enumerate(path_components): if component in current_node.child_keys: current_node = current_node.children[component] continue if current_node.is_path_param_node: current_node = current_node.children[PathParameterSentinel] if current_node.is_path_type: path_params.append(normalize_path("/".join(path_components[i:]))) break path_params.append(component) continue raise NotFoundException() if not current_node.asgi_handlers: raise NotFoundException() return current_node, path_params, path def parse_node_handlers( node: RouteTrieNode, method: Method | None, ) -> ASGIHandlerTuple: """Retrieve the handler tuple from the node. Args: node: The trie node to parse. method: The scope's method. Raises: KeyError: If no matching method is found. Returns: An ASGI Handler tuple. """ if node.is_asgi: return node.asgi_handlers["asgi"] if method: return node.asgi_handlers[method] return node.asgi_handlers["websocket"] @lru_cache(1024) def parse_path_params( parameter_definitions: tuple[PathParameterDefinition, ...], path_param_values: tuple[str, ...] ) -> dict[str, Any]: """Parse path parameters into a dictionary of values. Args: parameter_definitions: The parameter definitions tuple from the route. path_param_values: The string values extracted from the url Raises: ValueError: If any of path parameters can not be parsed into a value. Returns: A dictionary of parsed path parameters. """ return { param_definition.name: param_definition.parser(value) if param_definition.parser else value for param_definition, value in zip(parameter_definitions, path_param_values) } def parse_path_to_route( method: Method | None, mount_paths_regex: Pattern | None, mount_routes: dict[str, RouteTrieNode], path: str, plain_routes: set[str], root_node: RouteTrieNode, ) -> tuple[ASGIApp, RouteHandlerType, str, dict[str, Any], str]: """Given a scope object, retrieve the asgi_handlers and is_mount boolean values from correct trie node. Args: method: The scope's method, if any. root_node: The root trie node. path: The path to resolve scope instance. plain_routes: The set of plain routes. mount_routes: Mapping of mount routes to trie nodes. mount_paths_regex: A compiled regex to match the mount routes. Raises: MethodNotAllowedException: if no matching method is found. NotFoundException: If no correlating node is found or if path params can not be parsed into values according to the node definition. Returns: A tuple containing the stack of middlewares and the route handler that is wrapped by it. """ try: if path in plain_routes: asgi_app, handler = parse_node_handlers(node=root_node.children[path], method=method) return asgi_app, handler, path, {}, path if mount_paths_regex and (match := mount_paths_regex.match(path)): mount_path = path[: match.end()] mount_node = mount_routes[mount_path] remaining_path = path[match.end() :] # since we allow regular handlers under static paths, we must validate that the request does not match # any such handler. children = ( normalize_path(sub_route) for sub_route in mount_node.children or [] if sub_route != mount_path and isinstance(sub_route, str) ) if not any(remaining_path.startswith(f"{sub_route}/") for sub_route in children): asgi_app, handler = parse_node_handlers(node=mount_node, method=method) remaining_path = remaining_path or "/" if not mount_node.is_static: remaining_path = remaining_path if remaining_path.endswith("/") else f"{remaining_path}/" return asgi_app, handler, remaining_path, {}, root_node.path_template node, path_parameters, path = traverse_route_map( root_node=root_node, path=path, ) asgi_app, handler = parse_node_handlers(node=node, method=method) key = method or ("asgi" if node.is_asgi else "websocket") parsed_path_parameters = parse_path_params(node.path_parameters[key], tuple(path_parameters)) return ( asgi_app, handler, path, parsed_path_parameters, node.path_template, ) except KeyError as e: raise MethodNotAllowedException() from e except ValueError as e: raise NotFoundException() from e litestar-2.16.0/litestar/_asgi/routing_trie/types.py000066400000000000000000000053201500564371300225740ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Literal, NamedTuple __all__ = ("ASGIHandlerTuple", "PathParameterSentinel", "RouteTrieNode", "create_node") if TYPE_CHECKING: from litestar.types import ASGIApp, Method, RouteHandlerType from litestar.types.internal_types import PathParameterDefinition class PathParameterSentinel: """Sentinel class designating a path parameter.""" class ASGIHandlerTuple(NamedTuple): """Encapsulation of a route handler node.""" asgi_app: ASGIApp """An ASGI stack, composed of a handler function and layers of middleware that wrap it.""" handler: RouteHandlerType """The route handler instance.""" @dataclass(unsafe_hash=True) class RouteTrieNode: """A radix trie node.""" __slots__ = ( "asgi_handlers", "child_keys", "children", "is_asgi", "is_mount", "is_path_param_node", "is_path_type", "is_static", "path_parameters", "path_template", ) asgi_handlers: dict[Method | Literal["websocket", "asgi"], ASGIHandlerTuple] """A mapping of ASGI handlers stored on the node.""" child_keys: set[str | type[PathParameterSentinel]] """ A set containing the child keys, same as the children dictionary - but as a set, which offers faster lookup. """ children: dict[str | type[PathParameterSentinel], RouteTrieNode] """A dictionary mapping path components or using the PathParameterSentinel class to child nodes.""" is_path_param_node: bool """Designates the node as having a path parameter.""" is_path_type: bool """Designates the node as having a 'path' type path parameter.""" is_asgi: bool """Designate the node as having an `asgi` type handler.""" is_mount: bool """Designate the node as being a mount route.""" is_static: bool """Designate the node as being a static mount route.""" path_parameters: dict[Method | Literal["websocket"] | Literal["asgi"], tuple[PathParameterDefinition, ...]] """A list of tuples containing path parameter definitions. This is used for parsing extracted path parameter values. """ path_template: str """The path template string used to lower prometheus cardinality when group_path enabled""" def create_node() -> RouteTrieNode: """Create a RouteMapNode instance. Returns: A route map node instance. """ return RouteTrieNode( asgi_handlers={}, child_keys=set(), children={}, is_path_param_node=False, is_asgi=False, is_mount=False, is_static=False, is_path_type=False, path_parameters={}, path_template="", ) litestar-2.16.0/litestar/_asgi/routing_trie/validate.py000066400000000000000000000024131500564371300232210ustar00rootroot00000000000000from __future__ import annotations from itertools import chain from typing import TYPE_CHECKING from litestar.exceptions import ImproperlyConfiguredException __all__ = ("validate_node",) if TYPE_CHECKING: from litestar._asgi.routing_trie.types import RouteTrieNode def validate_node(node: RouteTrieNode) -> None: """Recursively traverses the trie from the given node upwards. Args: node: A trie node. Raises: ImproperlyConfiguredException Returns: None """ if node.is_asgi and bool(set(node.asgi_handlers).difference({"asgi"})): raise ImproperlyConfiguredException("ASGI handlers must have a unique path not shared by other route handlers.") if ( node.is_mount and node.children and any( chain.from_iterable( list(child.path_parameters.values()) if isinstance(child.path_parameters, dict) else child.path_parameters for child in node.children.values() ) ) ): raise ImproperlyConfiguredException("Path parameters are not allowed under a static or mount route.") for child in node.children.values(): if child is node: continue validate_node(node=child) litestar-2.16.0/litestar/_asgi/utils.py000066400000000000000000000023751500564371300200650ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, cast if TYPE_CHECKING: from litestar.routes import ASGIRoute, HTTPRoute, WebSocketRoute from litestar.routes.base import BaseRoute from litestar.types import ASGIApp, RouteHandlerType __all__ = ("get_route_handlers", "wrap_in_exception_handler") def wrap_in_exception_handler(app: ASGIApp) -> ASGIApp: """Wrap the given ASGIApp in an instance of ExceptionHandlerMiddleware. Args: app: The ASGI app that is being wrapped. Returns: A wrapped ASGIApp. """ from litestar.middleware._internal.exceptions import ExceptionHandlerMiddleware return ExceptionHandlerMiddleware(app=app, debug=None) def get_route_handlers(route: BaseRoute) -> list[RouteHandlerType]: """Retrieve handler(s) as a list for given route. Args: route: The route from which the route handlers are extracted. Returns: The route handlers defined on the route. """ route_handlers: list[RouteHandlerType] = [] if hasattr(route, "route_handlers"): route_handlers.extend(cast("HTTPRoute", route).route_handlers) else: route_handlers.append(cast("WebSocketRoute | ASGIRoute", route).route_handler) return route_handlers litestar-2.16.0/litestar/_kwargs/000077500000000000000000000000001500564371300167175ustar00rootroot00000000000000litestar-2.16.0/litestar/_kwargs/__init__.py000066400000000000000000000001021500564371300210210ustar00rootroot00000000000000from .kwargs_model import KwargsModel __all__ = ("KwargsModel",) litestar-2.16.0/litestar/_kwargs/cleanup.py000066400000000000000000000106401500564371300207210ustar00rootroot00000000000000from __future__ import annotations import sys from contextlib import AbstractAsyncContextManager from inspect import isasyncgen from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Generator if sys.version_info < (3, 11): from exceptiongroup import ExceptionGroup from anyio import create_task_group from litestar.utils import ensure_async_callable from litestar.utils.compat import async_next __all__ = ("DependencyCleanupGroup",) if TYPE_CHECKING: from types import TracebackType from litestar.types import AnyGenerator class DependencyCleanupGroup(AbstractAsyncContextManager): """Wrapper for generator based dependencies. Simplify cleanup by wrapping :func:`next` / :func:`anext` calls and providing facilities to :meth:`throw ` / :meth:`athrow ` into all generators consecutively. An instance of this class can be used as a contextmanager, which will automatically throw any exceptions into its generators. All exceptions caught in this manner will be re-raised after they have been thrown in the generators. """ def __init__(self, generators: list[AnyGenerator] | None = None) -> None: """Initialize ``DependencyCleanupGroup``. Args: generators: An optional list of generators to be called at cleanup """ self._generators = generators or [] self._closed = False def add(self, generator: Generator[Any, None, None] | AsyncGenerator[Any, None]) -> None: """Add a new generator to the group. Args: generator: The generator to add Returns: None """ if self._closed: raise RuntimeError("Cannot call .add on a closed DependencyCleanupGroup") self._generators.append(generator) @staticmethod def _wrap_next(generator: AnyGenerator) -> Callable[[], Awaitable[None]]: if isasyncgen(generator): async def wrapped_async() -> None: await async_next(generator, None) return wrapped_async def wrapped() -> None: next(generator, None) # type: ignore[arg-type] return ensure_async_callable(wrapped) async def close(self, exc: BaseException | None = None) -> None: if self._closed: raise RuntimeError("Cannot call cleanup on a closed DependencyCleanupGroup") self._closed = True if exc is None: await self._cleanup() else: await self._throw(exc) async def _cleanup(self) -> None: """Execute cleanup by calling :func:`next` / :func:`anext` on all generators. If there are multiple generators to be called, they will be executed in a :class:`anyio.TaskGroup`. Returns: None """ if not self._generators: return if len(self._generators) == 1: await self._wrap_next(self._generators[0])() return async with create_task_group() as task_group: for generator in self._generators: task_group.start_soon(self._wrap_next(generator)) async def __aenter__(self) -> None: """Support the async contextmanager protocol to allow for easier catching and throwing of exceptions into the generators. """ async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: """If an exception was raised within the contextmanager block, throw it into all generators.""" await self.close(exc_val) async def _throw(self, exc: BaseException) -> None: """Throw an exception in all generators sequentially. Args: exc: Exception to throw """ exceptions = [] for gen in self._generators: try: if isasyncgen(gen): await gen.athrow(exc) else: gen.throw(exc) # type: ignore[union-attr] except (StopIteration, StopAsyncIteration): continue except Exception as cleanup_exc: # noqa: BLE001 if cleanup_exc is not exc: exceptions.append(cleanup_exc) if exceptions: raise ExceptionGroup( "Exceptions occurred during cleanup of dependencies", exceptions, ) from exc litestar-2.16.0/litestar/_kwargs/dependencies.py000066400000000000000000000101501500564371300217140ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from litestar.utils.compat import async_next __all__ = ("Dependency", "create_dependency_batches", "map_dependencies_recursively", "resolve_dependency") if TYPE_CHECKING: from litestar._kwargs.cleanup import DependencyCleanupGroup from litestar.connection import ASGIConnection from litestar.di import Provide class Dependency: """Dependency graph of a given combination of ``Route`` + ``RouteHandler``""" __slots__ = ("dependencies", "key", "provide") def __init__(self, key: str, provide: Provide, dependencies: list[Dependency]) -> None: """Initialize a dependency. Args: key: The dependency key provide: Provider dependencies: List of child nodes """ self.key = key self.provide = provide self.dependencies = dependencies def __eq__(self, other: Any) -> bool: # check if memory address is identical, otherwise compare attributes return other is self or (isinstance(other, self.__class__) and other.key == self.key) def __hash__(self) -> int: return hash(self.key) async def resolve_dependency( dependency: Dependency, connection: ASGIConnection, kwargs: dict[str, Any], cleanup_group: DependencyCleanupGroup, ) -> None: """Resolve a given instance of :class:`Dependency `. All required sub dependencies must already be resolved into the kwargs. The result of the dependency will be stored in the kwargs. Args: dependency: An instance of :class:`Dependency ` connection: An instance of :class:`Request ` or :class:`WebSocket `. kwargs: Any kwargs to pass to the dependency, the result will be stored here as well. cleanup_group: DependencyCleanupGroup to which generators returned by ``dependency`` will be added """ signature_model = dependency.provide.signature_model dependency_kwargs = ( signature_model.parse_values_from_connection_kwargs(connection=connection, kwargs=kwargs) if signature_model._fields else {} ) value = await dependency.provide(**dependency_kwargs) if dependency.provide.has_sync_generator_dependency: cleanup_group.add(value) value = next(value) elif dependency.provide.has_async_generator_dependency: cleanup_group.add(value) value = await async_next(value) kwargs[dependency.key] = value def create_dependency_batches(expected_dependencies: set[Dependency]) -> list[set[Dependency]]: """Calculate batches for all dependencies, recursively. Args: expected_dependencies: A set of all direct :class:`Dependencies `. Returns: A list of batches. """ dependencies_to: dict[Dependency, set[Dependency]] = {} for dependency in expected_dependencies: if dependency not in dependencies_to: map_dependencies_recursively(dependency, dependencies_to) batches = [] while dependencies_to: current_batch = { dependency for dependency, remaining_sub_dependencies in dependencies_to.items() if not remaining_sub_dependencies } for dependency in current_batch: del dependencies_to[dependency] for others_dependencies in dependencies_to.values(): others_dependencies.discard(dependency) batches.append(current_batch) return batches def map_dependencies_recursively(dependency: Dependency, dependencies_to: dict[Dependency, set[Dependency]]) -> None: """Recursively map dependencies to their sub dependencies. Args: dependency: The current dependency to map. dependencies_to: A map of dependency to its sub dependencies. """ dependencies_to[dependency] = set(dependency.dependencies) for sub in dependency.dependencies: if sub not in dependencies_to: map_dependencies_recursively(sub, dependencies_to) litestar-2.16.0/litestar/_kwargs/extractors.py000066400000000000000000000413451500564371300214760ustar00rootroot00000000000000from __future__ import annotations from collections import defaultdict from functools import lru_cache, partial from typing import TYPE_CHECKING, Any, Callable, Coroutine, Mapping, NamedTuple, cast from litestar._multipart import parse_multipart_form from litestar._parsers import ( parse_query_string, parse_url_encoded_form_data, ) from litestar.datastructures import Headers from litestar.datastructures.upload_file import UploadFile from litestar.datastructures.url import URL from litestar.enums import ParamType, RequestEncodingType from litestar.exceptions import ValidationException from litestar.params import BodyKwarg from litestar.types import Empty from litestar.utils import make_non_optional_union from litestar.utils.predicates import is_non_string_sequence, is_optional_union from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from litestar._kwargs import KwargsModel from litestar._kwargs.parameter_definition import ParameterDefinition from litestar._kwargs.types import Extractor from litestar.connection import ASGIConnection, Request from litestar.dto import AbstractDTO from litestar.typing import FieldDefinition __all__ = ( "body_extractor", "cookies_extractor", "create_connection_value_extractor", "create_data_extractor", "create_multipart_extractor", "create_query_default_dict", "create_url_encoded_data_extractor", "headers_extractor", "json_extractor", "msgpack_extractor", "parse_connection_headers", "parse_connection_query_params", "query_extractor", "request_extractor", "scope_extractor", "socket_extractor", "state_extractor", ) class ParamMappings(NamedTuple): alias_and_key_tuples: list[tuple[str, str]] alias_defaults: dict[str, Any] alias_to_param: dict[str, ParameterDefinition] def _create_param_mappings(expected_params: set[ParameterDefinition]) -> ParamMappings: alias_and_key_tuples = [] alias_defaults = {} alias_to_params: dict[str, ParameterDefinition] = {} for param in expected_params: alias = param.field_alias if param.param_type == ParamType.HEADER: alias = alias.lower() alias_and_key_tuples.append((alias, param.field_name)) if not (param.is_required or param.default is Ellipsis): alias_defaults[alias] = param.default alias_to_params[alias] = param return ParamMappings( alias_and_key_tuples=alias_and_key_tuples, alias_defaults=alias_defaults, alias_to_param=alias_to_params, ) def create_connection_value_extractor( kwargs_model: KwargsModel, connection_key: str, expected_params: set[ParameterDefinition], parser: Callable[[ASGIConnection, KwargsModel], Mapping[str, Any]] | None = None, ) -> Extractor: """Create a kwargs extractor function. Args: kwargs_model: The KwargsModel instance. connection_key: The attribute key to use. expected_params: The set of expected params. parser: An optional parser function. Returns: An extractor function. """ alias_and_key_tuples, alias_defaults, alias_to_params = _create_param_mappings(expected_params) async def extractor(values: dict[str, Any], connection: ASGIConnection) -> None: data = parser(connection, kwargs_model) if parser else getattr(connection, connection_key, {}) try: connection_mapping: dict[str, Any] = { key: data[alias] if alias in data else alias_defaults[alias] for alias, key in alias_and_key_tuples } values.update(connection_mapping) except KeyError as e: param = alias_to_params[e.args[0]] path = URL.from_components( path=connection.url.path, query=connection.url.query, ) raise ValidationException( f"Missing required {param.param_type.value} parameter {param.field_alias!r} for path {path}" ) from e return extractor @lru_cache(1024) def create_query_default_dict( parsed_query: tuple[tuple[str, str], ...], sequence_query_parameter_names: tuple[str, ...] ) -> defaultdict[str, list[str] | str]: """Transform a list of tuples into a default dict. Ensures non-list values are not wrapped in a list. Args: parsed_query: The parsed query list of tuples. sequence_query_parameter_names: A set of query parameters that should be wrapped in list. Returns: A default dict """ output: defaultdict[str, list[str] | str] = defaultdict(list) for k, v in parsed_query: if k in sequence_query_parameter_names: output[k].append(v) # type: ignore[union-attr] else: output[k] = v return output def parse_connection_query_params(connection: ASGIConnection, kwargs_model: KwargsModel) -> dict[str, Any]: """Parse query params and cache the result in scope. Args: connection: The ASGI connection instance. kwargs_model: The KwargsModel instance. Returns: A dictionary of parsed values. """ parsed_query = ( connection._parsed_query if connection._parsed_query is not Empty else parse_query_string(connection.scope.get("query_string", b"")) ) ScopeState.from_scope(connection.scope).parsed_query = parsed_query return create_query_default_dict( parsed_query=parsed_query, sequence_query_parameter_names=kwargs_model.sequence_query_parameter_names, ) def parse_connection_headers(connection: ASGIConnection, _: KwargsModel) -> Headers: """Parse header parameters and cache the result in scope. Args: connection: The ASGI connection instance. _: The KwargsModel instance. Returns: A Headers instance """ return Headers.from_scope(connection.scope) async def state_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Extract the app state from the connection and insert it to the kwargs injected to the handler. Args: connection: The ASGI connection instance. values: The kwargs that are extracted from the connection and will be injected into the handler. Returns: None """ values["state"] = connection.app.state._state async def headers_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Extract the headers from the connection and insert them to the kwargs injected to the handler. Args: connection: The ASGI connection instance. values: The kwargs that are extracted from the connection and will be injected into the handler. Returns: None """ # TODO: This should be removed in 3.0 and instead Headers should be injected # directly. We are only keeping this one around to not break things values["headers"] = dict(connection.headers.items()) async def cookies_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Extract the cookies from the connection and insert them to the kwargs injected to the handler. Args: connection: The ASGI connection instance. values: The kwargs that are extracted from the connection and will be injected into the handler. Returns: None """ values["cookies"] = connection.cookies async def query_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Extract the query params from the connection and insert them to the kwargs injected to the handler. Args: connection: The ASGI connection instance. values: The kwargs that are extracted from the connection and will be injected into the handler. Returns: None """ values["query"] = connection.query_params async def scope_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Extract the scope from the connection and insert it into the kwargs injected to the handler. Args: connection: The ASGI connection instance. values: The kwargs that are extracted from the connection and will be injected into the handler. Returns: None """ values["scope"] = connection.scope async def request_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Set the connection instance as the 'request' value in the kwargs injected to the handler. Args: connection: The ASGI connection instance. values: The kwargs that are extracted from the connection and will be injected into the handler. Returns: None """ values["request"] = connection async def socket_extractor(values: dict[str, Any], connection: ASGIConnection) -> None: """Set the connection instance as the 'socket' value in the kwargs injected to the handler. Args: connection: The ASGI connection instance. values: The kwargs that are extracted from the connection and will be injected into the handler. Returns: None """ values["socket"] = connection async def body_extractor( values: dict[str, Any], connection: Request[Any, Any, Any], ) -> None: """Extract the body from the request instance. Notes: - this extractor sets a Coroutine as the value in the kwargs. These are resolved at a later stage. Args: connection: The ASGI connection instance. values: The kwargs that are extracted from the connection and will be injected into the handler. Returns: The Body value. """ values["body"] = await connection.body() async def json_extractor(connection: Request[Any, Any, Any]) -> Any: """Extract the data from request and insert it into the kwargs injected to the handler. Notes: - this extractor sets a Coroutine as the value in the kwargs. These are resolved at a later stage. Args: connection: The ASGI connection instance. Returns: The JSON value. """ if not await connection.body(): return Empty return await connection.json() async def msgpack_extractor(connection: Request[Any, Any, Any]) -> Any: """Extract the data from request and insert it into the kwargs injected to the handler. Notes: - this extractor sets a Coroutine as the value in the kwargs. These are resolved at a later stage. Args: connection: The ASGI connection instance. Returns: The MessagePack value. """ if not await connection.body(): return Empty return await connection.msgpack() async def _extract_multipart( connection: Request[Any, Any, Any], body_kwarg_multipart_form_part_limit: int | None, field_definition: FieldDefinition, is_data_optional: bool, data_dto: type[AbstractDTO] | None, ) -> Any: multipart_form_part_limit = ( body_kwarg_multipart_form_part_limit if body_kwarg_multipart_form_part_limit is not None else connection.app.multipart_form_part_limit ) scope_state = ScopeState.from_scope(connection.scope) if scope_state.form is Empty: scope_state.form = form_values = await parse_multipart_form( stream=connection.stream(), boundary=connection.content_type[-1].get("boundary", "").encode(), multipart_form_part_limit=multipart_form_part_limit, type_decoders=connection.route_handler.resolve_type_decoders(), ) else: form_values = scope_state.form if field_definition.is_non_string_sequence: values = list(form_values.values()) if isinstance(values[0], list) and ( field_definition.has_inner_subclass_of(UploadFile) or (field_definition.is_optional and field_definition.inner_types[0].is_non_string_sequence) ): return values[0] return values if field_definition.is_simple_type and field_definition.annotation is UploadFile and form_values: return next(v for v in form_values.values() if isinstance(v, UploadFile)) if not form_values and is_data_optional: return None if data_dto: return data_dto(connection).decode_builtins(form_values) for name, tp in field_definition.get_type_hints().items(): value = form_values.get(name) if ( value is not None and not isinstance(value, list) and ( is_non_string_sequence(tp) or (is_optional_union(tp) and is_non_string_sequence(make_non_optional_union(tp))) ) ): form_values[name] = [value] # pyright: ignore return form_values def create_multipart_extractor( field_definition: FieldDefinition, is_data_optional: bool, data_dto: type[AbstractDTO] | None ) -> Callable[[ASGIConnection[Any, Any, Any, Any]], Coroutine[Any, Any, Any]]: """Create a multipart form-data extractor. Args: field_definition: A FieldDefinition instance. is_data_optional: Boolean dictating whether the field is optional. data_dto: A data DTO type, if configured for handler. Returns: An extractor function. """ body_kwarg_multipart_form_part_limit: int | None = None if field_definition.kwarg_definition and isinstance(field_definition.kwarg_definition, BodyKwarg): body_kwarg_multipart_form_part_limit = field_definition.kwarg_definition.multipart_form_part_limit extract_multipart = partial( _extract_multipart, body_kwarg_multipart_form_part_limit=body_kwarg_multipart_form_part_limit, is_data_optional=is_data_optional, data_dto=data_dto, field_definition=field_definition, ) return cast("Callable[[ASGIConnection[Any, Any, Any, Any]], Coroutine[Any, Any, Any]]", extract_multipart) def create_url_encoded_data_extractor( is_data_optional: bool, data_dto: type[AbstractDTO] | None ) -> Callable[[ASGIConnection[Any, Any, Any, Any]], Coroutine[Any, Any, Any]]: """Create extractor for url encoded form-data. Args: is_data_optional: Boolean dictating whether the field is optional. data_dto: A data DTO type, if configured for handler. Returns: An extractor function. """ async def extract_url_encoded_extractor( connection: Request[Any, Any, Any], ) -> Any: scope_state = ScopeState.from_scope(connection.scope) if scope_state.form is Empty: scope_state.form = form_values = ( # type: ignore[assignment] parse_url_encoded_form_data(await connection.body()) ) else: form_values = scope_state.form # type: ignore[assignment] if not form_values and is_data_optional: return None return data_dto(connection).decode_builtins(form_values) if data_dto else form_values return cast( "Callable[[ASGIConnection[Any, Any, Any, Any]], Coroutine[Any, Any, Any]]", extract_url_encoded_extractor ) def create_data_extractor(kwargs_model: KwargsModel) -> Extractor: """Create an extractor for a request's body. Args: kwargs_model: The KwargsModel instance. Returns: An extractor for the request's body. """ if kwargs_model.expected_form_data: media_type, field_definition = kwargs_model.expected_form_data if media_type == RequestEncodingType.MULTI_PART: data_extractor = create_multipart_extractor( field_definition=field_definition, is_data_optional=kwargs_model.is_data_optional, data_dto=kwargs_model.expected_data_dto, ) else: data_extractor = create_url_encoded_data_extractor( is_data_optional=kwargs_model.is_data_optional, data_dto=kwargs_model.expected_data_dto, ) elif kwargs_model.expected_msgpack_data: data_extractor = cast( "Callable[[ASGIConnection[Any, Any, Any, Any]], Coroutine[Any, Any, Any]]", msgpack_extractor ) elif kwargs_model.expected_data_dto: data_extractor = create_dto_extractor(data_dto=kwargs_model.expected_data_dto) else: data_extractor = cast( "Callable[[ASGIConnection[Any, Any, Any, Any]], Coroutine[Any, Any, Any]]", json_extractor ) async def extractor( values: dict[str, Any], connection: ASGIConnection[Any, Any, Any, Any], ) -> None: values["data"] = await data_extractor(connection) return extractor def create_dto_extractor( data_dto: type[AbstractDTO], ) -> Callable[[ASGIConnection[Any, Any, Any, Any]], Coroutine[Any, Any, Any]]: """Create a DTO data extractor. Returns: An extractor function. """ async def dto_extractor(connection: Request[Any, Any, Any]) -> Any: if not (body := await connection.body()): return Empty return data_dto(connection).decode_bytes(body) return dto_extractor # type:ignore[return-value] litestar-2.16.0/litestar/_kwargs/kwargs_model.py000066400000000000000000000477071500564371300217660ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from anyio import create_task_group from litestar._kwargs.cleanup import DependencyCleanupGroup from litestar._kwargs.dependencies import ( Dependency, create_dependency_batches, resolve_dependency, ) from litestar._kwargs.extractors import ( body_extractor, cookies_extractor, create_connection_value_extractor, create_data_extractor, headers_extractor, parse_connection_headers, parse_connection_query_params, query_extractor, request_extractor, scope_extractor, socket_extractor, state_extractor, ) from litestar._kwargs.parameter_definition import ( ParameterDefinition, create_parameter_definition, merge_parameter_sets, ) from litestar.constants import RESERVED_KWARGS from litestar.enums import ParamType, RequestEncodingType from litestar.exceptions import ImproperlyConfiguredException from litestar.params import BodyKwarg, ParameterKwarg from litestar.typing import FieldDefinition from litestar.utils.helpers import get_exception_group __all__ = ("KwargsModel",) if TYPE_CHECKING: from litestar._kwargs.types import Extractor from litestar._signature import SignatureModel from litestar.connection import ASGIConnection from litestar.di import Provide from litestar.dto import AbstractDTO from litestar.utils.signature import ParsedSignature _ExceptionGroup = get_exception_group() class KwargsModel: """Model required kwargs for a given RouteHandler and its dependencies. This is done once and is memoized during application bootstrap, ensuring minimal runtime overhead. """ __slots__ = ( "dependency_batches", "expected_cookie_params", "expected_data_dto", "expected_form_data", "expected_header_params", "expected_msgpack_data", "expected_path_params", "expected_query_params", "expected_reserved_kwargs", "extractors", "has_kwargs", "is_data_optional", "sequence_query_parameter_names", ) def __init__( self, *, expected_cookie_params: set[ParameterDefinition], expected_data_dto: type[AbstractDTO] | None, expected_dependencies: set[Dependency], expected_form_data: tuple[RequestEncodingType | str, FieldDefinition] | None, expected_header_params: set[ParameterDefinition], expected_msgpack_data: FieldDefinition | None, expected_path_params: set[ParameterDefinition], expected_query_params: set[ParameterDefinition], expected_reserved_kwargs: set[str], is_data_optional: bool, sequence_query_parameter_names: set[str], ) -> None: """Initialize ``KwargsModel``. Args: expected_cookie_params: Any expected cookie parameter kwargs expected_dependencies: Any expected dependency kwargs expected_form_data: Any expected form data kwargs expected_header_params: Any expected header parameter kwargs expected_msgpack_data: Any expected MessagePack data kwargs expected_path_params: Any expected path parameter kwargs expected_query_params: Any expected query parameter kwargs expected_reserved_kwargs: Any expected reserved kwargs, e.g. 'state' expected_data_dto: A data DTO, if defined is_data_optional: Treat data as optional sequence_query_parameter_names: Any query parameters that are sequences """ self.expected_cookie_params = expected_cookie_params self.expected_form_data = expected_form_data self.expected_header_params = expected_header_params self.expected_msgpack_data = expected_msgpack_data self.expected_path_params = expected_path_params self.expected_query_params = expected_query_params self.expected_reserved_kwargs = expected_reserved_kwargs self.expected_data_dto = expected_data_dto self.sequence_query_parameter_names = tuple(sequence_query_parameter_names) self.has_kwargs = ( expected_cookie_params or expected_dependencies or expected_form_data or expected_msgpack_data or expected_header_params or expected_path_params or expected_query_params or expected_reserved_kwargs or expected_data_dto ) self.is_data_optional = is_data_optional self.extractors: list[Extractor] = self._create_extractors() self.dependency_batches = create_dependency_batches(expected_dependencies) def _create_extractors(self) -> list[Extractor]: reserved_kwargs_extractors: dict[str, Extractor] = { "data": create_data_extractor(self), "state": state_extractor, "scope": scope_extractor, "request": request_extractor, "socket": socket_extractor, "headers": headers_extractor, "cookies": cookies_extractor, "query": query_extractor, "body": body_extractor, # type: ignore[dict-item] } extractors: list[Extractor] = [ reserved_kwargs_extractors[reserved_kwarg] for reserved_kwarg in self.expected_reserved_kwargs ] if self.expected_header_params: extractors.append( create_connection_value_extractor( connection_key="headers", expected_params=self.expected_header_params, kwargs_model=self, parser=parse_connection_headers, ), ) if self.expected_path_params: extractors.append( create_connection_value_extractor( connection_key="path_params", expected_params=self.expected_path_params, kwargs_model=self, ), ) if self.expected_cookie_params: extractors.append( create_connection_value_extractor( connection_key="cookies", expected_params=self.expected_cookie_params, kwargs_model=self, ), ) if self.expected_query_params: extractors.append( create_connection_value_extractor( connection_key="query_params", expected_params=self.expected_query_params, kwargs_model=self, parser=parse_connection_query_params, ), ) return extractors @classmethod def _get_param_definitions( cls, path_parameters: set[str], layered_parameters: dict[str, FieldDefinition], dependencies: dict[str, Provide], field_definitions: dict[str, FieldDefinition], ) -> tuple[set[ParameterDefinition], set[Dependency]]: """Get parameter_definitions for the construction of KwargsModel instance. Args: path_parameters: Any expected path parameters. layered_parameters: A string keyed dictionary of layered parameters. dependencies: A string keyed dictionary mapping dependency providers. field_definitions: The SignatureModel fields. Returns: A Tuple of sets """ expected_dependencies = { cls._create_dependency_graph(key=key, dependencies=dependencies) for key in dependencies if key in field_definitions } ignored_keys = {*RESERVED_KWARGS, *(dependency.key for dependency in expected_dependencies)} param_definitions = { *( create_parameter_definition( field_definition=field_definition, field_name=field_name, path_parameters=path_parameters, ) for field_name, field_definition in layered_parameters.items() if field_name not in ignored_keys and field_name not in field_definitions ), *( create_parameter_definition( field_definition=field_definition, field_name=field_name, path_parameters=path_parameters, ) for field_name, field_definition in field_definitions.items() if field_name not in ignored_keys and field_name not in layered_parameters ), } for field_name, field_definition in ( (k, v) for k, v in field_definitions.items() if k not in ignored_keys and k in layered_parameters ): layered_parameter = layered_parameters[field_name] field = field_definition if field_definition.is_parameter_field else layered_parameter default = field_definition.default if field_definition.has_default else layered_parameter.default param_definitions.add( create_parameter_definition( field_definition=FieldDefinition.from_kwarg( name=field.name, default=default, inner_types=field.inner_types, annotation=field.annotation, kwarg_definition=field.kwarg_definition, extra=field.extra, ), field_name=field_name, path_parameters=path_parameters, ) ) return param_definitions, expected_dependencies @classmethod def create_for_signature_model( cls, signature_model: type[SignatureModel], parsed_signature: ParsedSignature, dependencies: dict[str, Provide], path_parameters: set[str], layered_parameters: dict[str, FieldDefinition], ) -> KwargsModel: """Pre-determine what parameters are required for a given combination of route + route handler. It is executed during the application bootstrap process. Args: signature_model: A :class:`SignatureModel ` subclass. parsed_signature: A :class:`ParsedSignature ` instance. dependencies: A string keyed dictionary mapping dependency providers. path_parameters: Any expected path parameters. layered_parameters: A string keyed dictionary of layered parameters. Returns: An instance of KwargsModel """ field_definitions = signature_model._fields cls._validate_raw_kwargs( path_parameters=path_parameters, dependencies=dependencies, field_definitions=field_definitions, layered_parameters=layered_parameters, ) param_definitions, expected_dependencies = cls._get_param_definitions( path_parameters=path_parameters, layered_parameters=layered_parameters, dependencies=dependencies, field_definitions=field_definitions, ) expected_reserved_kwargs = {field_name for field_name in field_definitions if field_name in RESERVED_KWARGS} expected_path_parameters = {p for p in param_definitions if p.param_type == ParamType.PATH} expected_header_parameters = {p for p in param_definitions if p.param_type == ParamType.HEADER} expected_cookie_parameters = {p for p in param_definitions if p.param_type == ParamType.COOKIE} expected_query_parameters = {p for p in param_definitions if p.param_type == ParamType.QUERY} sequence_query_parameter_names = {p.field_alias for p in expected_query_parameters if p.is_sequence} expected_form_data: tuple[RequestEncodingType | str, FieldDefinition] | None = None expected_msgpack_data: FieldDefinition | None = None expected_data_dto: type[AbstractDTO] | None = None data_field_definition = field_definitions.get("data") media_type: RequestEncodingType | str | None = None if data_field_definition: if isinstance(data_field_definition.kwarg_definition, BodyKwarg): media_type = data_field_definition.kwarg_definition.media_type if media_type in (RequestEncodingType.MULTI_PART, RequestEncodingType.URL_ENCODED): expected_form_data = (media_type, data_field_definition) expected_data_dto = signature_model._data_dto elif signature_model._data_dto: expected_data_dto = signature_model._data_dto elif media_type == RequestEncodingType.MESSAGEPACK: expected_msgpack_data = data_field_definition for dependency in expected_dependencies: dependency_kwargs_model = cls.create_for_signature_model( signature_model=dependency.provide.signature_model, parsed_signature=parsed_signature, dependencies=dependencies, path_parameters=path_parameters, layered_parameters=layered_parameters, ) expected_path_parameters = merge_parameter_sets( expected_path_parameters, dependency_kwargs_model.expected_path_params ) expected_query_parameters = merge_parameter_sets( expected_query_parameters, dependency_kwargs_model.expected_query_params ) expected_cookie_parameters = merge_parameter_sets( expected_cookie_parameters, dependency_kwargs_model.expected_cookie_params ) expected_header_parameters = merge_parameter_sets( expected_header_parameters, dependency_kwargs_model.expected_header_params ) if "data" in expected_reserved_kwargs and "data" in dependency_kwargs_model.expected_reserved_kwargs: cls._validate_dependency_data( expected_form_data=expected_form_data, dependency_kwargs_model=dependency_kwargs_model, ) expected_reserved_kwargs.update(dependency_kwargs_model.expected_reserved_kwargs) sequence_query_parameter_names.update(dependency_kwargs_model.sequence_query_parameter_names) return KwargsModel( expected_cookie_params=expected_cookie_parameters, expected_dependencies=expected_dependencies, expected_data_dto=expected_data_dto, expected_form_data=expected_form_data, expected_header_params=expected_header_parameters, expected_msgpack_data=expected_msgpack_data, expected_path_params=expected_path_parameters, expected_query_params=expected_query_parameters, expected_reserved_kwargs=expected_reserved_kwargs, is_data_optional=field_definitions["data"].is_optional if "data" in expected_reserved_kwargs else False, sequence_query_parameter_names=sequence_query_parameter_names, ) async def to_kwargs(self, connection: ASGIConnection) -> dict[str, Any]: """Return a dictionary of kwargs. Async values, i.e. CoRoutines, are not resolved to ensure this function is sync. Args: connection: An instance of :class:`Request ` or :class:`WebSocket `. Returns: A string keyed dictionary of kwargs expected by the handler function and its dependencies. """ output: dict[str, Any] = {} for extractor in self.extractors: await extractor(output, connection) return output async def resolve_dependencies(self, connection: ASGIConnection, kwargs: dict[str, Any]) -> DependencyCleanupGroup: """Resolve all dependencies into the kwargs, recursively. Args: connection: An instance of :class:`Request ` or :class:`WebSocket `. kwargs: Kwargs to pass to dependencies. """ cleanup_group = DependencyCleanupGroup() for batch in self.dependency_batches: if len(batch) == 1: await resolve_dependency(next(iter(batch)), connection, kwargs, cleanup_group) else: try: async with create_task_group() as task_group: for dependency in batch: task_group.start_soon(resolve_dependency, dependency, connection, kwargs, cleanup_group) except _ExceptionGroup as excgroup: raise excgroup.exceptions[0] from excgroup # type: ignore[attr-defined] return cleanup_group @classmethod def _create_dependency_graph(cls, key: str, dependencies: dict[str, Provide]) -> Dependency: """Create a graph like structure of dependencies, with each dependency including its own dependencies as a list. """ provide = dependencies[key] sub_dependency_keys = [k for k in provide.signature_model._fields if k in dependencies] return Dependency( key=key, provide=provide, dependencies=[cls._create_dependency_graph(key=k, dependencies=dependencies) for k in sub_dependency_keys], ) @classmethod def _validate_dependency_data( cls, expected_form_data: tuple[RequestEncodingType | str, FieldDefinition] | None, dependency_kwargs_model: KwargsModel, ) -> None: """Validate that the 'data' kwarg is compatible across dependencies.""" if bool(expected_form_data) != bool(dependency_kwargs_model.expected_form_data): raise ImproperlyConfiguredException( "Dependencies have incompatible 'data' kwarg types: one expects JSON and the other expects form-data" ) if expected_form_data and dependency_kwargs_model.expected_form_data: local_media_type = expected_form_data[0] dependency_media_type = dependency_kwargs_model.expected_form_data[0] if local_media_type != dependency_media_type: raise ImproperlyConfiguredException( "Dependencies have incompatible form-data encoding: one expects url-encoded and the other expects multi-part" ) @classmethod def _validate_raw_kwargs( cls, path_parameters: set[str], dependencies: dict[str, Provide], field_definitions: dict[str, FieldDefinition], layered_parameters: dict[str, FieldDefinition], ) -> None: """Validate that there are no ambiguous kwargs, that is, kwargs declared using the same key in different places. """ dependency_keys = set(dependencies.keys()) parameter_names = { *( k for k, f in field_definitions.items() if isinstance(f.kwarg_definition, ParameterKwarg) and (f.kwarg_definition.header or f.kwarg_definition.query or f.kwarg_definition.cookie) ), *list(layered_parameters.keys()), } intersection = ( path_parameters.intersection(dependency_keys) or path_parameters.intersection(parameter_names) or dependency_keys.intersection(parameter_names) ) if intersection: raise ImproperlyConfiguredException( f"Kwarg resolution ambiguity detected for the following keys: {', '.join(intersection)}. " f"Make sure to use distinct keys for your dependencies, path parameters, and aliased parameters." ) if used_reserved_kwargs := { *parameter_names, *path_parameters, *dependency_keys, }.intersection(RESERVED_KWARGS): raise ImproperlyConfiguredException( f"Reserved kwargs ({', '.join(RESERVED_KWARGS)}) cannot be used for dependencies and parameter arguments. " f"The following kwargs have been used: {', '.join(used_reserved_kwargs)}" ) litestar-2.16.0/litestar/_kwargs/parameter_definition.py000066400000000000000000000053701500564371300234660ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from litestar.enums import ParamType from litestar.params import ParameterKwarg if TYPE_CHECKING: from litestar.typing import FieldDefinition __all__ = ("ParameterDefinition", "create_parameter_definition", "merge_parameter_sets") class ParameterDefinition(NamedTuple): """Tuple defining a kwarg representing a request parameter.""" default: Any field_alias: str field_name: str is_required: bool is_sequence: bool param_type: ParamType def create_parameter_definition( field_definition: FieldDefinition, field_name: str, path_parameters: set[str], ) -> ParameterDefinition: """Create a ParameterDefinition for the given FieldDefinition. Args: field_definition: FieldDefinition instance. field_name: The field's name. path_parameters: A set of path parameter names. Returns: A ParameterDefinition tuple. """ default = field_definition.default if field_definition.has_default else None kwarg_definition = ( field_definition.kwarg_definition if isinstance(field_definition.kwarg_definition, ParameterKwarg) else None ) field_alias = kwarg_definition.query if kwarg_definition and kwarg_definition.query else field_name param_type = ParamType.QUERY if field_name in path_parameters: field_alias = field_name param_type = ParamType.PATH elif kwarg_definition and kwarg_definition.header: field_alias = kwarg_definition.header param_type = ParamType.HEADER elif kwarg_definition and kwarg_definition.cookie: field_alias = kwarg_definition.cookie param_type = ParamType.COOKIE return ParameterDefinition( param_type=param_type, field_name=field_name, field_alias=field_alias, default=default, is_required=field_definition.is_required and default is None and not field_definition.is_optional and not field_definition.is_any, is_sequence=field_definition.is_non_string_sequence, ) def merge_parameter_sets(first: set[ParameterDefinition], second: set[ParameterDefinition]) -> set[ParameterDefinition]: """Given two sets of parameter definitions, coming from different dependencies for example, merge them into a single set. """ result: set[ParameterDefinition] = first.intersection(second) difference = first.symmetric_difference(second) for param in difference: # add the param if it's either required or no-other param in difference is the same but required if param.is_required or not any(p.field_alias == param.field_alias and p.is_required for p in difference): result.add(param) return result litestar-2.16.0/litestar/_kwargs/types.py000066400000000000000000000004031500564371300204320ustar00rootroot00000000000000from __future__ import annotations from typing import Any, Awaitable, Callable, Dict from typing_extensions import TypeAlias from litestar.connection import ASGIConnection Extractor: TypeAlias = Callable[[Dict[str, Any], ASGIConnection], Awaitable[None]] litestar-2.16.0/litestar/_layers/000077500000000000000000000000001500564371300167205ustar00rootroot00000000000000litestar-2.16.0/litestar/_layers/__init__.py000066400000000000000000000000001500564371300210170ustar00rootroot00000000000000litestar-2.16.0/litestar/_layers/utils.py000066400000000000000000000024101500564371300204270ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Mapping, Sequence from litestar.datastructures.cookie import Cookie from litestar.datastructures.response_header import ResponseHeader __all__ = ("narrow_response_cookies", "narrow_response_headers") if TYPE_CHECKING: from litestar.types.composite_types import ResponseCookies, ResponseHeaders def narrow_response_headers(headers: ResponseHeaders | None) -> Sequence[ResponseHeader] | None: """Given :class:`.types.ResponseHeaders` as a :class:`typing.Mapping`, create a list of :class:`.datastructures.response_header.ResponseHeader` from it, otherwise return ``headers`` unchanged """ return ( tuple(ResponseHeader(name=name, value=value) for name, value in headers.items()) if isinstance(headers, Mapping) else headers ) def narrow_response_cookies(cookies: ResponseCookies | None) -> Sequence[Cookie] | None: """Given :class:`.types.ResponseCookies` as a :class:`typing.Mapping`, create a list of :class:`.datastructures.cookie.Cookie` from it, otherwise return ``cookies`` unchanged """ return ( tuple(Cookie(key=key, value=value) for key, value in cookies.items()) if isinstance(cookies, Mapping) else cookies ) litestar-2.16.0/litestar/_multipart.py000066400000000000000000000115461500564371300200230ustar00rootroot00000000000000from __future__ import annotations import re from collections import defaultdict from typing import TYPE_CHECKING, Any, AsyncGenerator from multipart import ( # type: ignore[import-untyped] MultipartSegment, ParserError, ParserLimitReached, PushMultipartParser, ) from litestar.datastructures.upload_file import UploadFile from litestar.exceptions import ClientException __all__ = ("parse_content_header", "parse_multipart_form") from litestar.utils.compat import async_next if TYPE_CHECKING: from litestar.types import TypeDecodersSequence _token = r"([\w!#$%&'*+\-.^_`|~]+)" # noqa: S105 _quoted = r'"([^"]*)"' _param = re.compile(rf";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII) _firefox_quote_escape = re.compile(r'\\"(?!; |\s*$)') def parse_content_header(value: str) -> tuple[str, dict[str, str]]: """Parse content-type and content-disposition header values. Args: value: A header string value to parse. Returns: A tuple containing the normalized header string and a dictionary of parameters. """ value = _firefox_quote_escape.sub("%22", value) pos = value.find(";") if pos == -1: options: dict[str, str] = {} else: options = { m.group(1).lower(): m.group(2) or m.group(3).replace("%22", '"') for m in _param.finditer(value[pos:]) } value = value[:pos] return value.strip().lower(), options async def _close_upload_files(fields: dict[str, list[Any]]) -> None: for values in fields.values(): for value in values: if isinstance(value, UploadFile): await value.close() async def parse_multipart_form( # noqa: C901 stream: AsyncGenerator[bytes, None], boundary: bytes, multipart_form_part_limit: int = 1000, type_decoders: TypeDecodersSequence | None = None, ) -> dict[str, Any]: """Parse multipart form data. Args: stream: Body of the request. boundary: Boundary of the multipart message. multipart_form_part_limit: Limit of the number of parts allowed. type_decoders: A sequence of type decoders to use. Returns: A dictionary of parsed results. """ fields: defaultdict[str, list[Any]] = defaultdict(list) chunk = await async_next(stream, b"") if not chunk: return fields data: UploadFile | bytearray = bytearray() try: with PushMultipartParser(boundary, max_segment_count=multipart_form_part_limit) as parser: segment: MultipartSegment | None = None while not parser.closed: for form_part in parser.parse(chunk): if isinstance(form_part, MultipartSegment): segment = form_part if segment.filename: data = UploadFile( content_type=segment.content_type or "text/plain", filename=segment.filename, headers=dict(segment.headerlist), ) elif form_part: if isinstance(data, UploadFile): await data.write(form_part) else: data.extend(form_part) else: # end of part if segment is None: # we have reached the end of a segment before we have # received a complete header segment raise ClientException("Unexpected eof in multipart/form-data") if isinstance(data, UploadFile): await data.seek(0) fields[segment.name].append(data) elif data: fields[segment.name].append(data.decode(segment.charset or "utf-8")) else: fields[segment.name].append(None) # reset for next part data = bytearray() segment = None chunk = await async_next(stream, b"") except ParserError as exc: # if an exception is raised, make sure that all 'UploadFile's are closed if isinstance(data, UploadFile): await data.close() await _close_upload_files(fields) raise ClientException("Invalid multipart/form-data") from exc except ParserLimitReached: if isinstance(data, UploadFile): await data.close() await _close_upload_files(fields) # FIXME (3.0): This should raise a '413 - Request Entity Too Large', but for # backwards compatibility, we keep it as a 400 for now raise ClientException("Request Entity Too Large") from None return {k: v if len(v) > 1 else v[0] for k, v in fields.items()} litestar-2.16.0/litestar/_openapi/000077500000000000000000000000001500564371300170545ustar00rootroot00000000000000litestar-2.16.0/litestar/_openapi/__init__.py000066400000000000000000000000001500564371300211530ustar00rootroot00000000000000litestar-2.16.0/litestar/_openapi/datastructures.py000066400000000000000000000232331500564371300225060ustar00rootroot00000000000000from __future__ import annotations import re from collections import defaultdict from typing import TYPE_CHECKING, Iterator, Sequence, _GenericAlias # type: ignore[attr-defined] from litestar.exceptions import ImproperlyConfiguredException from litestar.openapi.spec import Reference, Schema from litestar.params import KwargDefinition if TYPE_CHECKING: from litestar.openapi import OpenAPIConfig from litestar.plugins import OpenAPISchemaPluginProtocol from litestar.typing import FieldDefinition INVALID_KEY_CHARACTER_PATTERN = re.compile(r"[^a-zA-Z0-9._-]+") def _longest_common_prefix(tuples_: list[tuple[str, ...]]) -> tuple[str, ...]: """Find the longest common prefix of a list of tuples. Args: tuples_: A list of tuples to find the longest common prefix of. Returns: The longest common prefix of the tuples. """ prefix_ = tuples_[0] for t in tuples_: # Compare the current prefix with each tuple and shorten it prefix_ = prefix_[: min(len(prefix_), len(t))] for i in range(len(prefix_)): if prefix_[i] != t[i]: prefix_ = prefix_[:i] break return prefix_ def _get_component_key_override(field: FieldDefinition) -> str | None: if ( (kwarg_definition := field.kwarg_definition) and isinstance(kwarg_definition, KwargDefinition) and (schema_key := kwarg_definition.schema_component_key) ): return schema_key return None def _get_normalized_schema_key(field_definition: FieldDefinition) -> tuple[str, ...]: """Create a key for a type annotation. The key should be a tuple such as ``("path", "to", "type", "TypeName")``. Args: field_definition: Field definition Returns: A tuple of strings. """ if override := _get_component_key_override(field_definition): return (override,) annotation = field_definition.annotation module = getattr(annotation, "__module__", "") name = str(annotation)[len(module) + 1 :] if isinstance(annotation, _GenericAlias) else annotation.__qualname__ name = name.replace("..", ".") return *module.split("."), re.sub(INVALID_KEY_CHARACTER_PATTERN, "_", name) class RegisteredSchema: """Object to store a schema and any references to it.""" def __init__(self, key: tuple[str, ...], schema: Schema, references: list[Reference]) -> None: """Create a new RegisteredSchema object. Args: key: The key used to register the schema. schema: The schema object. references: A list of references to the schema. """ self.key = key self.schema = schema self.references = references class SchemaRegistry: """A registry for object schemas. This class is used to store schemas that we reference from other parts of the spec. Its main purpose is to allow us to generate the components/schemas section of the spec once we have collected all the schemas that should be included. This allows us to determine a path to the schema in the components/schemas section of the spec that is unique and as short as possible. """ def __init__(self) -> None: self._schema_key_map: dict[tuple[str, ...], RegisteredSchema] = {} self._schema_reference_map: dict[int, RegisteredSchema] = {} self._model_name_groups: defaultdict[str, list[RegisteredSchema]] = defaultdict(list) self._component_type_map: dict[tuple[str, ...], FieldDefinition] = {} def get_schema_for_field_definition(self, field: FieldDefinition) -> Schema: """Get a registered schema by its key. Args: field: The field definition to get the schema for Returns: A RegisteredSchema object. """ key = _get_normalized_schema_key(field) if key not in self._schema_key_map: self._schema_key_map[key] = registered_schema = RegisteredSchema(key, Schema(), []) self._model_name_groups[key[-1]].append(registered_schema) self._component_type_map[key] = field else: if (existing_type := self._component_type_map[key]) != field: raise ImproperlyConfiguredException( f"Schema component keys must be unique. Cannot override existing key {'_'.join(key)!r} for type " f"{existing_type.raw!r} with new type {field.raw!r}" ) return self._schema_key_map[key].schema def get_reference_for_field_definition(self, field: FieldDefinition) -> Reference | None: """Get a reference to a registered schema by its key. Args: field: The field definition to get the reference for Returns: A Reference object. """ key = _get_normalized_schema_key(field) if key not in self._schema_key_map: return None if (existing_type := self._component_type_map[key]) != field: # TODO: This should check for strict equality, e.g. changes in type metadata # However, this is currently not possible to do without breaking things, as # we allow to define metadata on a type annotation in one place to be used # for the same type in a different place, where that same type is *not* # annotated with this metadata. The proper fix for this would be to e.g. # inline DTO definitions when they are created at the handler level, as # they won't be reused (they already generate a unique key), and create a # more strict lookup policy for component schemas msg = ( f"Schema component keys must be unique. While obtaining a reference for the type '{field.raw!r}', the " f"generated key {'_'.join(key)!r} was already associated with a different type '{existing_type.raw!r}'. " ) if key_override := _get_component_key_override(field): # pragma: no branch # Currently, this can never not be true, however, in the future we might # decide to do a stricter equality check as lined out above, in which # case there can be other cases than overrides that cause this error msg += f"Hint: Both types are defining a 'schema_component_key' with the value of {key_override!r}" raise ImproperlyConfiguredException(msg) registered_schema = self._schema_key_map[key] reference = Reference(f"#/components/schemas/{'_'.join(key)}") registered_schema.references.append(reference) self._schema_reference_map[id(reference)] = registered_schema return reference def from_reference(self, reference: Reference) -> RegisteredSchema: """Get a registered schema by its reference. Args: reference: The reference to the schema to get. Returns: A RegisteredSchema object. """ return self._schema_reference_map[id(reference)] def __iter__(self) -> Iterator[RegisteredSchema]: """Iterate over the registered schemas.""" return iter(self._schema_key_map.values()) @staticmethod def set_reference_paths(name: str, registered_schema: RegisteredSchema) -> None: """Set the reference paths for a registered schema.""" for reference in registered_schema.references: reference.ref = f"#/components/schemas/{name}" @staticmethod def remove_common_prefix(tuples: list[tuple[str, ...]]) -> list[tuple[str, ...]]: """Remove the common prefix from a list of tuples. Args: tuples: A list of tuples to remove the common prefix from. Returns: A list of tuples with the common prefix removed. """ prefix = _longest_common_prefix(tuples) prefix_length = len(prefix) return [t[prefix_length:] for t in tuples] def generate_components_schemas(self) -> dict[str, Schema]: """Generate the components/schemas section of the spec. Returns: A dictionary of schemas. """ components_schemas: dict[str, Schema] = {} for name, name_group in self._model_name_groups.items(): if len(name_group) == 1: self.set_reference_paths(name, name_group[0]) components_schemas[name] = name_group[0].schema continue full_keys = [registered_schema.key for registered_schema in name_group] names = ["_".join(k) for k in self.remove_common_prefix(full_keys)] for name_, registered_schema in zip(names, name_group): self.set_reference_paths(name_, registered_schema) components_schemas[name_] = registered_schema.schema # Sort them by name to ensure they're always generated in the same order. return {name: components_schemas[name] for name in sorted(components_schemas.keys())} class OpenAPIContext: def __init__( self, openapi_config: OpenAPIConfig, plugins: Sequence[OpenAPISchemaPluginProtocol], ) -> None: self.openapi_config = openapi_config self.plugins = plugins self.operation_ids: set[str] = set() self.schema_registry = SchemaRegistry() def add_operation_id(self, operation_id: str) -> None: """Add an operation ID to the context. Args: operation_id: Operation ID to add. """ if operation_id in self.operation_ids: raise ImproperlyConfiguredException( "operation_ids must be unique, " f"please ensure the value of 'operation_id' is either not set or unique for {operation_id}" ) self.operation_ids.add(operation_id) litestar-2.16.0/litestar/_openapi/parameters.py000066400000000000000000000251061500564371300215750ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar._openapi.schema_generation import SchemaCreator from litestar._openapi.schema_generation.utils import get_formatted_examples from litestar.constants import RESERVED_KWARGS from litestar.enums import ParamType from litestar.exceptions import ImproperlyConfiguredException from litestar.openapi.spec.parameter import Parameter from litestar.openapi.spec.schema import Schema from litestar.params import DependencyKwarg, ParameterKwarg from litestar.types import Empty from litestar.typing import FieldDefinition if TYPE_CHECKING: from litestar._openapi.datastructures import OpenAPIContext from litestar.handlers.base import BaseRouteHandler from litestar.openapi.spec import Reference from litestar.types.internal_types import PathParameterDefinition __all__ = ("create_parameters_for_handler",) class ParameterCollection: """Facilitates conditional deduplication of parameters. If multiple parameters with the same name are produced for a handler, the condition is ignored if the two ``Parameter`` instances are the same (the first is retained and any duplicates are ignored). If the ``Parameter`` instances are not the same, an exception is raised. """ def __init__(self, route_handler: BaseRouteHandler) -> None: """Initialize ``ParameterCollection``. Args: route_handler: Associated route handler """ self.route_handler = route_handler self._parameters: dict[tuple[str, str], Parameter] = {} def add(self, parameter: Parameter) -> None: """Add a ``Parameter`` to the collection. If an existing parameter with the same name and type already exists, the parameter is ignored. If an existing parameter with the same name but different type exists, raises ``ImproperlyConfiguredException``. """ if (parameter.name, parameter.param_in) not in self._parameters: # because we are defining routes as unique per path, we have to handle here a situation when there is an optional # path parameter. e.g. get(path=["/", "/{param:str}"]). When parsing the parameter for path, the route handler # would still have a kwarg called param: # def handler(param: str | None) -> ... if parameter.param_in != ParamType.QUERY or all( f"{{{parameter.name}:" not in path for path in self.route_handler.paths ): self._parameters[(parameter.name, parameter.param_in)] = parameter return pre_existing = self._parameters[(parameter.name, parameter.param_in)] if parameter == pre_existing: return raise ImproperlyConfiguredException( f"OpenAPI schema generation for handler `{self.route_handler}` detected multiple parameters named " f"'{parameter.name}' with different types." ) def list(self) -> list[Parameter]: """Return a list of all ``Parameter``'s in the collection.""" return list(self._parameters.values()) class ParameterFactory: """Factory for creating OpenAPI Parameters for a given route handler.""" def __init__( self, context: OpenAPIContext, route_handler: BaseRouteHandler, path_parameters: dict[str, PathParameterDefinition], ) -> None: """Initialize ParameterFactory. Args: context: The OpenAPI context. route_handler: The route handler. path_parameters: The path parameters for the route. """ self.context = context self.schema_creator = SchemaCreator.from_openapi_context(self.context, prefer_alias=True) self.route_handler = route_handler self.parameters = ParameterCollection(route_handler) self.dependency_providers = route_handler.resolve_dependencies() self.layered_parameters = route_handler.resolve_layered_parameters() self.path_parameters = path_parameters def create_parameter(self, field_definition: FieldDefinition, parameter_name: str) -> Parameter: """Create an OpenAPI Parameter instance for a field definition. Args: field_definition: The field definition. parameter_name: The name of the parameter. """ result: Schema | Reference | None = None kwarg_definition = ( field_definition.kwarg_definition if isinstance(field_definition.kwarg_definition, ParameterKwarg) else None ) if parameter_name in self.path_parameters: param_in = ParamType.PATH is_required = True result = self.schema_creator.for_field_definition(field_definition) elif kwarg_definition and kwarg_definition.header: parameter_name = kwarg_definition.header param_in = ParamType.HEADER is_required = field_definition.is_required elif kwarg_definition and kwarg_definition.cookie: parameter_name = kwarg_definition.cookie param_in = ParamType.COOKIE is_required = field_definition.is_required else: is_required = field_definition.is_required param_in = ParamType.QUERY parameter_name = kwarg_definition.query if kwarg_definition and kwarg_definition.query else parameter_name if not result: result = self.schema_creator.for_field_definition(field_definition) schema = result if isinstance(result, Schema) else self.context.schema_registry.from_reference(result).schema examples_list = kwarg_definition.examples or [] if kwarg_definition else [] examples = get_formatted_examples(field_definition, examples_list) return Parameter( description=schema.description, name=parameter_name, param_in=param_in, required=is_required, schema=result, examples=examples or None, ) def get_layered_parameter(self, field_name: str, field_definition: FieldDefinition) -> Parameter: """Create a parameter for a field definition that has a KwargDefinition defined on the layers. Args: field_name: The name of the field. field_definition: The field definition. """ layer_field = self.layered_parameters[field_name] field = field_definition if field_definition.is_parameter_field else layer_field default = layer_field.default if field_definition.has_default else field_definition.default annotation = field_definition.annotation if field_definition is not Empty else layer_field.annotation parameter_name = field_name if isinstance(field.kwarg_definition, ParameterKwarg): parameter_name = ( field.kwarg_definition.query or field.kwarg_definition.header or field.kwarg_definition.cookie or field_name ) field_definition = FieldDefinition.from_kwarg( inner_types=field.inner_types, default=default, extra=field.extra, annotation=annotation, kwarg_definition=field.kwarg_definition, name=field_name, ) return self.create_parameter(field_definition=field_definition, parameter_name=parameter_name) def create_parameters_for_field_definitions(self, fields: dict[str, FieldDefinition]) -> None: """Add Parameter models to the handler's collection for the given field definitions. Args: fields: The field definitions. """ unique_handler_fields = ( (k, v) for k, v in fields.items() if k not in RESERVED_KWARGS and k not in self.layered_parameters ) unique_layered_fields = ( (k, v) for k, v in self.layered_parameters.items() if k not in RESERVED_KWARGS and k not in fields ) intersection_fields = ( (k, v) for k, v in fields.items() if k not in RESERVED_KWARGS and k in self.layered_parameters ) for field_name, field_definition in unique_handler_fields: if ( isinstance(field_definition.kwarg_definition, DependencyKwarg) and field_name not in self.dependency_providers ): # never document explicit dependencies continue if provider := self.dependency_providers.get(field_name): self.create_parameters_for_field_definitions(fields=provider.parsed_fn_signature.parameters) else: self.parameters.add(self.create_parameter(field_definition=field_definition, parameter_name=field_name)) for field_name, field_definition in unique_layered_fields: self.parameters.add(self.create_parameter(field_definition=field_definition, parameter_name=field_name)) for field_name, field_definition in intersection_fields: self.parameters.add(self.get_layered_parameter(field_name=field_name, field_definition=field_definition)) def create_parameters_for_handler(self) -> list[Parameter]: """Create a list of path/query/header Parameter models for the given PathHandler.""" handler_fields = self.route_handler.parsed_fn_signature.parameters # not all path parameters have to be consumed by the handler. Because even not # consumed path parameters must still be specified, we create stub parameters # for the unconsumed ones so a correct OpenAPI schema can be generated dependency_fields = { name for dep in self.dependency_providers.values() for name in dep.parsed_fn_signature.parameters } params_not_consumed_by_handler = set(self.path_parameters) - handler_fields.keys() unconsumed_path_parameters = params_not_consumed_by_handler - dependency_fields handler_fields.update( { param_name: FieldDefinition.from_kwarg(self.path_parameters[param_name].type, name=param_name) for param_name in unconsumed_path_parameters } ) self.create_parameters_for_field_definitions(handler_fields) return self.parameters.list() def create_parameters_for_handler( context: OpenAPIContext, route_handler: BaseRouteHandler, path_parameters: dict[str, PathParameterDefinition], ) -> list[Parameter]: """Create a list of path/query/header Parameter models for the given PathHandler.""" factory = ParameterFactory( context=context, route_handler=route_handler, path_parameters=path_parameters, ) return factory.create_parameters_for_handler() litestar-2.16.0/litestar/_openapi/path_item.py000066400000000000000000000152671500564371300214130ustar00rootroot00000000000000from __future__ import annotations import dataclasses from inspect import cleandoc from typing import TYPE_CHECKING from litestar._openapi.parameters import create_parameters_for_handler from litestar._openapi.request_body import create_request_body from litestar._openapi.responses import create_responses_for_handler from litestar._openapi.utils import SEPARATORS_CLEANUP_PATTERN from litestar.enums import HttpMethod from litestar.exceptions import ImproperlyConfiguredException from litestar.openapi.spec import Operation, PathItem from litestar.utils.helpers import unwrap_partial if TYPE_CHECKING: from litestar._openapi.datastructures import OpenAPIContext from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.routes import HTTPRoute __all__ = ("create_path_item_for_route", "merge_path_item_operations") class PathItemFactory: """Factory for creating a PathItem instance for a given route.""" def __init__(self, openapi_context: OpenAPIContext, route: HTTPRoute) -> None: self.context = openapi_context self.route = route self._path_item = PathItem() def create_path_item(self) -> PathItem: """Create a PathItem for the given route parsing all http_methods into Operation Models. Returns: A PathItem instance. """ for http_method, handler_tuple in self.route.route_handler_map.items(): route_handler, _ = handler_tuple if not route_handler.resolve_include_in_schema(): continue operation = self.create_operation_for_handler_method(route_handler, HttpMethod(http_method)) setattr(self._path_item, http_method.lower(), operation) return self._path_item def create_operation_for_handler_method( self, route_handler: HTTPRouteHandler, http_method: HttpMethod ) -> Operation: """Create an Operation instance for a given route handler and http method. Args: route_handler: A route handler instance. http_method: An HttpMethod enum value. Returns: An Operation instance. """ operation_id = self.create_operation_id(route_handler, http_method) parameters = create_parameters_for_handler(self.context, route_handler, self.route.path_parameters) signature_fields = route_handler.parsed_fn_signature.parameters request_body = None if data_field := signature_fields.get("data"): request_body = create_request_body( self.context, route_handler.handler_id, route_handler.resolve_data_dto(), data_field ) raises_validation_error = bool(data_field or self._path_item.parameters or parameters) responses = create_responses_for_handler( self.context, route_handler, raises_validation_error=raises_validation_error ) return route_handler.operation_class( operation_id=operation_id, tags=route_handler.resolve_tags() or None, summary=route_handler.summary or SEPARATORS_CLEANUP_PATTERN.sub("", route_handler.handler_name.title()), description=self.create_description_for_handler(route_handler), deprecated=route_handler.deprecated, responses=responses, request_body=request_body, parameters=parameters or None, # type: ignore[arg-type] security=route_handler.resolve_security() or None, ) def create_operation_id(self, route_handler: HTTPRouteHandler, http_method: HttpMethod) -> str: """Create an operation id for a given route handler and http method. Adds the operation id to the context's operation id set, where it is checked for uniqueness. Args: route_handler: A route handler instance. http_method: An HttpMethod enum value. Returns: An operation id string. """ if isinstance(route_handler.operation_id, str): operation_id = route_handler.operation_id elif callable(route_handler.operation_id): operation_id = route_handler.operation_id(route_handler, http_method, self.route.path_components) else: operation_id = self.context.openapi_config.operation_id_creator( route_handler, http_method, self.route.path_components ) self.context.add_operation_id(operation_id) return operation_id def create_description_for_handler(self, route_handler: HTTPRouteHandler) -> str | None: """Produce the operation description for a route handler. Args: route_handler: A route handler instance. Returns: An optional description string """ handler_description = route_handler.description if handler_description is None and self.context.openapi_config.use_handler_docstrings: fn = unwrap_partial(route_handler.fn) return cleandoc(fn.__doc__) if fn.__doc__ else None return handler_description def create_path_item_for_route(openapi_context: OpenAPIContext, route: HTTPRoute) -> PathItem: """Create a PathItem for the given route parsing all http_methods into Operation Models. Args: openapi_context: The OpenAPIContext instance. route: The route to create a PathItem for. Returns: A PathItem instance. """ path_item_factory = PathItemFactory(openapi_context, route) return path_item_factory.create_path_item() def merge_path_item_operations(source: PathItem, other: PathItem, for_path: str) -> PathItem: """Merge operations from path items, creating a new path item that includes operations from both. """ attrs_to_merge = {"get", "put", "post", "delete", "options", "head", "patch", "trace"} fields = {f.name for f in dataclasses.fields(PathItem)} - attrs_to_merge if any(getattr(source, attr) and getattr(other, attr) for attr in attrs_to_merge): raise ValueError("Cannot merge operation for PathItem if operation is set on both items") if differing_values := [ (value_a, value_b) for attr in fields if (value_a := getattr(source, attr)) != (value_b := getattr(other, attr)) ]: raise ImproperlyConfiguredException( f"Conflicting OpenAPI path configuration for {for_path!r}. " f"{', '.join(f'{a} != {b}' for a, b in differing_values)}" ) return dataclasses.replace( source, get=source.get or other.get, post=source.post or other.post, patch=source.patch or other.patch, put=source.put or other.put, delete=source.delete or other.delete, options=source.options or other.options, trace=source.trace or other.trace, ) litestar-2.16.0/litestar/_openapi/plugin.py000066400000000000000000000172141500564371300207310ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from litestar._openapi.datastructures import OpenAPIContext from litestar._openapi.path_item import create_path_item_for_route, merge_path_item_operations from litestar.constants import OPENAPI_JSON_HANDLER_NAME from litestar.enums import MediaType from litestar.exceptions import ImproperlyConfiguredException, NotFoundException from litestar.handlers import get from litestar.openapi.plugins import JsonRenderPlugin from litestar.plugins import InitPlugin from litestar.plugins.base import ReceiveRoutePlugin from litestar.response import Response from litestar.router import Router from litestar.routes import HTTPRoute from litestar.status_codes import HTTP_404_NOT_FOUND if TYPE_CHECKING: from litestar.app import Litestar from litestar.config.app import AppConfig from litestar.connection import Request from litestar.handlers import HTTPRouteHandler from litestar.openapi.config import OpenAPIConfig from litestar.openapi.plugins import OpenAPIRenderPlugin from litestar.openapi.spec import OpenAPI, PathItem from litestar.routes import BaseRoute def handle_schema_path_not_found(path: str = "/") -> Response: """Handler for returning HTML formatted errors from not-found schema paths. This preserves backward compatibility with the Controller-based OpenAPI implementation. """ if path.endswith((".json", ".yaml", ".yml")): raise NotFoundException content = b""" 404 Not found

Error 404

""" return Response(content, media_type=MediaType.HTML, status_code=HTTP_404_NOT_FOUND) class OpenAPIPlugin(InitPlugin, ReceiveRoutePlugin): __slots__ = ( "_openapi", "_openapi_config", "_openapi_schema", "app", "included_routes", ) def __init__(self, app: Litestar) -> None: self.app = app self.included_routes: dict[str, HTTPRoute] = {} self._openapi_config: OpenAPIConfig | None = None self._openapi: OpenAPI | None = None self._openapi_schema: dict[str, object] | None = None def _build_openapi(self) -> OpenAPI: openapi_config = self.openapi_config if openapi_config.create_examples: from litestar._openapi.schema_generation.examples import ExampleFactory ExampleFactory.seed_random(openapi_config.random_seed) openapi = openapi_config.to_openapi_schema() context = OpenAPIContext(openapi_config=openapi_config, plugins=self.app.plugins.openapi) path_items: dict[str, PathItem] = {} for route in self.included_routes.values(): path = route.path_format or "/" path_item = create_path_item_for_route(context, route) if existing_path_item := path_items.get(path): path_item = merge_path_item_operations(existing_path_item, path_item, for_path=path) path_items[path] = path_item openapi.paths = path_items openapi.components.schemas = context.schema_registry.generate_components_schemas() return openapi def provide_openapi(self) -> OpenAPI: if not self._openapi: self._openapi = self._build_openapi() return self._openapi def provide_openapi_schema(self) -> dict[str, Any]: if not self._openapi_schema: self._openapi_schema = self.provide_openapi().to_schema() return self._openapi_schema def create_openapi_router(self) -> Router: """Create a router for serving OpenAPI documentation and schema files. For each OpenAPI render plugin, a route is created to serve the plugin's documentation site. A handler is added for serving a 404 page for any schema path that is not configured by a plugin. A handler is added for serving the JSON OpenAPI schema file if it is not configured. For each plugin, the plugin's `receive_router` method is called with the router instance. Returns: The router. """ if (router := self.openapi_config.openapi_router) is None: router = Router( self.openapi_config.path or "/schema", route_handlers=[], include_in_schema=False, dto=None, return_dto=None, ) root_configured = False openapi_json_found = False def create_handler(plugin_: OpenAPIRenderPlugin) -> HTTPRouteHandler: """Create a handler for serving the plugin's documentation site. If the plugin is the default plugin, a handler is created for the root path in addition to the plugin's configured paths. If the plugin has a path for serving the OpenAPI schema file, the `openapi_json_found` flag is set to `True`, so that we don't create a handler for serving the JSON schema file. Args: plugin_: The plugin to create the handler for. Returns: The handler. """ paths = list(plugin_.paths) if plugin_ is self.openapi_config.default_plugin: if not plugin_.has_path("/"): paths.append("/") nonlocal root_configured root_configured = True handler_name = None if plugin_.has_path("/openapi.json"): nonlocal openapi_json_found openapi_json_found = True handler_name = OPENAPI_JSON_HANDLER_NAME @get(paths, media_type=plugin_.media_type, sync_to_thread=False, name=handler_name) def _handler(request: Request) -> bytes: return plugin_.render(request, self.provide_openapi_schema()) return _handler for plugin in self.openapi_config.render_plugins: router.register(create_handler(plugin)) not_found_handler_paths = ["/{path:str}"] if not root_configured: not_found_handler_paths.append("/") not_found_handler = get(not_found_handler_paths, media_type=MediaType.HTML, sync_to_thread=False)( handle_schema_path_not_found ) router.register(not_found_handler) if not openapi_json_found: router.register(create_handler(JsonRenderPlugin())) for plugin in self.openapi_config.render_plugins: plugin.receive_router(router) return router def on_app_init(self, app_config: AppConfig) -> AppConfig: if app_config.openapi_config: self._openapi_config = app_config.openapi_config if (controller := app_config.openapi_config.openapi_controller) is not None: app_config.route_handlers.append(controller) else: app_config.route_handlers.append(self.create_openapi_router()) return app_config @property def openapi_config(self) -> OpenAPIConfig: if not self._openapi_config: raise ImproperlyConfiguredException("OpenAPIConfig not initialized") return self._openapi_config def receive_route(self, route: BaseRoute) -> None: if not isinstance(route, HTTPRoute): return if any(route_handler.resolve_include_in_schema() for route_handler, _ in route.route_handler_map.values()): # Force recompute the schema if a new route is added self._openapi = None self.included_routes[route.path] = route litestar-2.16.0/litestar/_openapi/request_body.py000066400000000000000000000033061500564371300221350ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar._openapi.schema_generation import SchemaCreator from litestar.enums import RequestEncodingType from litestar.openapi.spec.media_type import OpenAPIMediaType from litestar.openapi.spec.request_body import RequestBody from litestar.params import BodyKwarg __all__ = ("create_request_body",) if TYPE_CHECKING: from litestar._openapi.datastructures import OpenAPIContext from litestar.dto import AbstractDTO from litestar.typing import FieldDefinition def create_request_body( context: OpenAPIContext, handler_id: str, resolved_data_dto: type[AbstractDTO] | None, data_field: FieldDefinition, ) -> RequestBody: """Create a RequestBody instance for the given route handler's data field. Args: context: The OpenAPIContext instance. handler_id: The handler id. resolved_data_dto: The resolved data dto. data_field: The data field. Returns: A RequestBody instance. """ media_type: RequestEncodingType | str = RequestEncodingType.JSON schema_creator = SchemaCreator.from_openapi_context(context, prefer_alias=True) if isinstance(data_field.kwarg_definition, BodyKwarg) and data_field.kwarg_definition.media_type: media_type = data_field.kwarg_definition.media_type if resolved_data_dto: schema = resolved_data_dto.create_openapi_schema( field_definition=data_field, handler_id=handler_id, schema_creator=schema_creator, ) else: schema = schema_creator.for_field_definition(data_field) return RequestBody(required=True, content={media_type: OpenAPIMediaType(schema=schema)}) litestar-2.16.0/litestar/_openapi/responses.py000066400000000000000000000327471500564371300214640ustar00rootroot00000000000000from __future__ import annotations import contextlib import re from copy import copy from dataclasses import asdict from http import HTTPStatus from operator import attrgetter from typing import TYPE_CHECKING, Any, Iterator from litestar._openapi.schema_generation import SchemaCreator from litestar._openapi.schema_generation.utils import get_formatted_examples from litestar.enums import MediaType from litestar.exceptions import HTTPException, ValidationException from litestar.openapi.spec import Example, OpenAPIResponse, Reference from litestar.openapi.spec.enums import OpenAPIFormat, OpenAPIType from litestar.openapi.spec.header import OpenAPIHeader from litestar.openapi.spec.media_type import OpenAPIMediaType from litestar.openapi.spec.schema import Schema from litestar.response import ( File, Redirect, Stream, Template, ) from litestar.response import ( Response as LitestarResponse, ) from litestar.response.base import ASGIResponse from litestar.types.builtin_types import NoneType from litestar.typing import FieldDefinition from litestar.utils import get_enum_string_value, get_name if TYPE_CHECKING: from litestar._openapi.datastructures import OpenAPIContext from litestar.datastructures.cookie import Cookie from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.openapi.spec.responses import Responses __all__ = ("create_responses_for_handler",) CAPITAL_LETTERS_PATTERN = re.compile(r"(?=[A-Z])") def pascal_case_to_text(string: str) -> str: """Given a 'PascalCased' string, return its split form- 'Pascal Cased'.""" return " ".join(re.split(CAPITAL_LETTERS_PATTERN, string)).strip() def create_cookie_schema(cookie: Cookie) -> Schema: """Given a Cookie instance, return its corresponding OpenAPI schema. Args: cookie: Cookie Returns: Schema """ cookie_copy = copy(cookie) cookie_copy.value = "" value = cookie_copy.to_header(header="") return Schema(description=cookie.description or "", example=value) class ResponseFactory: """Factory for creating a Response instance for a given route handler.""" def __init__(self, context: OpenAPIContext, route_handler: HTTPRouteHandler) -> None: """Initialize the factory. Args: context: An OpenAPIContext instance. route_handler: An HTTPRouteHandler instance. """ self.context = context self.route_handler = route_handler self.field_definition = route_handler.parsed_fn_signature.return_type self.schema_creator = SchemaCreator.from_openapi_context(context, prefer_alias=False) def create_responses(self, raises_validation_error: bool) -> Responses | None: """Create the schema for responses, if any. Args: raises_validation_error: Boolean flag indicating whether the handler raises a ValidationException. Returns: Responses """ responses: Responses = { str(self.route_handler.status_code): self.create_success_response(), } exceptions = list(self.route_handler.raises or []) if raises_validation_error and ValidationException not in exceptions: exceptions.append(ValidationException) for status_code, response in create_error_responses(exceptions=exceptions): responses[status_code] = response for status_code, response in self.create_additional_responses(): responses[status_code] = response return responses or None def create_description(self) -> str: """Create the description for a success response.""" default_descriptions: dict[Any, str] = { Stream: "Stream Response", Redirect: "Redirect Response", File: "File Download", } return ( self.route_handler.response_description or default_descriptions.get(self.field_definition.annotation) or HTTPStatus(self.route_handler.status_code).description ) def create_success_response(self) -> OpenAPIResponse: """Create the schema for a success response.""" if self.field_definition.is_subclass_of((NoneType, ASGIResponse)): response = OpenAPIResponse(content=None, description=self.create_description()) elif self.field_definition.is_subclass_of(Redirect): response = self.create_redirect_response() elif self.field_definition.is_subclass_of((File, Stream)): response = self.create_file_response() else: media_type = self.route_handler.media_type if dto := self.route_handler.resolve_return_dto(): result = dto.create_openapi_schema( field_definition=self.field_definition, handler_id=self.route_handler.handler_id, schema_creator=self.schema_creator, ) else: if self.field_definition.is_subclass_of(Template): field_def = FieldDefinition.from_annotation(str) media_type = media_type or MediaType.HTML elif self.field_definition.is_subclass_of(LitestarResponse): field_def = ( self.field_definition.inner_types[0] if self.field_definition.inner_types else FieldDefinition.from_annotation(Any) ) media_type = media_type or MediaType.JSON else: field_def = self.field_definition result = self.schema_creator.for_field_definition(field_def) schema = ( result if isinstance(result, Schema) else self.context.schema_registry.from_reference(result).schema ) schema.content_encoding = self.route_handler.content_encoding schema.content_media_type = self.route_handler.content_media_type response = OpenAPIResponse( content={get_enum_string_value(media_type): OpenAPIMediaType(schema=result)}, description=self.create_description(), ) self.set_success_response_headers(response) return response def create_redirect_response(self) -> OpenAPIResponse: """Create the schema for a redirect response.""" return OpenAPIResponse( content=None, description=self.create_description(), headers={ "location": OpenAPIHeader( schema=Schema(type=OpenAPIType.STRING), description="target path for the redirect" ) }, ) def create_file_response(self) -> OpenAPIResponse: """Create the schema for a file/stream response.""" return OpenAPIResponse( content={ self.route_handler.media_type: OpenAPIMediaType( schema=Schema( type=OpenAPIType.STRING, content_encoding=self.route_handler.content_encoding, content_media_type=self.route_handler.content_media_type or "application/octet-stream", ), ) }, description=self.create_description(), headers={ "content-length": OpenAPIHeader( schema=Schema(type=OpenAPIType.STRING), description="File size in bytes" ), "last-modified": OpenAPIHeader( schema=Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.DATE_TIME), description="Last modified data-time in RFC 2822 format", ), "etag": OpenAPIHeader(schema=Schema(type=OpenAPIType.STRING), description="Entity tag"), }, ) def set_success_response_headers(self, response: OpenAPIResponse) -> None: """Set the schema for success response headers, if any.""" if response.headers is None: response.headers = {} if not self.schema_creator.generate_examples: schema_creator = self.schema_creator else: schema_creator = SchemaCreator.from_openapi_context(self.context, generate_examples=False) for response_header in self.route_handler.resolve_response_headers(): header = OpenAPIHeader() for attribute_name, attribute_value in ( (k, v) for k, v in asdict(response_header).items() if v is not None ): if attribute_name == "value": header.schema = schema_creator.for_field_definition( FieldDefinition.from_annotation(type(attribute_value)) ) elif attribute_name != "documentation_only": setattr(header, attribute_name, attribute_value) response.headers[response_header.name] = header if cookies := self.route_handler.resolve_response_cookies(): response.headers["Set-Cookie"] = OpenAPIHeader( schema=Schema( all_of=[create_cookie_schema(cookie=cookie) for cookie in sorted(cookies, key=attrgetter("key"))] ) ) def create_additional_responses(self) -> Iterator[tuple[str, OpenAPIResponse]]: """Create the schema for additional responses, if any.""" if not self.route_handler.responses: return for status_code, additional_response in self.route_handler.responses.items(): schema_creator = SchemaCreator.from_openapi_context( self.context, prefer_alias=False, generate_examples=additional_response.generate_examples, ) field_def = FieldDefinition.from_annotation(additional_response.data_container) examples: dict[str, Example | Reference] | None = ( dict(get_formatted_examples(field_def, additional_response.examples)) if additional_response.examples else None ) content: dict[str, OpenAPIMediaType] | None if additional_response.data_container is not None: schema = schema_creator.for_field_definition(field_def) media_type = additional_response.media_type content = { get_enum_string_value(media_type) if not isinstance(media_type, str) else media_type: OpenAPIMediaType(schema=schema, examples=examples) } else: content = None yield ( str(status_code), OpenAPIResponse( description=additional_response.description, content=content, ), ) def create_error_responses(exceptions: list[type[HTTPException]]) -> Iterator[tuple[str, OpenAPIResponse]]: """Create the schema for error responses, if any.""" grouped_exceptions: dict[int, list[type[HTTPException]]] = {} for exc in exceptions: if not grouped_exceptions.get(exc.status_code): grouped_exceptions[exc.status_code] = [] grouped_exceptions[exc.status_code].append(exc) for status_code, exception_group in grouped_exceptions.items(): exceptions_schemas = [] group_description: str = "" for exc in exception_group: example_detail = "" if hasattr(exc, "detail") and exc.detail: group_description = exc.detail example_detail = exc.detail if not example_detail: with contextlib.suppress(Exception): example_detail = HTTPStatus(status_code).phrase exceptions_schemas.append( Schema( type=OpenAPIType.OBJECT, required=["detail", "status_code"], properties={ "status_code": Schema(type=OpenAPIType.INTEGER), "detail": Schema(type=OpenAPIType.STRING), "extra": Schema( type=[OpenAPIType.NULL, OpenAPIType.OBJECT, OpenAPIType.ARRAY], additional_properties=Schema(), ), }, description=pascal_case_to_text(get_name(exc)), examples=[{"status_code": status_code, "detail": example_detail, "extra": {}}], ) ) if len(exceptions_schemas) > 1: # noqa: SIM108 schema = Schema(one_of=exceptions_schemas) else: schema = exceptions_schemas[0] if not group_description: with contextlib.suppress(Exception): group_description = HTTPStatus(status_code).description yield ( str(status_code), OpenAPIResponse( description=group_description, content={MediaType.JSON: OpenAPIMediaType(schema=schema)}, ), ) def create_responses_for_handler( context: OpenAPIContext, route_handler: HTTPRouteHandler, raises_validation_error: bool ) -> Responses | None: """Create the schema for responses, if any. Args: context: An OpenAPIContext instance. route_handler: An HTTPRouteHandler instance. raises_validation_error: Boolean flag indicating whether the handler raises a ValidationException. Returns: Responses """ return ResponseFactory(context, route_handler).create_responses(raises_validation_error=raises_validation_error) litestar-2.16.0/litestar/_openapi/schema_generation/000077500000000000000000000000001500564371300225275ustar00rootroot00000000000000litestar-2.16.0/litestar/_openapi/schema_generation/__init__.py000066400000000000000000000002201500564371300246320ustar00rootroot00000000000000from .plugins import openapi_schema_plugins from .schema import SchemaCreator __all__ = ( "SchemaCreator", "openapi_schema_plugins", ) litestar-2.16.0/litestar/_openapi/schema_generation/constrained_fields.py000066400000000000000000000063321500564371300267440ustar00rootroot00000000000000from __future__ import annotations from datetime import date, datetime, timezone from re import Pattern from typing import TYPE_CHECKING from litestar.openapi.spec.enums import OpenAPIFormat, OpenAPIType from litestar.openapi.spec.schema import Schema if TYPE_CHECKING: from decimal import Decimal from litestar.params import KwargDefinition __all__ = ( "create_date_constrained_field_schema", "create_numerical_constrained_field_schema", "create_string_constrained_field_schema", ) def create_numerical_constrained_field_schema( field_type: type[int] | type[float] | type[Decimal], kwarg_definition: KwargDefinition, ) -> Schema: """Create Schema from Constrained Int/Float/Decimal field.""" schema = Schema(type=OpenAPIType.INTEGER if issubclass(field_type, int) else OpenAPIType.NUMBER) if kwarg_definition.le is not None: schema.maximum = float(kwarg_definition.le) if kwarg_definition.lt is not None: schema.exclusive_maximum = float(kwarg_definition.lt) if kwarg_definition.ge is not None: schema.minimum = float(kwarg_definition.ge) if kwarg_definition.gt is not None: schema.exclusive_minimum = float(kwarg_definition.gt) if kwarg_definition.multiple_of is not None: schema.multiple_of = float(kwarg_definition.multiple_of) return schema def create_date_constrained_field_schema( field_type: type[date] | type[datetime], kwarg_definition: KwargDefinition, ) -> Schema: """Create Schema from Constrained Date Field.""" schema = Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.DATE if issubclass(field_type, date) else OpenAPIFormat.DATE_TIME ) for kwarg_definition_attr, schema_attr in [ ("le", "maximum"), ("lt", "exclusive_maximum"), ("ge", "minimum"), ("gt", "exclusive_minimum"), ]: if attr := getattr(kwarg_definition, kwarg_definition_attr): setattr( schema, schema_attr, datetime.combine( datetime.fromtimestamp(attr, tz=timezone.utc) if isinstance(attr, (float, int)) else attr, datetime.min.time(), tzinfo=timezone.utc, ).timestamp(), ) return schema def create_string_constrained_field_schema( field_type: type[str] | type[bytes], kwarg_definition: KwargDefinition, ) -> Schema: """Create Schema from Constrained Str/Bytes field.""" schema = Schema(type=OpenAPIType.STRING) if issubclass(field_type, bytes): schema.content_encoding = "utf-8" if kwarg_definition.min_length: schema.min_length = kwarg_definition.min_length if kwarg_definition.max_length: schema.max_length = kwarg_definition.max_length if kwarg_definition.pattern: schema.pattern = ( kwarg_definition.pattern.pattern # type: ignore[attr-defined] if isinstance(kwarg_definition.pattern, Pattern) # type: ignore[unreachable] else kwarg_definition.pattern ) if kwarg_definition.lower_case: schema.description = "must be in lower case" if kwarg_definition.upper_case: schema.description = "must be in upper case" return schema litestar-2.16.0/litestar/_openapi/schema_generation/examples.py000066400000000000000000000053471500564371300247300ustar00rootroot00000000000000from __future__ import annotations import typing from dataclasses import replace from decimal import Decimal from enum import Enum from typing import TYPE_CHECKING, Any import msgspec from polyfactory.exceptions import ParameterException from polyfactory.factories import DataclassFactory from polyfactory.field_meta import FieldMeta, Null from polyfactory.utils.helpers import unwrap_annotation from polyfactory.utils.predicates import is_union from typing_extensions import get_args from litestar.openapi.spec import Example from litestar.plugins.pydantic.utils import is_pydantic_model_instance from litestar.types import Empty if TYPE_CHECKING: from litestar.typing import FieldDefinition class ExampleFactory(DataclassFactory[Example]): __model__ = Example __random_seed__ = 10 def _normalize_example_value(value: Any) -> Any: """Normalize the example value to make it look a bit prettier.""" # if UnsetType is part of the union, then it might get chosen as the value # but that will not be properly serialized by msgspec unless it is for a field # in a msgspec Struct if is_union(value): args = list(get_args(value)) try: args.remove(msgspec.UnsetType) value = typing.Union[tuple(args)] # pyright: ignore except ValueError: # UnsetType not part of the Union pass value = unwrap_annotation(annotation=value, random=ExampleFactory.__random__) if isinstance(value, (Decimal, float)): value = round(float(value), 2) if isinstance(value, Enum): value = value.value if is_pydantic_model_instance(value): from litestar.plugins.pydantic import _model_dump value = _model_dump(value) if isinstance(value, (list, set)): value = [_normalize_example_value(v) for v in value] if isinstance(value, dict): for k, v in value.items(): value[k] = _normalize_example_value(v) return value def _create_field_meta(field: FieldDefinition) -> FieldMeta: return FieldMeta.from_type( annotation=field.annotation, default=field.default if field.default is not Empty else Null, name=field.name, random=ExampleFactory.__random__, ) def create_examples_for_field(field: FieldDefinition) -> list[Example]: """Create an OpenAPI Example instance. Args: field: A signature field. Returns: A list including a single example. """ try: field_meta = _create_field_meta(replace(field, annotation=_normalize_example_value(field.annotation))) value = ExampleFactory.get_field_value(field_meta) return [Example(description=f"Example {field.name} value", value=value)] except ParameterException: return [] litestar-2.16.0/litestar/_openapi/schema_generation/plugins/000077500000000000000000000000001500564371300242105ustar00rootroot00000000000000litestar-2.16.0/litestar/_openapi/schema_generation/plugins/__init__.py000066400000000000000000000010671500564371300263250ustar00rootroot00000000000000from .dataclass import DataclassSchemaPlugin from .pagination import PaginationSchemaPlugin from .struct import StructSchemaPlugin from .typed_dict import TypedDictSchemaPlugin __all__ = ("openapi_schema_plugins",) # NOTE: The Pagination type plugin has to come before the Dataclass plugin since the Pagination # classes are dataclasses, but we want to handle them differently from how dataclasses are normally # handled. openapi_schema_plugins = [ PaginationSchemaPlugin(), StructSchemaPlugin(), DataclassSchemaPlugin(), TypedDictSchemaPlugin(), ] litestar-2.16.0/litestar/_openapi/schema_generation/plugins/dataclass.py000066400000000000000000000032061500564371300265220ustar00rootroot00000000000000from __future__ import annotations import dataclasses from dataclasses import MISSING, fields from typing import TYPE_CHECKING from litestar.plugins import OpenAPISchemaPlugin from litestar.types import Empty from litestar.typing import FieldDefinition from litestar.utils.predicates import is_optional_union if TYPE_CHECKING: from litestar._openapi.schema_generation import SchemaCreator from litestar.openapi.spec import Schema class DataclassSchemaPlugin(OpenAPISchemaPlugin): def is_plugin_supported_field(self, field_definition: FieldDefinition) -> bool: return field_definition.is_dataclass_type def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: type_hints = field_definition.get_type_hints(include_extras=True, resolve_generics=True) dataclass_fields = fields(field_definition.type_) return schema_creator.create_component_schema( field_definition, required=sorted( field.name for field in dataclass_fields if ( field.default is MISSING and field.default_factory is MISSING and not is_optional_union(type_hints[field.name]) ) ), property_fields={ field.name: FieldDefinition.from_kwarg( annotation=type_hints[field.name], name=field.name, default=field.default if field.default is not dataclasses.MISSING else Empty, ) for field in dataclass_fields }, ) litestar-2.16.0/litestar/_openapi/schema_generation/plugins/pagination.py000066400000000000000000000055771500564371300267310ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar.openapi.spec import OpenAPIType, Schema from litestar.pagination import ClassicPagination, CursorPagination, OffsetPagination from litestar.plugins import OpenAPISchemaPlugin if TYPE_CHECKING: from litestar._openapi.schema_generation import SchemaCreator from litestar.typing import FieldDefinition class PaginationSchemaPlugin(OpenAPISchemaPlugin): def is_plugin_supported_field(self, field_definition: FieldDefinition) -> bool: return field_definition.origin in (ClassicPagination, CursorPagination, OffsetPagination) def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: if field_definition.origin is ClassicPagination: return Schema( type=OpenAPIType.OBJECT, properties={ "items": Schema( type=OpenAPIType.ARRAY, items=schema_creator.for_field_definition(field_definition.inner_types[0]), ), "page_size": Schema(type=OpenAPIType.INTEGER, description="Number of items per page."), "current_page": Schema(type=OpenAPIType.INTEGER, description="Current page number."), "total_pages": Schema(type=OpenAPIType.INTEGER, description="Total number of pages."), }, ) if field_definition.origin is OffsetPagination: return Schema( type=OpenAPIType.OBJECT, properties={ "items": Schema( type=OpenAPIType.ARRAY, items=schema_creator.for_field_definition(field_definition.inner_types[0]), ), "limit": Schema(type=OpenAPIType.INTEGER, description="Maximal number of items to send."), "offset": Schema(type=OpenAPIType.INTEGER, description="Offset from the beginning of the query."), "total": Schema(type=OpenAPIType.INTEGER, description="Total number of items."), }, ) cursor_schema = schema_creator.not_generating_examples.for_field_definition(field_definition.inner_types[0]) cursor_schema.description = "Unique ID, designating the last identifier in the given data set. This value can be used to request the 'next' batch of records." return Schema( type=OpenAPIType.OBJECT, properties={ "items": Schema( type=OpenAPIType.ARRAY, items=schema_creator.for_field_definition(field_definition=field_definition.inner_types[1]), ), "cursor": cursor_schema, "results_per_page": Schema(type=OpenAPIType.INTEGER, description="Maximal number of items to send."), }, ) litestar-2.16.0/litestar/_openapi/schema_generation/plugins/struct.py000066400000000000000000000055301500564371300261110ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Literal import msgspec from msgspec import Struct from litestar.plugins import OpenAPISchemaPlugin from litestar.plugins.core._msgspec import kwarg_definition_from_field from litestar.types.empty import Empty from litestar.typing import FieldDefinition from litestar.utils.predicates import is_optional_union if TYPE_CHECKING: from litestar._openapi.schema_generation import SchemaCreator from litestar.openapi.spec import Schema class StructSchemaPlugin(OpenAPISchemaPlugin): def is_plugin_supported_field(self, field_definition: FieldDefinition) -> bool: return not field_definition.is_union and field_definition.is_subclass_of(Struct) @staticmethod def _is_field_required(field: msgspec.inspect.Field) -> bool: return field.required or field.default_factory is Empty def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: type_hints = field_definition.get_type_hints(include_extras=True, resolve_generics=True) struct_info: msgspec.inspect.StructType = msgspec.inspect.type_info(field_definition.type_) # type: ignore[assignment] struct_fields = struct_info.fields property_fields = {} for field in struct_fields: field_definition_kwargs = {} if kwarg_definition := kwarg_definition_from_field(field)[0]: field_definition_kwargs["kwarg_definition"] = kwarg_definition property_fields[field.encode_name] = FieldDefinition.from_annotation( annotation=type_hints[field.name], name=field.encode_name, default=field.default if field.default not in {msgspec.NODEFAULT, msgspec.UNSET} else Empty, **field_definition_kwargs, ) required = [ field.encode_name for field in struct_fields if self._is_field_required(field=field) and not is_optional_union(type_hints[field.name]) ] # Support tagged unions: https://jcristharif.com/msgspec/structs.html#tagged-unions # These structs contain a tag_field and a tag. Since these fields are added # dynamically, they are not present within the regular struct fields and don't # have any type annotation associated with them, so we create a FieldDefinition # manually if struct_info.tag_field: # using a Literal here will set these as a const in the schema property_fields[struct_info.tag_field] = FieldDefinition.from_annotation(Literal[struct_info.tag]) # pyright: ignore required.append(struct_info.tag_field) return schema_creator.create_component_schema( field_definition, required=sorted(required), property_fields=property_fields, ) litestar-2.16.0/litestar/_openapi/schema_generation/plugins/typed_dict.py000066400000000000000000000017071500564371300267170ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar.plugins import OpenAPISchemaPlugin from litestar.typing import FieldDefinition if TYPE_CHECKING: from litestar._openapi.schema_generation import SchemaCreator from litestar.openapi.spec import Schema class TypedDictSchemaPlugin(OpenAPISchemaPlugin): def is_plugin_supported_field(self, field_definition: FieldDefinition) -> bool: return field_definition.is_typeddict_type def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: type_hints = field_definition.get_type_hints(include_extras=True, resolve_generics=True) return schema_creator.create_component_schema( field_definition, required=sorted(getattr(field_definition.type_, "__required_keys__", [])), property_fields={k: FieldDefinition.from_kwarg(v, k) for k, v in type_hints.items()}, ) litestar-2.16.0/litestar/_openapi/schema_generation/schema.py000066400000000000000000000657621500564371300243610ustar00rootroot00000000000000from __future__ import annotations from collections import deque from copy import copy from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network from pathlib import Path from typing import ( TYPE_CHECKING, Any, DefaultDict, Deque, Dict, FrozenSet, Hashable, Iterable, List, Literal, Mapping, MutableMapping, MutableSequence, OrderedDict, Pattern, Sequence, Set, Tuple, Union, cast, ) from uuid import UUID from typing_extensions import Self, get_args from litestar._openapi.datastructures import SchemaRegistry from litestar._openapi.schema_generation.constrained_fields import ( create_date_constrained_field_schema, create_numerical_constrained_field_schema, create_string_constrained_field_schema, ) from litestar._openapi.schema_generation.utils import ( _should_create_literal_schema, get_json_schema_formatted_examples, ) from litestar.datastructures import SecretBytes, SecretString, UploadFile from litestar.exceptions import ImproperlyConfiguredException from litestar.openapi.spec.enums import OpenAPIFormat, OpenAPIType from litestar.openapi.spec.schema import Schema, SchemaDataContainer from litestar.params import BodyKwarg, KwargDefinition, ParameterKwarg from litestar.plugins import OpenAPISchemaPlugin from litestar.types import Empty from litestar.types.builtin_types import NoneType from litestar.typing import FieldDefinition from litestar.utils.helpers import get_name from litestar.utils.predicates import ( is_class_and_subclass, is_undefined_sentinel, ) from litestar.utils.typing import ( get_origin_or_inner_type, make_non_optional_union, unwrap_new_type, ) if TYPE_CHECKING: from litestar._openapi.datastructures import OpenAPIContext from litestar.openapi.spec import Example, Reference from litestar.plugins import OpenAPISchemaPluginProtocol KWARG_DEFINITION_ATTRIBUTE_TO_OPENAPI_PROPERTY_MAP: dict[str, str] = { "content_encoding": "content_encoding", "default": "default", "description": "description", "enum": "enum", "examples": "examples", "external_docs": "external_docs", "format": "format", "ge": "minimum", "gt": "exclusive_minimum", "le": "maximum", "lt": "exclusive_maximum", "max_items": "max_items", "max_length": "max_length", "min_items": "min_items", "min_length": "min_length", "multiple_of": "multiple_of", "pattern": "pattern", "title": "title", "read_only": "read_only", } TYPE_MAP: dict[type[Any] | None | Any, Schema] = { Decimal: Schema(type=OpenAPIType.NUMBER), DefaultDict: Schema(type=OpenAPIType.OBJECT), Deque: Schema(type=OpenAPIType.ARRAY), Dict: Schema(type=OpenAPIType.OBJECT), FrozenSet: Schema(type=OpenAPIType.ARRAY), IPv4Address: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.IPV4), IPv4Interface: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.IPV4), IPv4Network: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.IPV4), IPv6Address: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.IPV6), IPv6Interface: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.IPV6), IPv6Network: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.IPV6), Iterable: Schema(type=OpenAPIType.ARRAY), List: Schema(type=OpenAPIType.ARRAY), Mapping: Schema(type=OpenAPIType.OBJECT), MutableMapping: Schema(type=OpenAPIType.OBJECT), MutableSequence: Schema(type=OpenAPIType.ARRAY), None: Schema(type=OpenAPIType.NULL), NoneType: Schema(type=OpenAPIType.NULL), OrderedDict: Schema(type=OpenAPIType.OBJECT), Path: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI), Pattern: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.REGEX), SecretBytes: Schema(type=OpenAPIType.STRING), SecretString: Schema(type=OpenAPIType.STRING), Sequence: Schema(type=OpenAPIType.ARRAY), Set: Schema(type=OpenAPIType.ARRAY), Tuple: Schema(type=OpenAPIType.ARRAY), UUID: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.UUID), bool: Schema(type=OpenAPIType.BOOLEAN), bytearray: Schema(type=OpenAPIType.STRING), bytes: Schema(type=OpenAPIType.STRING), date: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.DATE), datetime: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.DATE_TIME), deque: Schema(type=OpenAPIType.ARRAY), dict: Schema(type=OpenAPIType.OBJECT), float: Schema(type=OpenAPIType.NUMBER), frozenset: Schema(type=OpenAPIType.ARRAY), int: Schema(type=OpenAPIType.INTEGER), list: Schema(type=OpenAPIType.ARRAY), set: Schema(type=OpenAPIType.ARRAY), str: Schema(type=OpenAPIType.STRING), time: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.DURATION), timedelta: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.DURATION), tuple: Schema(type=OpenAPIType.ARRAY), } def _types_in_list(lst: list[Any]) -> list[OpenAPIType] | OpenAPIType: """Extract unique OpenAPITypes present in the values of a list. Args: lst: A list of values Returns: OpenAPIType in the given list. If more then one exists, return a list of OpenAPITypes. """ schema_types: list[OpenAPIType] = [] for item in lst: schema_type = TYPE_MAP[type(item)].type if isinstance(schema_type, OpenAPIType): schema_types.append(schema_type) else: raise RuntimeError("Unexpected type for schema item") # pragma: no cover schema_types = list(set(schema_types)) return schema_types[0] if len(schema_types) == 1 else schema_types def _get_type_schema_name(field_definition: FieldDefinition) -> str: """Extract the schema name from a data container. Args: field_definition: A field definition instance. Returns: A string """ if name := getattr(field_definition.annotation, "__schema_name__", None): return cast("str", name) name = get_name(field_definition.annotation) if field_definition.inner_types: inner_parts = ", ".join(_get_type_schema_name(t) for t in field_definition.inner_types) return f"{name}[{inner_parts}]" return name def _iter_flat_literal_args(annotation: Any) -> Iterable[Any]: """Iterate over the flattened arguments of a Literal. Args: annotation: An Literal annotation. Yields: The flattened arguments of the Literal. """ for arg in get_args(annotation): if get_origin_or_inner_type(arg) is Literal: yield from _iter_flat_literal_args(arg) else: yield arg.value if isinstance(arg, Enum) else arg def create_literal_schema(annotation: Any, include_null: bool = False) -> Schema: """Create a schema instance for a Literal. Args: annotation: An Literal annotation. include_null: Whether to include null as a possible value. Returns: A schema instance. """ args = list(_iter_flat_literal_args(annotation)) if include_null and None not in args: args.append(None) schema = Schema(type=_types_in_list(args)) if len(args) > 1: schema.enum = args else: schema.const = args[0] return schema def create_schema_for_annotation(annotation: Any) -> Schema: """Get a schema from the type mapping - if possible. Args: annotation: A type annotation. Returns: A schema instance or None. """ return copy(TYPE_MAP[annotation]) if annotation in TYPE_MAP else Schema() class SchemaCreator: __slots__ = ("generate_examples", "plugins", "prefer_alias", "schema_registry") def __init__( self, generate_examples: bool = False, plugins: Iterable[OpenAPISchemaPluginProtocol] | None = None, prefer_alias: bool = True, schema_registry: SchemaRegistry | None = None, ) -> None: """Instantiate a SchemaCreator. Args: generate_examples: Whether to generate examples if none are given. plugins: A list of plugins. prefer_alias: Whether to prefer the alias name for the schema. schema_registry: A SchemaRegistry instance. """ self.generate_examples = generate_examples self.plugins = plugins if plugins is not None else [] self.prefer_alias = prefer_alias self.schema_registry = schema_registry or SchemaRegistry() @classmethod def from_openapi_context(cls, context: OpenAPIContext, prefer_alias: bool = True, **kwargs: Any) -> Self: kwargs.setdefault("generate_examples", context.openapi_config.create_examples) kwargs.setdefault("plugins", context.plugins) kwargs.setdefault("schema_registry", context.schema_registry) return cls(**kwargs, prefer_alias=prefer_alias) @property def not_generating_examples(self) -> SchemaCreator: """Return a SchemaCreator with generate_examples set to False.""" if not self.generate_examples: return self return type(self)(generate_examples=False, plugins=self.plugins, prefer_alias=False) @staticmethod def plugin_supports_field(plugin: OpenAPISchemaPluginProtocol, field: FieldDefinition) -> bool: if predicate := getattr(plugin, "is_plugin_supported_field", None): return predicate(field) # type: ignore[no-any-return] return plugin.is_plugin_supported_type(field.annotation) def get_plugin_for(self, field_definition: FieldDefinition) -> OpenAPISchemaPluginProtocol | None: return next( (plugin for plugin in self.plugins if self.plugin_supports_field(plugin, field_definition)), None, ) def is_constrained_field(self, field_definition: FieldDefinition) -> bool: """Return if the field is constrained, taking into account constraints defined by plugins""" return ( isinstance(field_definition.kwarg_definition, (ParameterKwarg, BodyKwarg)) and field_definition.kwarg_definition.is_constrained ) or any( p.is_constrained_field(field_definition) for p in self.plugins if isinstance(p, OpenAPISchemaPlugin) and p.is_plugin_supported_field(field_definition) ) def is_undefined(self, value: Any) -> bool: """Return if the field is undefined, taking into account undefined types defined by plugins""" return is_undefined_sentinel(value) or any( p.is_undefined_sentinel(value) for p in self.plugins if isinstance(p, OpenAPISchemaPlugin) ) def for_field_definition(self, field_definition: FieldDefinition) -> Schema | Reference: """Create a Schema for a given FieldDefinition. Args: field_definition: A signature field instance. Returns: A schema instance. """ result: Schema | Reference if field_definition.is_new_type: result = self.for_new_type(field_definition) elif field_definition.is_type_alias_type: result = self.for_type_alias_type(field_definition) elif plugin_for_annotation := self.get_plugin_for(field_definition): result = self.for_plugin(field_definition, plugin_for_annotation) elif _should_create_literal_schema(field_definition): annotation = ( make_non_optional_union(field_definition.annotation) if field_definition.is_optional else field_definition.annotation ) result = create_literal_schema( annotation, include_null=field_definition.is_optional, ) elif field_definition.is_optional: result = self.for_optional_field(field_definition) elif field_definition.is_enum: result = self.for_enum_field(field_definition) elif field_definition.is_union: result = self.for_union_field(field_definition) elif field_definition.is_type_var: result = self.for_typevar() elif self.is_constrained_field(field_definition): result = self.for_constrained_field(field_definition) elif field_definition.inner_types and not field_definition.is_generic: # this case does not recurse for all base cases, so it needs to happen # after all non-concrete cases result = self.for_object_type(field_definition) elif field_definition.is_subclass_of(UploadFile): result = self.for_upload_file(field_definition) else: result = create_schema_for_annotation(field_definition.annotation) return self.process_schema_result(field_definition, result) if isinstance(result, Schema) else result def for_new_type(self, field_definition: FieldDefinition) -> Schema | Reference: return self.for_field_definition( FieldDefinition.from_kwarg( annotation=unwrap_new_type(field_definition.annotation), name=field_definition.name, default=field_definition.default, ) ) def for_type_alias_type(self, field_definition: FieldDefinition) -> Schema | Reference: return self.for_field_definition( FieldDefinition.from_kwarg( annotation=field_definition.annotation.__value__, name=field_definition.name, default=field_definition.default, kwarg_definition=field_definition.kwarg_definition, ) ) @staticmethod def for_upload_file(field_definition: FieldDefinition) -> Schema: """Create schema for UploadFile. Args: field_definition: A field definition instance. Returns: A Schema instance. """ property_key = "file" schema = Schema( type=OpenAPIType.STRING, content_media_type="application/octet-stream", format=OpenAPIFormat.BINARY, ) # If the type is `dict[str, UploadFile]`, then it's the same as a `list[UploadFile]` # but we will internally convert that into a `dict[str, UploadFile]`. if field_definition.is_non_string_sequence or field_definition.is_mapping: property_key = "files" schema = Schema(type=OpenAPIType.ARRAY, items=schema) # If the uploadfile is annotated directly on the handler, then the # 'properties' needs to be created. Else, the 'properties' will be # created by the corresponding plugin. is_defined_on_handler = field_definition.name == "data" and isinstance( field_definition.kwarg_definition, BodyKwarg ) if is_defined_on_handler: return Schema(type=OpenAPIType.OBJECT, properties={property_key: schema}) return schema @staticmethod def for_typevar() -> Schema: """Create a schema for a TypeVar. Returns: A schema instance. """ return Schema(type=OpenAPIType.OBJECT) def for_optional_field(self, field_definition: FieldDefinition) -> Schema: """Create a Schema for an optional FieldDefinition. Args: field_definition: A signature field instance. Returns: A schema instance. """ schema_or_reference = self.for_field_definition( FieldDefinition.from_kwarg( annotation=make_non_optional_union(field_definition.annotation), name=field_definition.name, default=field_definition.default, ) ) if isinstance(schema_or_reference, Schema) and isinstance(schema_or_reference.one_of, list): result = schema_or_reference.one_of else: result = [schema_or_reference] return Schema(one_of=[*result, Schema(type=OpenAPIType.NULL)]) def for_union_field(self, field_definition: FieldDefinition) -> Schema: """Create a Schema for a union FieldDefinition. Args: field_definition: A signature field instance. Returns: A schema instance. """ inner_types = (f for f in (field_definition.inner_types or []) if not self.is_undefined(f.annotation)) values = list(map(self.for_field_definition, inner_types)) return Schema(one_of=values) def for_object_type(self, field_definition: FieldDefinition) -> Schema: """Create schema for object types (dict, Mapping, list, Sequence etc.) types. Args: field_definition: A signature field instance. Returns: A schema instance. """ if field_definition.has_inner_subclass_of(UploadFile): return self.for_upload_file(field_definition) if field_definition.is_mapping: return Schema( type=OpenAPIType.OBJECT, additional_properties=( self.for_field_definition(field_definition.inner_types[1]) if field_definition.inner_types and len(field_definition.inner_types) == 2 else None ), ) if field_definition.is_non_string_sequence or field_definition.is_non_string_iterable: # filters out ellipsis from tuple[int, ...] type annotations inner_types = tuple(f for f in field_definition.inner_types if f.annotation is not Ellipsis) # Handle tuple elements using prefixItems if (field_definition.origin is tuple) and inner_types == field_definition.inner_types: return Schema( type=OpenAPIType.ARRAY, prefix_items=[self.for_field_definition(f) for f in inner_types], ) # Handle other sequence types normally items = list(map(self.for_field_definition, inner_types)) return Schema( type=OpenAPIType.ARRAY, items=Schema(one_of=items) if len(items) > 1 else items[0], ) raise ImproperlyConfiguredException( # pragma: no cover f"Parameter '{field_definition.name}' with type '{field_definition.annotation}' could not be mapped to an Open API type. " f"This can occur if a user-defined generic type is resolved as a parameter. If '{field_definition.name}' should " "not be documented as a parameter, annotate it using the `Dependency` function, e.g., " f"`{field_definition.name}: ... = Dependency(...)`." ) def for_plugin(self, field_definition: FieldDefinition, plugin: OpenAPISchemaPluginProtocol) -> Schema | Reference: """Create a schema using a plugin. Args: field_definition: A signature field instance. plugin: A plugin for the field type. Returns: A schema instance. """ if (ref := self.schema_registry.get_reference_for_field_definition(field_definition)) is not None: return ref schema = plugin.to_openapi_schema(field_definition=field_definition, schema_creator=self) if isinstance(schema, SchemaDataContainer): # pragma: no cover return self.for_field_definition( FieldDefinition.from_kwarg( annotation=schema.data_container, name=field_definition.name, default=field_definition.default, extra=field_definition.extra, kwarg_definition=field_definition.kwarg_definition, ) ) return schema def for_constrained_field(self, field: FieldDefinition) -> Schema: """Create Schema for Pydantic Constrained fields (created using constr(), conint() and so forth, or by subclassing Constrained*) Args: field: A signature field instance. Returns: A schema instance. """ kwarg_definition = cast(Union[ParameterKwarg, BodyKwarg], field.kwarg_definition) if any(is_class_and_subclass(field.annotation, t) for t in (int, float, Decimal)): return create_numerical_constrained_field_schema(field.annotation, kwarg_definition) if any(is_class_and_subclass(field.annotation, t) for t in (str, bytes)): return create_string_constrained_field_schema(field.annotation, kwarg_definition) if any(is_class_and_subclass(field.annotation, t) for t in (date, datetime)): return create_date_constrained_field_schema(field.annotation, kwarg_definition) return self.for_collection_constrained_field(field) def for_collection_constrained_field(self, field_definition: FieldDefinition) -> Schema: """Create Schema from Constrained List/Set field. Args: field_definition: A signature field instance. Returns: A schema instance. """ schema = Schema(type=OpenAPIType.ARRAY) kwarg_definition = cast(Union[ParameterKwarg, BodyKwarg], field_definition.kwarg_definition) if kwarg_definition.min_items: schema.min_items = kwarg_definition.min_items if kwarg_definition.max_items: schema.max_items = kwarg_definition.max_items if any(is_class_and_subclass(field_definition.annotation, t) for t in (set, frozenset)): schema.unique_items = True item_creator = self.not_generating_examples if field_definition.inner_types: items = list(map(item_creator.for_field_definition, field_definition.inner_types)) schema.items = Schema(one_of=items) if len(items) > 1 else items[0] # INFO: Removed because it was only for pydantic constrained collections return schema def for_enum_field( self, field_definition: FieldDefinition, ) -> Schema | Reference: """Create a schema instance for an enum. Args: field_definition: A signature field instance. Returns: A schema or reference instance. """ enum_type: None | OpenAPIType | list[OpenAPIType] = None if issubclass(field_definition.annotation, Enum): # pragma: no branch # This method is only called for enums, so this branch is always executed if issubclass(field_definition.annotation, str): # StrEnum enum_type = OpenAPIType.STRING elif issubclass(field_definition.annotation, int): # IntEnum enum_type = OpenAPIType.INTEGER enum_values: list[Any] = [v.value for v in field_definition.annotation] if enum_type is None: enum_type = _types_in_list(enum_values) schema = self.schema_registry.get_schema_for_field_definition(field_definition) schema.type = enum_type schema.enum = enum_values schema.title = get_name(field_definition.annotation) schema.description = field_definition.annotation.__doc__ return self.schema_registry.get_reference_for_field_definition(field_definition) or schema def process_schema_result(self, field: FieldDefinition, schema: Schema) -> Schema | Reference: if field.kwarg_definition and field.is_const and field.has_default and schema.const is None: schema.const = field.default if field.kwarg_definition: for kwarg_definition_key, schema_key in KWARG_DEFINITION_ATTRIBUTE_TO_OPENAPI_PROPERTY_MAP.items(): if (value := getattr(field.kwarg_definition, kwarg_definition_key, Empty)) and ( not isinstance(value, Hashable) or not self.is_undefined(value) ): if schema_key == "examples": value = get_json_schema_formatted_examples(cast("list[Example]", value)) # we only want to transfer values from the `KwargDefinition` to `Schema` if the schema object # doesn't already have a value for that property. For example, if a field is a constrained date, # by this point, we have already set the `exclusive_minimum` and/or `exclusive_maximum` fields # to floating point timestamp values on the schema object. However, the original `date` objects # that define those constraints on `KwargDefinition` are still `date` objects. We don't want to # overwrite them here. if getattr(schema, schema_key, None) is None: setattr(schema, schema_key, value) if isinstance(field.kwarg_definition, KwargDefinition) and (extra := field.kwarg_definition.schema_extra): field_aliases = schema.field_aliases() for schema_key, value in extra.items(): schema_key = field_aliases.get(schema_key, schema_key) if not hasattr(schema, schema_key): raise ValueError( f"`schema_extra` declares key `{schema_key}` which does not exist in `Schema` object" ) setattr(schema, schema_key, value) if schema.default is None and field.default is not Empty: schema.default = field.default if not schema.examples and self.generate_examples: from litestar._openapi.schema_generation.examples import create_examples_for_field schema.examples = get_json_schema_formatted_examples(create_examples_for_field(field)) if schema.title and schema.type == OpenAPIType.OBJECT: return self.schema_registry.get_reference_for_field_definition(field) or schema return schema def create_component_schema( self, type_: FieldDefinition, /, required: list[str], property_fields: Mapping[str, FieldDefinition], openapi_type: OpenAPIType = OpenAPIType.OBJECT, title: str | None = None, examples: list[Any] | None = None, ) -> Schema: """Create a schema for the components/schemas section of the OpenAPI spec. These are schemas that can be referenced by other schemas in the document, including self references. To support self referencing schemas, the schema is added to the registry before schemas for its properties are created. This allows the schema to be referenced by its properties. Args: type_: ``FieldDefinition`` instance of the type to create a schema for. required: A list of required fields. property_fields: Mapping of name to ``FieldDefinition`` instances for the properties of the schema. openapi_type: The OpenAPI type, defaults to ``OpenAPIType.OBJECT``. title: The schema title, generated if not provided. examples: A mapping of example names to ``Example`` instances, not required. Returns: A schema instance. """ schema = self.schema_registry.get_schema_for_field_definition(type_) schema.title = title or _get_type_schema_name(type_) schema.required = required schema.type = openapi_type schema.properties = {k: self.for_field_definition(v) for k, v in property_fields.items()} schema.examples = examples return schema litestar-2.16.0/litestar/_openapi/schema_generation/utils.py000066400000000000000000000036501500564371300242450ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Mapping from litestar.utils.helpers import get_name if TYPE_CHECKING: from collections.abc import Sequence from litestar.openapi.spec import Example from litestar.typing import FieldDefinition __all__ = ("_should_create_literal_schema",) def _should_create_literal_schema(field_definition: FieldDefinition) -> bool: """Predicate to determine if we should create a literal schema for the field def, or not. This returns ``True`` if the field definition is an literal, or if the field definition is a union of a literal and None. When an annotation is `Literal["anything"] | None` we should create a schema for the literal that includes `null` in the enum values. Args: field_definition: A field definition instance. Returns: A boolean """ return field_definition.is_literal or ( field_definition.is_optional and all(inner.is_literal for inner in field_definition.inner_types if not inner.is_none_type) ) def get_example_id(example: Example, name: str, index: int) -> str: """Get the example ID. Args: example: The example instance. name: The name of the field. index: The index of the example. Returns: The example ID. """ return example.id or f"{name}-example-{index}" def get_formatted_examples(field_definition: FieldDefinition, examples: Sequence[Example]) -> Mapping[str, Example]: """Format the examples into the OpenAPI schema format.""" name = field_definition.name or get_name(field_definition.type_) name = name.lower() return {get_example_id(example, name, i): example for i, example in enumerate(examples, 1)} def get_json_schema_formatted_examples(examples: Sequence[Example]) -> list[Any]: """Format the examples into the JSON schema format.""" return [example.value for example in examples] litestar-2.16.0/litestar/_openapi/typescript_converter/000077500000000000000000000000001500564371300233515ustar00rootroot00000000000000litestar-2.16.0/litestar/_openapi/typescript_converter/__init__.py000066400000000000000000000000001500564371300254500ustar00rootroot00000000000000litestar-2.16.0/litestar/_openapi/typescript_converter/converter.py000066400000000000000000000252721500564371300257420ustar00rootroot00000000000000from __future__ import annotations from copy import copy from dataclasses import fields from typing import Any, TypeVar, cast from litestar._openapi.typescript_converter.schema_parsing import ( normalize_typescript_namespace, parse_schema, ) from litestar._openapi.typescript_converter.types import ( TypeScriptInterface, TypeScriptNamespace, TypeScriptPrimitive, TypeScriptProperty, TypeScriptType, TypeScriptUnion, ) from litestar.enums import HttpMethod, ParamType from litestar.openapi.spec import ( Components, OpenAPI, Operation, Parameter, Reference, RequestBody, Responses, Schema, ) __all__ = ( "convert_openapi_to_typescript", "deref_container", "get_openapi_type", "parse_params", "parse_request_body", "parse_responses", "resolve_ref", ) from litestar.openapi.spec.base import BaseSchemaObject T = TypeVar("T") def _deref_schema_object(value: BaseSchemaObject, components: Components) -> BaseSchemaObject: for field in fields(value): if field_value := getattr(value, field.name, None): if isinstance(field_value, Reference): setattr( value, field.name, deref_container(resolve_ref(field_value, components=components), components=components), ) elif isinstance(field_value, (Schema, dict, list)): setattr(value, field.name, deref_container(field_value, components=components)) return value def _deref_dict(value: dict[str, Any], components: Components) -> dict[str, Any]: for k, v in value.items(): if isinstance(v, Reference): value[k] = deref_container(resolve_ref(v, components=components), components=components) elif isinstance(v, (Schema, dict, list)): value[k] = deref_container(v, components=components) return value def _deref_list(values: list[Any], components: Components) -> list[Any]: for i, value in enumerate(values): if isinstance(value, Reference): values[i] = deref_container(resolve_ref(value, components=components), components=components) elif isinstance(value, (Schema, (dict, list))): values[i] = deref_container(value, components=components) return values def deref_container(open_api_container: T, components: Components) -> T: """Dereference an object that may contain Reference instances. Args: open_api_container: Either an OpenAPI content, a dict or a list. components: The OpenAPI schema Components section. Returns: A dereferenced object. """ if isinstance(open_api_container, BaseSchemaObject): return cast("T", _deref_schema_object(open_api_container, components)) if isinstance(open_api_container, dict): return cast("T", _deref_dict(copy(open_api_container), components)) if isinstance(open_api_container, list): return cast("T", _deref_list(copy(open_api_container), components)) raise ValueError(f"unexpected container type {type(open_api_container).__name__}") # pragma: no cover def resolve_ref(ref: Reference, components: Components) -> Schema: """Resolve a reference object into the actual value it points at. Args: ref: A Reference instance. components: The OpenAPI schema Components section. Returns: An OpenAPI schema instance. """ current: Any = components for path in [p for p in ref.ref.split("/") if p not in {"#", "components"}]: current = current[path] if isinstance(current, dict) else getattr(current, path, None) if not isinstance(current, Schema): # pragma: no cover raise ValueError( f"unexpected value type, expected schema but received {type(current).__name__ if current is not None else 'None'}" ) return current def get_openapi_type(value: Reference | T, components: Components) -> T: """Extract or dereference an OpenAPI container type. Args: value: Either a reference or a container type. components: The OpenAPI schema Components section. Returns: The extracted container. """ if isinstance(value, Reference): resolved_ref = resolve_ref(value, components=components) return cast("T", deref_container(open_api_container=resolved_ref, components=components)) return deref_container(open_api_container=value, components=components) def parse_params( params: list[Parameter], components: Components, ) -> tuple[TypeScriptInterface, ...]: """Parse request parameters. Args: params: An OpenAPI Operation parameters. components: The OpenAPI schema Components section. Returns: A tuple of resolved interfaces. """ cookie_params: list[TypeScriptProperty] = [] header_params: list[TypeScriptProperty] = [] path_params: list[TypeScriptProperty] = [] query_params: list[TypeScriptProperty] = [] for param in params: if param.schema: schema = get_openapi_type(param.schema, components) ts_prop = TypeScriptProperty( key=normalize_typescript_namespace(param.name, allow_quoted=True), required=param.required, value=parse_schema(schema), ) if param.param_in == ParamType.COOKIE: cookie_params.append(ts_prop) elif param.param_in == ParamType.HEADER: header_params.append(ts_prop) elif param.param_in == ParamType.PATH: path_params.append(ts_prop) else: query_params.append(ts_prop) result: list[TypeScriptInterface] = [] if cookie_params: result.append(TypeScriptInterface("CookieParameters", tuple(cookie_params))) if header_params: result.append(TypeScriptInterface("HeaderParameters", tuple(header_params))) if path_params: result.append(TypeScriptInterface("PathParameters", tuple(path_params))) if query_params: result.append(TypeScriptInterface("QueryParameters", tuple(query_params))) return tuple(result) def parse_request_body(body: RequestBody, components: Components) -> TypeScriptType: """Parse the schema request body. Args: body: An OpenAPI RequestBody instance. components: The OpenAPI schema Components section. Returns: A TypeScript type. """ undefined = TypeScriptPrimitive("undefined") if not body.content: return TypeScriptType("RequestBody", undefined) if content := [get_openapi_type(v.schema, components) for v in body.content.values() if v.schema]: schema = content[0] return TypeScriptType( "RequestBody", parse_schema(schema) if body.required else TypeScriptUnion((parse_schema(schema), undefined)), ) return TypeScriptType("RequestBody", undefined) def parse_responses(responses: Responses, components: Components) -> tuple[TypeScriptNamespace, ...]: """Parse a given Operation's Responses object. Args: responses: An OpenAPI Responses object. components: The OpenAPI schema Components section. Returns: A tuple of namespaces, mapping response codes to data. """ result: list[TypeScriptNamespace] = [] for http_status, response in [ (status, get_openapi_type(res, components=components)) for status, res in responses.items() ]: if response.content and ( content := [get_openapi_type(v.schema, components) for v in response.content.values() if v.schema] ): ts_type = parse_schema(content[0]) else: ts_type = TypeScriptPrimitive("undefined") containers = [ TypeScriptType("ResponseBody", ts_type), TypeScriptInterface( "ResponseHeaders", tuple( TypeScriptProperty( required=get_openapi_type(header, components=components).required, key=normalize_typescript_namespace(key, allow_quoted=True), value=TypeScriptPrimitive("string"), ) for key, header in response.headers.items() ), ) if response.headers else None, ] result.append(TypeScriptNamespace(f"Http{http_status}", tuple(c for c in containers if c))) return tuple(result) def convert_openapi_to_typescript(openapi_schema: OpenAPI, namespace: str = "API") -> TypeScriptNamespace: """Convert an OpenAPI Schema instance to a TypeScript namespace. This function is the main entry point for the TypeScript converter. Args: openapi_schema: An OpenAPI Schema instance. namespace: The namespace to use. Returns: A string representing the generated types. """ if not openapi_schema.paths: # pragma: no cover raise ValueError("OpenAPI schema has no paths") if not openapi_schema.components: # type: ignore[truthy-bool] # pragma: no cover raise ValueError("OpenAPI schema has no components") operations: list[TypeScriptNamespace] = [] for path_item in openapi_schema.paths.values(): shared_params = [ get_openapi_type(p, components=openapi_schema.components) for p in (path_item.parameters or []) ] for method in HttpMethod: if ( operation := cast("Operation | None", getattr(path_item, method.lower(), "None")) ) and operation.operation_id: params = parse_params( [ *( get_openapi_type(p, components=openapi_schema.components) for p in (operation.parameters or []) ), *shared_params, ], components=openapi_schema.components, ) request_body = ( parse_request_body( get_openapi_type(operation.request_body, components=openapi_schema.components), components=openapi_schema.components, ) if operation.request_body else None ) responses = parse_responses(operation.responses or {}, components=openapi_schema.components) operations.append( TypeScriptNamespace( normalize_typescript_namespace(operation.operation_id, allow_quoted=False), tuple(container for container in (*params, request_body, *responses) if container), ) ) return TypeScriptNamespace(namespace, tuple(operations)) litestar-2.16.0/litestar/_openapi/typescript_converter/schema_parsing.py000066400000000000000000000121341500564371300267070ustar00rootroot00000000000000from __future__ import annotations import re from typing import TYPE_CHECKING, Any, Literal, overload from litestar._openapi.typescript_converter.types import ( TypeScriptAnonymousInterface, TypeScriptArray, TypeScriptElement, TypeScriptInterface, TypeScriptIntersection, TypeScriptLiteral, TypeScriptPrimitive, TypeScriptProperty, TypeScriptUnion, ) from litestar.openapi.spec import Schema from litestar.openapi.spec.enums import OpenAPIType __all__ = ("create_interface", "is_schema_value", "normalize_typescript_namespace", "parse_schema", "parse_type_schema") if TYPE_CHECKING: from typing_extensions import TypeGuard openapi_typescript_equivalent_types = Literal[ "string", "boolean", "number", "null", "Record", "unknown[]" ] openapi_to_typescript_type_map: dict[OpenAPIType, openapi_typescript_equivalent_types] = { OpenAPIType.ARRAY: "unknown[]", OpenAPIType.BOOLEAN: "boolean", OpenAPIType.INTEGER: "number", OpenAPIType.NULL: "null", OpenAPIType.NUMBER: "number", OpenAPIType.OBJECT: "Record", OpenAPIType.STRING: "string", } invalid_namespace_re = re.compile(r"[^\w+_$]*") allowed_key_re = re.compile(r"[\w+_$]*") def normalize_typescript_namespace(value: str, allow_quoted: bool) -> str: """Normalize a namespace, e.g. variable name, or object key, to values supported by TS. Args: value: A string to normalize. allow_quoted: Whether to allow quoting the value. Returns: A normalized value """ if not allow_quoted and not value[0].isalpha() and value[0] not in {"_", "$"}: raise ValueError(f"invalid typescript namespace {value}") if allow_quoted: return value if allowed_key_re.fullmatch(value) else f'"{value}"' return invalid_namespace_re.sub("", value) def is_schema_value(value: Any) -> TypeGuard[Schema]: """Typeguard for a schema value. Args: value: An arbitrary value Returns: A typeguard boolean dictating whether the passed in value is a Schema. """ return isinstance(value, Schema) @overload def create_interface(properties: dict[str, Schema], required: set[str] | None) -> TypeScriptAnonymousInterface: ... @overload def create_interface(properties: dict[str, Schema], required: set[str] | None, name: str) -> TypeScriptInterface: ... def create_interface( properties: dict[str, Schema], required: set[str] | None = None, name: str | None = None ) -> TypeScriptAnonymousInterface | TypeScriptInterface: """Create a typescript interface from the given schema.properties values. Args: properties: schema.properties mapping. required: An optional list of required properties. name: An optional string representing the interface name. Returns: A typescript interface or anonymous interface. """ parsed_properties = tuple( TypeScriptProperty( key=normalize_typescript_namespace(key, allow_quoted=True), value=parse_schema(schema), required=key in required if required is not None else True, ) for key, schema in properties.items() ) return ( TypeScriptInterface(name=name, properties=parsed_properties) if name is not None else TypeScriptAnonymousInterface(properties=parsed_properties) ) def parse_type_schema(schema: Schema) -> TypeScriptPrimitive | TypeScriptLiteral | TypeScriptUnion: """Parse an OpenAPI schema representing a primitive type(s). Args: schema: An OpenAPI schema. Returns: A typescript type. """ if schema.enum: return TypeScriptUnion(types=tuple(TypeScriptLiteral(value=value) for value in schema.enum)) if schema.const: return TypeScriptLiteral(value=schema.const) if isinstance(schema.type, list): return TypeScriptUnion( tuple(TypeScriptPrimitive(openapi_to_typescript_type_map[s_type]) for s_type in schema.type) ) if schema.type in openapi_to_typescript_type_map and isinstance(schema.type, OpenAPIType): return TypeScriptPrimitive(openapi_to_typescript_type_map[schema.type]) raise TypeError(f"received an unexpected openapi type: {schema.type}") # pragma: no cover def parse_schema(schema: Schema) -> TypeScriptElement: """Parse an OpenAPI schema object recursively to create typescript types. Args: schema: An OpenAPI Schema object. Returns: A typescript type. """ if schema.all_of: return TypeScriptIntersection(tuple(parse_schema(s) for s in schema.all_of if is_schema_value(s))) if schema.one_of: return TypeScriptUnion(tuple(parse_schema(s) for s in schema.one_of if is_schema_value(s))) if is_schema_value(schema.items): return TypeScriptArray(parse_schema(schema.items)) if schema.type == OpenAPIType.OBJECT: return create_interface( properties={k: v for k, v in schema.properties.items() if is_schema_value(v)} if schema.properties else {}, required=set(schema.required) if schema.required else None, ) return parse_type_schema(schema=schema) litestar-2.16.0/litestar/_openapi/typescript_converter/types.py000066400000000000000000000167601500564371300251010ustar00rootroot00000000000000from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Any, Literal __all__ = ( "TypeScriptAnonymousInterface", "TypeScriptArray", "TypeScriptConst", "TypeScriptContainer", "TypeScriptElement", "TypeScriptEnum", "TypeScriptInterface", "TypeScriptIntersection", "TypeScriptLiteral", "TypeScriptNamespace", "TypeScriptPrimitive", "TypeScriptProperty", "TypeScriptType", "TypeScriptUnion", ) def _as_string(value: Any) -> str: if isinstance(value, str): return f'"{value}"' if isinstance(value, bool): return "true" if value else "false" return "null" if value is None else str(value) class TypeScriptElement(ABC): """A class representing a TypeScript type element.""" @abstractmethod def write(self) -> str: """Write a typescript value corresponding to the given typescript element. Returns: A typescript string """ raise NotImplementedError("") class TypeScriptContainer(TypeScriptElement): """A class representing a TypeScript type container.""" name: str @abstractmethod def write(self) -> str: """Write a typescript value corresponding to the given typescript container. Returns: A typescript string """ raise NotImplementedError("") @dataclass(unsafe_hash=True) class TypeScriptIntersection(TypeScriptElement): """A class representing a TypeScript intersection type.""" types: tuple[TypeScriptElement, ...] def write(self) -> str: """Write a typescript intersection value. Example: { prop: string } & { another: number } Returns: A typescript string """ return " & ".join(t.write() for t in self.types) @dataclass(unsafe_hash=True) class TypeScriptUnion(TypeScriptElement): """A class representing a TypeScript union type.""" types: tuple[TypeScriptElement, ...] def write(self) -> str: """Write a typescript union value. Example: string | number Returns: A typescript string """ return " | ".join(sorted(t.write() for t in self.types)) @dataclass(unsafe_hash=True) class TypeScriptPrimitive(TypeScriptElement): """A class representing a TypeScript primitive type.""" type: Literal[ "string", "number", "boolean", "any", "null", "undefined", "symbol", "Record", "unknown[]" ] def write(self) -> str: """Write a typescript primitive type. Example: null Returns: A typescript string """ return self.type @dataclass(unsafe_hash=True) class TypeScriptLiteral(TypeScriptElement): """A class representing a TypeScript literal type.""" value: str | int | float | bool | None def write(self) -> str: """Write a typescript literal type. Example: "someValue" Returns: A typescript string """ return _as_string(self.value) @dataclass(unsafe_hash=True) class TypeScriptArray(TypeScriptElement): """A class representing a TypeScript array type.""" item_type: TypeScriptElement def write(self) -> str: """Write a typescript array type. Example: number[] Returns: A typescript string """ value = ( f"({self.item_type.write()})" if isinstance(self.item_type, (TypeScriptUnion, TypeScriptIntersection)) else self.item_type.write() ) return f"{value}[]" @dataclass(unsafe_hash=True) class TypeScriptProperty(TypeScriptElement): """A class representing a TypeScript interface property.""" required: bool key: str value: TypeScriptElement def write(self) -> str: """Write a typescript property. This class is used exclusively inside interfaces. Example: key: string; optional?: number; Returns: A typescript string """ return f"{self.key}{':' if self.required else '?:'} {self.value.write()};" @dataclass(unsafe_hash=True) class TypeScriptAnonymousInterface(TypeScriptElement): """A class representing a TypeScript anonymous interface.""" properties: tuple[TypeScriptProperty, ...] def write(self) -> str: """Write a typescript interface object, without a name. Example: { key: string; optional?: number; } Returns: A typescript string """ props = "\t" + "\n\t".join([prop.write() for prop in sorted(self.properties, key=lambda prop: prop.key)]) return f"{{\n{props}\n}}" @dataclass(unsafe_hash=True) class TypeScriptInterface(TypeScriptContainer): """A class representing a TypeScript interface.""" name: str properties: tuple[TypeScriptProperty, ...] def write(self) -> str: """Write a typescript interface. Example: export interface MyInterface { key: string; optional?: number; }; Returns: A typescript string """ interface = TypeScriptAnonymousInterface(properties=self.properties) return f"export interface {self.name} {interface.write()};" @dataclass(unsafe_hash=True) class TypeScriptEnum(TypeScriptContainer): """A class representing a TypeScript enum.""" name: str values: tuple[tuple[str, str], ...] | tuple[tuple[str, int | float], ...] def write(self) -> str: """Write a typescript enum. Example: export enum MyEnum { DOG = "canine", CAT = "feline", }; Returns: A typescript string """ members = "\t" + "\n\t".join( [f"{key} = {_as_string(value)}," for key, value in sorted(self.values, key=lambda member: member[0])] ) return f"export enum {self.name} {{\n{members}\n}};" @dataclass(unsafe_hash=True) class TypeScriptType(TypeScriptContainer): """A class representing a TypeScript type.""" name: str value: TypeScriptElement def write(self) -> str: """Write a typescript type. Example: export type MyType = number | "42"; Returns: A typescript string """ return f"export type {self.name} = {self.value.write()};" @dataclass(unsafe_hash=True) class TypeScriptConst(TypeScriptContainer): """A class representing a TypeScript const.""" name: str value: TypeScriptPrimitive | TypeScriptLiteral def write(self) -> str: """Write a typescript const. Example: export const MyConst: number; Returns: A typescript string """ return f"export const {self.name}: {self.value.write()};" @dataclass(unsafe_hash=True) class TypeScriptNamespace(TypeScriptContainer): """A class representing a TypeScript namespace.""" name: str values: tuple[TypeScriptContainer, ...] def write(self) -> str: """Write a typescript namespace. Example: export MyNamespace { export const MyConst: number; } Returns: A typescript string """ members = "\t" + "\n\n\t".join([value.write() for value in sorted(self.values, key=lambda el: el.name)]) return f"export namespace {self.name} {{\n{members}\n}};" litestar-2.16.0/litestar/_openapi/utils.py000066400000000000000000000027211500564371300205700ustar00rootroot00000000000000from __future__ import annotations import re from typing import TYPE_CHECKING from litestar.types.internal_types import PathParameterDefinition if TYPE_CHECKING: from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.types import Method __all__ = ("SEPARATORS_CLEANUP_PATTERN", "default_operation_id_creator") SEPARATORS_CLEANUP_PATTERN = re.compile(r"[!#$%&'*+\-.^_`|~:]+") def default_operation_id_creator( route_handler: HTTPRouteHandler, http_method: Method, path_components: list[str | PathParameterDefinition], ) -> str: """Create a unique 'operationId' for an OpenAPI PathItem entry. Args: route_handler: The HTTP Route Handler instance. http_method: The HTTP method for the given PathItem. path_components: A list of path components. Returns: A camelCased operationId created from the handler function name, http method and path components. """ handler_namespace = ( http_method.title() + route_handler.handler_name.title() if len(route_handler.http_methods) > 1 else route_handler.handler_name.title() ) components_namespace = "" for component in (c.name if isinstance(c, PathParameterDefinition) else c for c in path_components): if component.title() not in components_namespace: components_namespace += component.title() return SEPARATORS_CLEANUP_PATTERN.sub("", components_namespace + handler_namespace) litestar-2.16.0/litestar/_parsers.py000066400000000000000000000036771500564371300174670ustar00rootroot00000000000000from __future__ import annotations from collections import defaultdict from functools import lru_cache from http.cookies import _unquote as unquote_cookie from urllib.parse import unquote try: from fast_query_parsers import parse_query_string as parse_qsl except ImportError: from urllib.parse import parse_qsl as _parse_qsl def parse_qsl(qs: bytes, separator: str) -> list[tuple[str, str]]: return _parse_qsl(qs.decode("latin-1"), keep_blank_values=True, separator=separator) __all__ = ("parse_cookie_string", "parse_query_string", "parse_url_encoded_form_data") @lru_cache(1024) def parse_url_encoded_form_data(encoded_data: bytes) -> dict[str, str | list[str]]: """Parse an url encoded form data dict. Args: encoded_data: The encoded byte string. Returns: A parsed dict. """ decoded_dict: defaultdict[str, list[str]] = defaultdict(list) for k, v in parse_qsl(encoded_data, separator="&"): decoded_dict[k].append(v) return {k: v if len(v) > 1 else v[0] for k, v in decoded_dict.items()} @lru_cache(1024) def parse_query_string(query_string: bytes) -> tuple[tuple[str, str], ...]: """Parse a query string into a tuple of key value pairs. Args: query_string: A query string. Returns: A tuple of key value pairs. """ return tuple(parse_qsl(query_string, separator="&")) @lru_cache(1024) def parse_cookie_string(cookie_string: str) -> dict[str, str]: """Parse a cookie string into a dictionary of values. Args: cookie_string: A cookie string. Returns: A string keyed dictionary of values """ cookies = [cookie.split("=", 1) if "=" in cookie else ("", cookie) for cookie in cookie_string.split(";")] output: dict[str, str] = { k: unquote(unquote_cookie(v)) for k, v in filter( lambda x: x[0] or x[1], ((k.strip(), v.strip()) for k, v in cookies), ) } return output litestar-2.16.0/litestar/_signature/000077500000000000000000000000001500564371300174225ustar00rootroot00000000000000litestar-2.16.0/litestar/_signature/__init__.py000066400000000000000000000001011500564371300215230ustar00rootroot00000000000000from .model import SignatureModel __all__ = ("SignatureModel",) litestar-2.16.0/litestar/_signature/model.py000066400000000000000000000275011500564371300211010ustar00rootroot00000000000000# ruff: noqa: UP006, UP007 from __future__ import annotations import re from functools import partial from pathlib import Path, PurePath from typing import ( TYPE_CHECKING, Any, Callable, ClassVar, Dict, Literal, Optional, Sequence, Set, Type, TypedDict, Union, cast, ) from uuid import UUID from msgspec import NODEFAULT, Meta, Struct, ValidationError, convert, defstruct from msgspec.structs import asdict from typing_extensions import Annotated from litestar._signature.types import ExtendedMsgSpecValidationError from litestar._signature.utils import ( _get_decoder_for_type, _normalize_annotation, _validate_signature_dependencies, ) from litestar.datastructures.state import ImmutableState from litestar.datastructures.url import URL from litestar.dto import AbstractDTO, DTOData from litestar.enums import ParamType, ScopeType from litestar.exceptions import InternalServerException, ValidationException from litestar.params import KwargDefinition, ParameterKwarg from litestar.typing import FieldDefinition # noqa from litestar.utils import get_origin_or_inner_type, is_class_and_subclass from litestar.utils.dataclass import simple_asdict if TYPE_CHECKING: from typing_extensions import NotRequired from litestar.connection import ASGIConnection from litestar.types import AnyCallable, TypeDecodersSequence from litestar.utils.signature import ParsedSignature __all__ = ( "ErrorMessage", "SignatureModel", ) class ErrorMessage(TypedDict): # key may not be set in some cases, like when a query param is set but # doesn't match the required length during `attrs` validation # in this case, we don't show a key at all as it will be empty key: NotRequired[str] message: str source: NotRequired[Literal["body"] | ParamType] MSGSPEC_CONSTRAINT_FIELDS = ( "gt", "ge", "lt", "le", "multiple_of", "pattern", "min_length", "max_length", ) ERR_RE = re.compile(r"`\$\.(.+)`$") DEFAULT_TYPE_DECODERS = [ (lambda x: is_class_and_subclass(x, (Path, PurePath, ImmutableState, UUID)), lambda t, v: t(v)), ] def _deserializer(target_type: Any, value: Any, default_deserializer: Callable[[Any, Any], Any]) -> Any: if isinstance(value, DTOData): return value try: if isinstance(value, target_type): return value except TypeError as exc: if (origin := get_origin_or_inner_type(target_type)) is not None: if isinstance(value, origin): return value else: raise exc if decoder := getattr(target_type, "_decoder", None): return decoder(target_type, value) return default_deserializer(target_type, value) class SignatureModel(Struct): """Model that represents a function signature that uses a msgspec specific type or types.""" _data_dto: ClassVar[Optional[Type[AbstractDTO]]] _dependency_name_set: ClassVar[Set[str]] _fields: ClassVar[Dict[str, FieldDefinition]] _return_annotation: ClassVar[Any] @classmethod def _create_exception(cls, connection: ASGIConnection, messages: list[ErrorMessage]) -> Exception: """Create an exception class - either a ValidationException or an InternalServerException, depending on whether the failure is in client provided values or injected dependencies. Args: connection: An ASGI connection instance. messages: A list of error messages. Returns: An Exception """ method = connection.method if hasattr(connection, "method") else ScopeType.WEBSOCKET # pyright: ignore if client_errors := [ err_message for err_message in messages if ("key" in err_message and err_message["key"] not in cls._dependency_name_set) or "key" not in err_message ]: path = URL.from_components( path=connection.url.path, query=connection.url.query, ) return ValidationException(detail=f"Validation failed for {method} {path}", extra=client_errors) return InternalServerException() @classmethod def _build_error_message(cls, keys: Sequence[str], exc_msg: str, connection: ASGIConnection) -> ErrorMessage: """Build an error message. Args: keys: A list of keys. exc_msg: A message. connection: An ASGI connection instance. Returns: An ErrorMessage """ message: ErrorMessage = {"message": exc_msg.split(" - ")[0]} if keys: message["key"] = key = ".".join(keys) if keys[0].startswith("data"): message["key"] = message["key"].replace("data.", "") message["source"] = "body" elif key in connection.query_params: message["source"] = ParamType.QUERY elif key in connection.path_params: message["source"] = ParamType.PATH elif key in cls._fields and isinstance(cls._fields[key].kwarg_definition, ParameterKwarg): if cast(ParameterKwarg, cls._fields[key].kwarg_definition).cookie: message["source"] = ParamType.COOKIE elif cast(ParameterKwarg, cls._fields[key].kwarg_definition).header: message["source"] = ParamType.HEADER else: message["source"] = ParamType.QUERY return message @classmethod def _collect_errors( cls, deserializer: Callable[[Any, Any], Any], kwargs: dict[str, Any] ) -> list[tuple[str, Exception]]: exceptions: list[tuple[str, Exception]] = [] for field_name in cls._fields: try: raw_value = kwargs[field_name] annotation = cls.__annotations__[field_name] convert(raw_value, type=annotation, strict=False, dec_hook=deserializer, str_keys=True) except Exception as e: # noqa: BLE001 exceptions.append((field_name, e)) return exceptions @classmethod def parse_values_from_connection_kwargs(cls, connection: ASGIConnection, kwargs: dict[str, Any]) -> dict[str, Any]: """Extract values from the connection instance and return a dict of parsed values. Args: connection: The ASGI connection instance. kwargs: A dictionary of kwargs. Raises: ValidationException: If validation failed. InternalServerException: If another exception has been raised. Returns: A dictionary of parsed values """ messages: list[ErrorMessage] = [] deserializer = partial(_deserializer, default_deserializer=connection.route_handler.default_deserializer) try: return convert(kwargs, cls, strict=False, dec_hook=deserializer, str_keys=True).to_dict() except ExtendedMsgSpecValidationError as e: for exc in e.errors: keys = [str(loc) for loc in exc["loc"]] message = cls._build_error_message(keys=keys, exc_msg=exc["msg"], connection=connection) messages.append(message) raise cls._create_exception(messages=messages, connection=connection) from e except ValidationError as e: for field_name, exc in cls._collect_errors(deserializer=deserializer, kwargs=kwargs): # type: ignore[assignment] match = ERR_RE.search(str(exc)) keys = [field_name, str(match.group(1))] if match else [field_name] message = cls._build_error_message(keys=keys, exc_msg=str(exc), connection=connection) messages.append(message) raise cls._create_exception(messages=messages, connection=connection) from e def to_dict(self) -> dict[str, Any]: """Normalize access to the signature model's dictionary method, because different backends use different methods for this. Returns: A dictionary of string keyed values. """ return asdict(self) @classmethod def create( cls, dependency_name_set: set[str], fn: AnyCallable, parsed_signature: ParsedSignature, type_decoders: TypeDecodersSequence, data_dto: type[AbstractDTO] | None = None, ) -> type[SignatureModel]: fn_name = ( fn_name if (fn_name := getattr(fn, "__name__", "anonymous")) and fn_name != "" else "anonymous" ) dependency_names = _validate_signature_dependencies( dependency_name_set=dependency_name_set, fn_name=fn_name, parsed_signature=parsed_signature ) struct_fields: list[tuple[str, Any, Any]] = [] for field_definition in parsed_signature.parameters.values(): meta_data: Meta | None = None if isinstance(field_definition.kwarg_definition, KwargDefinition): meta_kwargs: dict[str, Any] = {"extra": {}} kwarg_definition = simple_asdict(field_definition.kwarg_definition, exclude_empty=True) if min_items := kwarg_definition.pop("min_items", None): meta_kwargs["min_length"] = min_items if max_items := kwarg_definition.pop("max_items", None): meta_kwargs["max_length"] = max_items for k, v in kwarg_definition.items(): if hasattr(Meta, k) and v is not None: meta_kwargs[k] = v else: meta_kwargs["extra"][k] = v meta_data = Meta(**meta_kwargs) annotation = cls._create_annotation( field_definition=field_definition, type_decoders=[*(type_decoders or []), *DEFAULT_TYPE_DECODERS], meta_data=meta_data, data_dto=data_dto, ) default = field_definition.default if field_definition.has_default else NODEFAULT struct_fields.append((field_definition.name, annotation, default)) return defstruct( # type:ignore[return-value] f"{fn_name}_signature_model", struct_fields, bases=(cls,), module=getattr(fn, "__module__", None), namespace={ "_return_annotation": parsed_signature.return_type.annotation, "_dependency_name_set": dependency_names, "_fields": parsed_signature.parameters, "_data_dto": data_dto, }, kw_only=True, ) @classmethod def _create_annotation( cls, field_definition: FieldDefinition, type_decoders: TypeDecodersSequence, meta_data: Meta | None = None, data_dto: type[AbstractDTO] | None = None, ) -> Any: # DTOs have already validated their data, so we can just use Any here if field_definition.name == "data" and data_dto: return Any annotation = _normalize_annotation(field_definition=field_definition) if annotation is Any: return annotation if field_definition.is_union: types = [ cls._create_annotation( field_definition=inner_type, type_decoders=type_decoders, meta_data=meta_data, ) for inner_type in field_definition.inner_types if not inner_type.is_none_type ] return Optional[Union[tuple(types)]] if field_definition.is_optional else Union[tuple(types)] # pyright: ignore if decoder := _get_decoder_for_type(annotation, type_decoders=type_decoders): # FIXME: temporary (hopefully) hack, see: https://github.com/jcrist/msgspec/issues/497 setattr(annotation, "_decoder", decoder) if meta_data: annotation = Annotated[annotation, meta_data] # pyright: ignore return annotation litestar-2.16.0/litestar/_signature/types.py000066400000000000000000000004251500564371300211410ustar00rootroot00000000000000from __future__ import annotations from typing import Any from msgspec import ValidationError class ExtendedMsgSpecValidationError(ValidationError): def __init__(self, errors: list[dict[str, Any]]) -> None: self.errors = errors super().__init__(errors) litestar-2.16.0/litestar/_signature/utils.py000066400000000000000000000040661500564371300211420ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable from litestar.constants import SKIP_VALIDATION_NAMES from litestar.exceptions import ImproperlyConfiguredException from litestar.params import DependencyKwarg from litestar.types import Empty, TypeDecodersSequence if TYPE_CHECKING: from litestar.typing import FieldDefinition from litestar.utils.signature import ParsedSignature __all__ = ("_get_decoder_for_type", "_normalize_annotation", "_validate_signature_dependencies") def _validate_signature_dependencies( dependency_name_set: set[str], fn_name: str, parsed_signature: ParsedSignature ) -> set[str]: """Validate dependencies of ``parsed_signature``. Args: dependency_name_set: A set of dependency names fn_name: A callable's name. parsed_signature: A parsed signature. Returns: A set of validated dependency names. """ dependency_names: set[str] = set(dependency_name_set) for parameter in parsed_signature.parameters.values(): if isinstance(parameter.kwarg_definition, DependencyKwarg) and parameter.name not in dependency_name_set: if not parameter.is_optional and parameter.default is Empty: raise ImproperlyConfiguredException( f"Explicit dependency '{parameter.name}' for '{fn_name}' has no default value, " f"or provided dependency." ) dependency_names.add(parameter.name) return dependency_names def _normalize_annotation(field_definition: FieldDefinition) -> Any: if field_definition.name in SKIP_VALIDATION_NAMES or ( isinstance(field_definition.kwarg_definition, DependencyKwarg) and field_definition.kwarg_definition.skip_validation ): return Any return field_definition.annotation def _get_decoder_for_type(target_type: Any, type_decoders: TypeDecodersSequence) -> Callable[[type, Any], Any] | None: return next( (decoder for predicate, decoder in type_decoders if predicate(target_type)), None, ) litestar-2.16.0/litestar/app.py000066400000000000000000001214111500564371300164140ustar00rootroot00000000000000from __future__ import annotations import inspect import logging import os import pdb # noqa: T100 import warnings from contextlib import ( AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager, suppress, ) from datetime import date, datetime, time, timedelta from functools import partial from itertools import chain from pathlib import Path from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Iterable, Mapping, Sequence, TypedDict, cast from uuid import UUID from litestar._asgi import ASGIRouter from litestar._asgi.utils import get_route_handlers, wrap_in_exception_handler from litestar._openapi.plugin import OpenAPIPlugin from litestar._openapi.schema_generation import openapi_schema_plugins from litestar.config.allowed_hosts import AllowedHostsConfig from litestar.config.app import AppConfig, ExperimentalFeatures from litestar.config.response_cache import ResponseCacheConfig from litestar.connection import Request, WebSocket from litestar.datastructures.state import State from litestar.events.emitter import BaseEventEmitterBackend, SimpleEventEmitter from litestar.exceptions import ( LitestarWarning, MissingDependencyException, NoRouteMatchFoundException, ) from litestar.logging.config import LoggingConfig, get_logger_placeholder from litestar.middleware._internal.cors import CORSMiddleware from litestar.openapi.config import OpenAPIConfig from litestar.plugins import ( CLIPluginProtocol, InitPluginProtocol, OpenAPISchemaPluginProtocol, PluginProtocol, PluginRegistry, SerializationPluginProtocol, ) from litestar.plugins.base import CLIPlugin from litestar.router import Router from litestar.routes import ASGIRoute, HTTPRoute, WebSocketRoute from litestar.static_files.base import StaticFiles from litestar.stores.registry import StoreRegistry from litestar.types import Empty, TypeDecodersSequence from litestar.types.internal_types import PathParameterDefinition, TemplateConfigType from litestar.utils import deprecated, ensure_async_callable, join_paths, unique from litestar.utils.dataclass import extract_dataclass_items from litestar.utils.predicates import is_async_callable from litestar.utils.warnings import warn_pdb_on_exception if TYPE_CHECKING: from typing_extensions import Self from litestar.config.compression import CompressionConfig from litestar.config.cors import CORSConfig from litestar.config.csrf import CSRFConfig from litestar.contrib.opentelemetry import OpenTelemetryPlugin from litestar.datastructures import CacheControlHeader, ETag from litestar.dto import AbstractDTO from litestar.events.listener import EventListener from litestar.logging.config import BaseLoggingConfig from litestar.openapi.spec import SecurityRequirement from litestar.openapi.spec.open_api import OpenAPI from litestar.response import Response from litestar.static_files.config import StaticFilesConfig from litestar.stores.base import Store from litestar.types import ( AfterExceptionHookHandler, AfterRequestHookHandler, AfterResponseHookHandler, AnyCallable, ASGIApp, BeforeMessageSendHookHandler, BeforeRequestHookHandler, ControllerRouterHandler, Debugger, Dependencies, EmptyType, ExceptionHandlersMap, GetLogger, Guard, LifeSpanReceive, LifeSpanScope, LifeSpanSend, Logger, Message, Middleware, OnAppInitHandler, ParametersMap, Receive, ResponseCookies, ResponseHeaders, RouteHandlerType, Scope, Send, TypeEncodersMap, ) from litestar.types.callable_types import LifespanHook __all__ = ("DEFAULT_OPENAPI_CONFIG", "HandlerIndex", "Litestar") DEFAULT_OPENAPI_CONFIG = OpenAPIConfig(title="Litestar API", version="1.0.0") """The default OpenAPI config used if not configuration is explicitly passed to the :class:`Litestar <.app.Litestar>` instance constructor. """ class HandlerIndex(TypedDict): """Map route handler names to a mapping of paths + route handler. It's returned from the 'get_handler_index_by_name' utility method. """ paths: list[str] """Full route paths to the route handler.""" handler: RouteHandlerType """Route handler instance.""" identifier: str """Unique identifier of the handler. Either equal to :attr`__name__ ` attribute or ``__str__`` value of the handler. """ class Litestar(Router): """The Litestar application. ``Litestar`` is the root level of the app - it has the base path of ``/`` and all root level Controllers, Routers and Route Handlers should be registered on it. """ __slots__ = ( "_debug", "_lifespan_managers", "_openapi_schema", "_server_lifespan_managers", "_static_files_config", "after_exception", "allowed_hosts", "asgi_handler", "asgi_router", "before_send", "compression_config", "cors_config", "csrf_config", "debugger_module", "event_emitter", "experimental_features", "get_logger", "logger", "logging_config", "multipart_form_part_limit", "on_shutdown", "on_startup", "openapi_config", "pdb_on_exception", "plugins", "response_cache_config", "route_map", "state", "stores", "template_engine", ) def __init__( self, route_handlers: Sequence[ControllerRouterHandler] | None = None, *, after_exception: Sequence[AfterExceptionHookHandler] | None = None, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, allowed_hosts: Sequence[str] | AllowedHostsConfig | None = None, before_request: BeforeRequestHookHandler | None = None, before_send: Sequence[BeforeMessageSendHookHandler] | None = None, cache_control: CacheControlHeader | None = None, compression_config: CompressionConfig | None = None, cors_config: CORSConfig | None = None, csrf_config: CSRFConfig | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, debug: bool | None = None, dependencies: Dependencies | None = None, etag: ETag | None = None, event_emitter_backend: type[BaseEventEmitterBackend] = SimpleEventEmitter, exception_handlers: ExceptionHandlersMap | None = None, guards: Sequence[Guard] | None = None, include_in_schema: bool | EmptyType = Empty, listeners: Sequence[EventListener] | None = None, logging_config: BaseLoggingConfig | EmptyType | None = Empty, middleware: Sequence[Middleware] | None = None, multipart_form_part_limit: int = 1000, on_app_init: Sequence[OnAppInitHandler] | None = None, on_shutdown: Sequence[LifespanHook] | None = None, on_startup: Sequence[LifespanHook] | None = None, openapi_config: OpenAPIConfig | None = DEFAULT_OPENAPI_CONFIG, opt: Mapping[str, Any] | None = None, parameters: ParametersMap | None = None, path: str | None = None, plugins: Sequence[PluginProtocol] | None = None, request_class: type[Request] | None = None, request_max_body_size: int | None = 10_000_000, response_cache_config: ResponseCacheConfig | None = None, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, security: Sequence[SecurityRequirement] | None = None, signature_namespace: Mapping[str, Any] | None = None, signature_types: Sequence[Any] | None = None, state: State | None = None, static_files_config: Sequence[StaticFilesConfig] | None = None, stores: StoreRegistry | dict[str, Store] | None = None, tags: Sequence[str] | None = None, template_config: TemplateConfigType | None = None, type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, websocket_class: type[WebSocket] | None = None, lifespan: Sequence[Callable[[Litestar], AbstractAsyncContextManager] | AbstractAsyncContextManager] | None = None, pdb_on_exception: bool | None = None, debugger_module: Debugger = pdb, experimental_features: Iterable[ExperimentalFeatures] | None = None, ) -> None: """Initialize a ``Litestar`` application. Args: after_exception: A sequence of :class:`exception hook handlers <.types.AfterExceptionHookHandler>`. This hook is called after an exception occurs. In difference to exception handlers, it is not meant to return a response - only to process the exception (e.g. log it, send it to Sentry etc.). after_request: A sync or async function executed after the route handler function returned and the response object has been resolved. Receives the response object. after_response: A sync or async function called after the response has been awaited. It receives the :class:`Request <.connection.Request>` object and should not return any values. allowed_hosts: A sequence of allowed hosts, or an :class:`AllowedHostsConfig <.config.allowed_hosts.AllowedHostsConfig>` instance. Enables the builtin allowed hosts middleware. before_request: A sync or async function called immediately before calling the route handler. Receives the :class:`Request <.connection.Request>` instance and any non-``None`` return value is used for the response, bypassing the route handler. before_send: A sequence of :class:`before send hook handlers <.types.BeforeMessageSendHookHandler>`. Called when the ASGI send function is called. cache_control: A ``cache-control`` header of type :class:`CacheControlHeader ` to add to route handlers of this app. Can be overridden by route handlers. compression_config: Configures compression behaviour of the application, this enabled a builtin or user defined Compression middleware. cors_config: If set, configures CORS handling for the application. csrf_config: If set, configures :class:`CSRFMiddleware <.middleware.csrf.CSRFMiddleware>`. debug: If ``True``, app errors rendered as HTML with a stack trace. dependencies: A string keyed mapping of dependency :class:`Providers <.di.Provide>`. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` to add to route handlers of this app. Can be overridden by route handlers. event_emitter_backend: A subclass of :class:`BaseEventEmitterBackend <.events.emitter.BaseEventEmitterBackend>`. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. lifespan: A list of callables returning async context managers, wrapping the lifespan of the ASGI application listeners: A sequence of :class:`EventListener <.events.listener.EventListener>`. logging_config: A subclass of :class:`BaseLoggingConfig <.logging.config.BaseLoggingConfig>`. middleware: A sequence of :class:`Middleware <.types.Middleware>`. multipart_form_part_limit: The maximal number of allowed parts in a multipart/formdata request. This limit is intended to protect from DoS attacks. on_app_init: A sequence of :class:`OnAppInitHandler <.types.OnAppInitHandler>` instances. Handlers receive an instance of :class:`AppConfig <.config.app.AppConfig>` that will have been initially populated with the parameters passed to :class:`Litestar `, and must return an instance of same. If more than one handler is registered they are called in the order they are provided. on_shutdown: A sequence of :class:`LifespanHook <.types.LifespanHook>` called during application shutdown. on_startup: A sequence of :class:`LifespanHook ` called during application startup. openapi_config: Defaults to :attr:`DEFAULT_OPENAPI_CONFIG` opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request ` or :class:`ASGI Scope <.types.Scope>`. parameters: A mapping of :class:`Parameter <.params.Parameter>` definitions available to all application paths. path: A path fragment that is prefixed to all route handlers, controllers and routers associated with the application instance. .. versionadded:: 2.8.0 pdb_on_exception: Drop into the PDB when an exception occurs. debugger_module: A `pdb`-like debugger module that supports the `post_mortem()` protocol. This module will be used when `pdb_on_exception` is set to True. plugins: Sequence of plugins. request_class: An optional subclass of :class:`Request <.connection.Request>` to use for http connections. request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded, a '413 - Request Entity Too Large' error response is returned. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as the app's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>`. response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` response_cache_config: Configures caching behavior of the application. return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. route_handlers: A sequence of route handlers, which can include instances of :class:`Router <.router.Router>`, subclasses of :class:`Controller <.controller.Controller>` or any callable decorated by the route handler decorators. security: A sequence of dicts that will be added to the schema of all route handlers in the application. See :data:`SecurityRequirement <.openapi.spec.SecurityRequirement>` for details. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. signature_types: A sequence of types for use in forward reference resolution during signature modelling. These types will be added to the signature namespace using their ``__name__`` attribute. state: An optional :class:`State <.datastructures.State>` for application state. static_files_config: A sequence of :class:`StaticFilesConfig <.static_files.StaticFilesConfig>` stores: Central registry of :class:`Store <.stores.base.Store>` that will be available throughout the application. If this is a dictionary to it will be passed to a :class:`StoreRegistry <.stores.registry.StoreRegistry>`. If it is a :class:`StoreRegistry <.stores.registry.StoreRegistry>`, this instance will be used directly. tags: A sequence of string tags that will be appended to the schema of all route handlers under the application. template_config: An instance of :class:`TemplateConfig <.template.TemplateConfig>` type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. type_encoders: A mapping of types to callables that transform them into types supported for serialization. websocket_class: An optional subclass of :class:`WebSocket <.connection.WebSocket>` to use for websocket connections. experimental_features: An iterable of experimental features to enable """ if logging_config is Empty: logging_config = LoggingConfig() if debug is None: debug = os.getenv("LITESTAR_DEBUG", "0") == "1" if pdb_on_exception is None: pdb_on_exception = os.getenv("LITESTAR_PDB", "0") == "1" config = AppConfig( after_exception=list(after_exception or []), after_request=after_request, after_response=after_response, allowed_hosts=allowed_hosts if isinstance(allowed_hosts, AllowedHostsConfig) else list(allowed_hosts or []), before_request=before_request, before_send=list(before_send or []), cache_control=cache_control, compression_config=compression_config, cors_config=cors_config, csrf_config=csrf_config, debug=debug, dependencies=dict(dependencies or {}), dto=dto, etag=etag, event_emitter_backend=event_emitter_backend, exception_handlers=exception_handlers or {}, guards=list(guards or []), include_in_schema=include_in_schema, lifespan=list(lifespan or []), listeners=list(listeners or []), logging_config=logging_config, middleware=list(middleware or []), multipart_form_part_limit=multipart_form_part_limit, on_shutdown=list(on_shutdown or []), on_startup=list(on_startup or []), openapi_config=openapi_config, opt=dict(opt or {}), path=path or "", parameters=parameters or {}, pdb_on_exception=pdb_on_exception, debugger_module=debugger_module, plugins=self._get_default_plugins(list(plugins or [])), request_class=request_class, request_max_body_size=request_max_body_size, response_cache_config=response_cache_config or ResponseCacheConfig(), response_class=response_class, response_cookies=response_cookies or [], response_headers=response_headers or [], return_dto=return_dto, route_handlers=list(route_handlers) if route_handlers is not None else [], security=list(security or []), signature_namespace=dict(signature_namespace or {}), signature_types=list(signature_types or []), state=state or State(), static_files_config=list(static_files_config or []), stores=stores, tags=list(tags or []), template_config=template_config, type_encoders=type_encoders, type_decoders=type_decoders, websocket_class=websocket_class, experimental_features=list(experimental_features or []), ) config.plugins.extend([OpenAPIPlugin(self), *openapi_schema_plugins]) for handler in chain( on_app_init or [], (p.on_app_init for p in config.plugins if isinstance(p, InitPluginProtocol)), [self._patch_opentelemetry_middleware], ): config = handler(config) # pyright: ignore self.plugins = PluginRegistry(config.plugins) self._openapi_schema: OpenAPI | None = None self._debug: bool = True self.stores: StoreRegistry = ( config.stores if isinstance(config.stores, StoreRegistry) else StoreRegistry(config.stores) ) self._lifespan_managers = config.lifespan for store in self.stores._stores.values(): self._lifespan_managers.append(store) self._server_lifespan_managers = [p.server_lifespan for p in config.plugins or [] if isinstance(p, CLIPlugin)] self.experimental_features = frozenset(config.experimental_features or []) if ExperimentalFeatures.DTO_CODEGEN in self.experimental_features: warnings.warn( "Use of redundant experimental feature flag DTO_CODEGEN. " "DTO codegen backend is enabled by default since Litestar 2.8. The " "DTO_CODEGEN feature flag can be safely removed from the configuration " "and will be removed in version 3.0.", category=LitestarWarning, stacklevel=2, ) self.get_logger: GetLogger = get_logger_placeholder self.logger: Logger | None = None self.routes: list[HTTPRoute | ASGIRoute | WebSocketRoute] = [] self.after_exception = [ensure_async_callable(h) for h in config.after_exception] self.allowed_hosts = cast("AllowedHostsConfig | None", config.allowed_hosts) self.before_send = [ensure_async_callable(h) for h in config.before_send] self.compression_config = config.compression_config self.cors_config = config.cors_config self.csrf_config = config.csrf_config self.event_emitter = config.event_emitter_backend(listeners=config.listeners) self.logging_config = config.logging_config self.multipart_form_part_limit = config.multipart_form_part_limit self.on_shutdown = config.on_shutdown self.on_startup = config.on_startup self.openapi_config = config.openapi_config self.request_class: type[Request] = config.request_class or Request self.response_cache_config = config.response_cache_config self.state = config.state self._static_files_config = config.static_files_config self.template_engine = config.template_config.engine_instance if config.template_config else None self.websocket_class: type[WebSocket] = config.websocket_class or WebSocket self.debug = config.debug self.pdb_on_exception: bool = config.pdb_on_exception self.debugger_module: Debugger = config.debugger_module self.include_in_schema = include_in_schema if self.pdb_on_exception: warn_pdb_on_exception() try: from starlette.exceptions import HTTPException as StarletteHTTPException from litestar.middleware._internal.exceptions.middleware import _starlette_exception_handler config.exception_handlers.setdefault(StarletteHTTPException, _starlette_exception_handler) except ImportError: pass super().__init__( after_request=config.after_request, after_response=config.after_response, before_request=config.before_request, cache_control=config.cache_control, dependencies=config.dependencies, dto=config.dto, etag=config.etag, exception_handlers=config.exception_handlers, guards=config.guards, middleware=config.middleware, opt=config.opt, parameters=config.parameters, path=config.path, request_class=self.request_class, request_max_body_size=request_max_body_size, response_class=config.response_class, response_cookies=config.response_cookies, response_headers=config.response_headers, return_dto=config.return_dto, # route handlers are registered below route_handlers=[], security=config.security, signature_namespace=config.signature_namespace, signature_types=config.signature_types, tags=config.tags, type_encoders=config.type_encoders, type_decoders=config.type_decoders, include_in_schema=config.include_in_schema, websocket_class=self.websocket_class, ) self.asgi_router = ASGIRouter(app=self) for route_handler in config.route_handlers: self.register(route_handler) if self.logging_config: self.get_logger = self.logging_config.configure() self.logger = self.get_logger("litestar") for static_config in self._static_files_config: self.register(static_config.to_static_files_app()) self.asgi_handler = self._create_asgi_handler() @staticmethod def _patch_opentelemetry_middleware(config: AppConfig) -> AppConfig: # workaround to support otel middleware priority. Should be replaced by regular # middleware priorities once available try: from litestar.contrib.opentelemetry import OpenTelemetryPlugin if not any(isinstance(p, OpenTelemetryPlugin) for p in config.plugins): config.middleware, otel_middleware = OpenTelemetryPlugin._pop_otel_middleware(config.middleware) if otel_middleware: otel_plugin = OpenTelemetryPlugin() otel_plugin._middleware = otel_middleware config.plugins = [*config.plugins, otel_plugin] except ImportError: pass return config @property @deprecated(version="2.6.0", kind="property", info="Use create_static_files router instead") def static_files_config(self) -> list[StaticFilesConfig]: return self._static_files_config @property @deprecated(version="2.0", alternative="Litestar.plugins.cli", kind="property") def cli_plugins(self) -> list[CLIPluginProtocol]: return list(self.plugins.cli) @property @deprecated(version="2.0", alternative="Litestar.plugins.openapi", kind="property") def openapi_schema_plugins(self) -> list[OpenAPISchemaPluginProtocol]: return list(self.plugins.openapi) @property @deprecated(version="2.0", alternative="Litestar.plugins.serialization", kind="property") def serialization_plugins(self) -> list[SerializationPluginProtocol]: return list(self.plugins.serialization) @staticmethod def _get_default_plugins(plugins: list[PluginProtocol]) -> list[PluginProtocol]: from litestar.plugins.core import MsgspecDIPlugin plugins.append(MsgspecDIPlugin()) with suppress(MissingDependencyException): from litestar.plugins.pydantic import ( PydanticDIPlugin, PydanticInitPlugin, PydanticPlugin, PydanticSchemaPlugin, ) pydantic_plugin_found = any(isinstance(plugin, PydanticPlugin) for plugin in plugins) pydantic_init_plugin_found = any(isinstance(plugin, PydanticInitPlugin) for plugin in plugins) pydantic_schema_plugin_found = any(isinstance(plugin, PydanticSchemaPlugin) for plugin in plugins) pydantic_serialization_plugin_found = any(isinstance(plugin, PydanticDIPlugin) for plugin in plugins) if not pydantic_plugin_found and not pydantic_init_plugin_found and not pydantic_schema_plugin_found: plugins.append(PydanticPlugin()) elif not pydantic_plugin_found and pydantic_init_plugin_found and not pydantic_schema_plugin_found: plugins.append(PydanticSchemaPlugin()) elif not pydantic_plugin_found and not pydantic_init_plugin_found: plugins.append(PydanticInitPlugin()) if not pydantic_plugin_found and not pydantic_serialization_plugin_found: plugins.append(PydanticDIPlugin()) with suppress(MissingDependencyException): from litestar.plugins.attrs import AttrsSchemaPlugin pre_configured = any(isinstance(plugin, AttrsSchemaPlugin) for plugin in plugins) if not pre_configured: plugins.append(AttrsSchemaPlugin()) return plugins @property def debug(self) -> bool: return self._debug @debug.setter def debug(self, value: bool) -> None: """Sets the debug logging level for the application. When possible, it calls the `self.logging_config.set_level` method. This allows for implementation specific code and APIs to be called. """ if self.logger and self.logging_config: self.logging_config.set_level(self.logger, logging.DEBUG if value else logging.INFO) elif self.logger and hasattr(self.logger, "setLevel"): # pragma: no cover self.logger.setLevel(logging.DEBUG if value else logging.INFO) # pragma: no cover if isinstance(self.logging_config, LoggingConfig): self.logging_config.loggers["litestar"]["level"] = "DEBUG" if value else "INFO" self._debug = value async def __call__( self, scope: Scope | LifeSpanScope, receive: Receive | LifeSpanReceive, send: Send | LifeSpanSend, ) -> None: """Application entry point. Lifespan events (startup / shutdown) are sent to the lifespan handler, otherwise the ASGI handler is used Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ if scope["type"] == "lifespan": await self.asgi_router.lifespan(receive=receive, send=send) # type: ignore[arg-type] return scope["app"] = scope["litestar_app"] = self scope.setdefault("state", {}) await self.asgi_handler(scope, receive, self._wrap_send(send=send, scope=scope)) # type: ignore[arg-type] @classmethod def from_scope(cls, scope: Scope) -> Litestar: """Retrieve the Litestar application from the current ASGI scope""" return scope["litestar_app"] async def _call_lifespan_hook(self, hook: LifespanHook) -> None: ret = hook(self) if inspect.signature(hook).parameters else hook() # type: ignore[call-arg] if is_async_callable(hook): # pyright: ignore[reportGeneralTypeIssues] await ret @asynccontextmanager async def lifespan(self) -> AsyncGenerator[None, None]: """Context manager handling the ASGI lifespan. It will be entered when the ``lifespan`` message has been received from the server, and exit after the ``asgi.shutdown`` message. During this period, it is responsible for calling the ``on_startup``, ``on_shutdown`` hooks, as well as custom lifespan managers. """ async with AsyncExitStack() as exit_stack: for hook in self.on_shutdown[::-1]: exit_stack.push_async_callback(partial(self._call_lifespan_hook, hook)) await exit_stack.enter_async_context(self.event_emitter) for manager in self._lifespan_managers: if not isinstance(manager, AbstractAsyncContextManager): manager = manager(self) await exit_stack.enter_async_context(manager) for hook in self.on_startup: await self._call_lifespan_hook(hook) yield @property def openapi_schema(self) -> OpenAPI: """Access the OpenAPI schema of the application. Returns: The :class:`OpenAPI` instance of the application. Raises: ImproperlyConfiguredException: If the application ``openapi_config`` attribute is ``None``. """ return self.plugins.get(OpenAPIPlugin).provide_openapi() @classmethod def from_config(cls, config: AppConfig) -> Self: """Initialize a ``Litestar`` application from a configuration instance. Args: config: An instance of :class:`AppConfig` <.config.AppConfig> Returns: An instance of ``Litestar`` application. """ return cls(**dict(extract_dataclass_items(config))) def register(self, value: ControllerRouterHandler) -> None: # type: ignore[override] """Register a route handler on the app. This method can be used to dynamically add endpoints to an application. Args: value: An instance of :class:`Router <.router.Router>`, a subclass of :class:`Controller <.controller.Controller>` or any function decorated by the route handler decorators. Returns: None """ routes = super().register(value=value) for route in routes: route_handlers = get_route_handlers(route) for route_handler in route_handlers: route_handler.on_registration(self) if isinstance(route, HTTPRoute): route.create_handler_map() elif isinstance(route, WebSocketRoute): handler = route.route_handler route.handler_parameter_model = handler.create_kwargs_model(path_parameters=route.path_parameters) for plugin in self.plugins.receive_route: plugin.receive_route(route) self.asgi_router.construct_routing_trie() def get_handler_index_by_name(self, name: str) -> HandlerIndex | None: """Receives a route handler name and returns an optional dictionary containing the route handler instance and list of paths sorted lexically. Examples: .. code-block:: python from litestar import Litestar, get @get("/", name="my-handler") def handler() -> None: pass app = Litestar(route_handlers=[handler]) handler_index = app.get_handler_index_by_name("my-handler") # { "paths": ["/"], "handler" ... } Args: name: A route handler unique name. Returns: A :class:`HandlerIndex <.app.HandlerIndex>` instance or ``None``. """ handler = self.asgi_router.route_handler_index.get(name) if not handler: return None identifier = handler.name or str(handler) routes = self.asgi_router.route_mapping[identifier] paths = sorted(unique([route.path for route in routes])) return HandlerIndex(handler=handler, paths=paths, identifier=identifier) def route_reverse(self, name: str, **path_parameters: Any) -> str: """Receives a route handler name, path parameter values and returns url path to the handler with filled path parameters. Examples: .. code-block:: python from litestar import Litestar, get @get("/group/{group_id:int}/user/{user_id:int}", name="get_membership_details") def get_membership_details(group_id: int, user_id: int) -> None: pass app = Litestar(route_handlers=[get_membership_details]) path = app.route_reverse("get_membership_details", user_id=100, group_id=10) # /group/10/user/100 Args: name: A route handler unique name. **path_parameters: Actual values for path parameters in the route. Parameters of type `datetime`, `date`, `time`, `timedelta`, `float`, `Path`, `UUID` may be passed in their string representations. Raises: NoRouteMatchFoundException: If route with 'name' does not exist, path parameters are missing in ``**path_parameters or have wrong type``. Returns: A fully formatted url path. """ handler_index = self.get_handler_index_by_name(name) if handler_index is None: raise NoRouteMatchFoundException(f"Route {name} can not be found") allow_str_instead = {datetime, date, time, timedelta, float, Path, UUID} routes = sorted( self.asgi_router.route_mapping[handler_index["identifier"]], key=lambda r: len(r.path_parameters), reverse=True, ) passed_parameters = set(path_parameters.keys()) selected_route = next( (route for route in routes if passed_parameters.issuperset(route.path_parameters)), routes[-1], ) output: list[str] = [] for component in selected_route.path_components: if isinstance(component, PathParameterDefinition): val = path_parameters.get(component.name) if not isinstance(val, component.type) and ( component.type not in allow_str_instead or not isinstance(val, str) ): raise NoRouteMatchFoundException( f"Received type for path parameter {component.name} doesn't match declared type {component.type}" ) output.append(str(val)) else: output.append(component) return join_paths(output) @deprecated( "2.6.0", info="Use create_static_files router instead of StaticFilesConfig, which works with route_reverse" ) def url_for_static_asset(self, name: str, file_path: str) -> str: """Receives a static files handler name, an asset file path and returns resolved url path to the asset. Examples: .. code-block:: python from litestar import Litestar from litestar.static_files.config import StaticFilesConfig app = Litestar( static_files_config=[ StaticFilesConfig(directories=["css"], path="/static/css", name="css") ] ) path = app.url_for_static_asset("css", "main.css") # /static/css/main.css Args: name: A static handler unique name. file_path: a string containing path to an asset. Raises: NoRouteMatchFoundException: If static files handler with ``name`` does not exist. Returns: A url path to the asset. """ handler_index = self.get_handler_index_by_name(name) if handler_index is None: raise NoRouteMatchFoundException(f"Static handler {name} can not be found") handler_fn = cast("AnyCallable", handler_index["handler"].fn) if not isinstance(handler_fn, StaticFiles): raise NoRouteMatchFoundException(f"Handler with name {name} is not a static files handler") return join_paths([handler_index["paths"][0], file_path]) @property def route_handler_method_view(self) -> dict[str, list[str]]: """Map route handlers to paths. Returns: A dictionary of router handlers and lists of paths as strings """ route_map: dict[str, list[str]] = { handler: [route.path for route in routes] for handler, routes in self.asgi_router.route_mapping.items() } return route_map def _create_asgi_handler(self) -> ASGIApp: """Create an ASGIApp that wraps the ASGI router inside an exception handler. If CORS or TrustedHost configs are provided to the constructor, they will wrap the router as well. """ asgi_handler = wrap_in_exception_handler(app=self.asgi_router) if self.cors_config: asgi_handler = CORSMiddleware(app=asgi_handler, config=self.cors_config) try: otel_plugin: OpenTelemetryPlugin = self.plugins.get("OpenTelemetryPlugin") asgi_handler = otel_plugin.middleware(app=asgi_handler) except KeyError: pass return asgi_handler def _wrap_send(self, send: Send, scope: Scope) -> Send: """Wrap the ASGI send and handles any 'before send' hooks. Args: send: The ASGI send function. scope: The ASGI scope. Returns: An ASGI send function. """ if self.before_send: async def wrapped_send(message: Message) -> None: for hook in self.before_send: await hook(message, scope) await send(message) return wrapped_send return send def update_openapi_schema(self) -> None: """Update the OpenAPI schema to reflect the route handlers registered on the app. Returns: None """ self.plugins.get(OpenAPIPlugin)._build_openapi() def emit(self, event_id: str, *args: Any, **kwargs: Any) -> None: """Emit an event to all attached listeners. Args: event_id: The ID of the event to emit, e.g ``my_event``. args: args to pass to the listener(s). kwargs: kwargs to pass to the listener(s) Returns: None """ self.event_emitter.emit(event_id, *args, **kwargs) litestar-2.16.0/litestar/background_tasks.py000066400000000000000000000042501500564371300211610ustar00rootroot00000000000000from typing import Any, Callable, Iterable from anyio import create_task_group from typing_extensions import ParamSpec from litestar.utils.sync import ensure_async_callable __all__ = ("BackgroundTask", "BackgroundTasks") P = ParamSpec("P") class BackgroundTask: """A container for a 'background' task function. Background tasks are called once a Response finishes. """ __slots__ = ("args", "fn", "kwargs") def __init__(self, fn: Callable[P, Any], *args: P.args, **kwargs: P.kwargs) -> None: """Initialize ``BackgroundTask``. Args: fn: A sync or async function to call as the background task. *args: Args to pass to the func. **kwargs: Kwargs to pass to the func """ self.fn = ensure_async_callable(fn) self.args = args self.kwargs = kwargs async def __call__(self) -> None: """Call the wrapped function with the passed in arguments. Returns: None """ await self.fn(*self.args, **self.kwargs) class BackgroundTasks: """A container for multiple 'background' task functions. Background tasks are called once a Response finishes. """ __slots__ = ("run_in_task_group", "tasks") def __init__(self, tasks: Iterable[BackgroundTask], run_in_task_group: bool = False) -> None: """Initialize ``BackgroundTasks``. Args: tasks: An iterable of :class:`BackgroundTask <.background_tasks.BackgroundTask>` instances. run_in_task_group: If you set this value to ``True`` than the tasks will run concurrently, using a :class:`TaskGroup `. Note: This will not preserve execution order. """ self.tasks = tasks self.run_in_task_group = run_in_task_group async def __call__(self) -> None: """Call the wrapped background tasks. Returns: None """ if self.run_in_task_group: async with create_task_group() as task_group: for task in self.tasks: task_group.start_soon(task) else: for task in self.tasks: await task() litestar-2.16.0/litestar/channels/000077500000000000000000000000001500564371300170555ustar00rootroot00000000000000litestar-2.16.0/litestar/channels/__init__.py000066400000000000000000000002601500564371300211640ustar00rootroot00000000000000from .backends.base import ChannelsBackend from .plugin import ChannelsPlugin from .subscriber import Subscriber __all__ = ("ChannelsBackend", "ChannelsPlugin", "Subscriber") litestar-2.16.0/litestar/channels/backends/000077500000000000000000000000001500564371300206275ustar00rootroot00000000000000litestar-2.16.0/litestar/channels/backends/__init__.py000066400000000000000000000000001500564371300227260ustar00rootroot00000000000000litestar-2.16.0/litestar/channels/backends/_redis_flushall_streams.lua000066400000000000000000000005321500564371300262270ustar00rootroot00000000000000local key_pattern = ARGV[1] local cursor = 0 local deleted_streams = 0 repeat local result = redis.call('SCAN', cursor, 'MATCH', key_pattern) for _,key in ipairs(result[2]) do redis.call('DEL', key) deleted_streams = deleted_streams + 1 end cursor = tonumber(result[1]) until cursor == 0 return deleted_streams litestar-2.16.0/litestar/channels/backends/_redis_pubsub_publish.lua000066400000000000000000000001451500564371300257050ustar00rootroot00000000000000local data = ARGV[1] for _, channel in ipairs(KEYS) do redis.call("PUBLISH", channel, data) end litestar-2.16.0/litestar/channels/backends/_redis_xadd_expire.lua000066400000000000000000000006211500564371300251520ustar00rootroot00000000000000local data = ARGV[1] local limit = ARGV[2] local exp = ARGV[3] local maxlen_approx = ARGV[4] for i, key in ipairs(KEYS) do if maxlen_approx == 1 then redis.call("XADD", key, "MAXLEN", "~", limit, "*", "data", data, "channel", ARGV[i + 4]) else redis.call("XADD", key, "MAXLEN", limit, "*", "data", data, "channel", ARGV[i + 4]) end redis.call("PEXPIRE", key, exp) end litestar-2.16.0/litestar/channels/backends/asyncpg.py000066400000000000000000000065531500564371300226560ustar00rootroot00000000000000from __future__ import annotations import asyncio from contextlib import AsyncExitStack from functools import partial from typing import AsyncGenerator, Awaitable, Callable, Iterable, overload import asyncpg from litestar.channels import ChannelsBackend from litestar.exceptions import ImproperlyConfiguredException class AsyncPgChannelsBackend(ChannelsBackend): _listener_conn: asyncpg.Connection @overload def __init__(self, dsn: str) -> None: ... @overload def __init__( self, *, make_connection: Callable[[], Awaitable[asyncpg.Connection]], ) -> None: ... def __init__( self, dsn: str | None = None, *, make_connection: Callable[[], Awaitable[asyncpg.Connection]] | None = None, ) -> None: if not (dsn or make_connection): raise ImproperlyConfiguredException("Need to specify dsn or make_connection") self._subscribed_channels: set[str] = set() self._exit_stack = AsyncExitStack() self._connect = make_connection or partial(asyncpg.connect, dsn=dsn) self._queue: asyncio.Queue[tuple[str, bytes]] | None = None async def on_startup(self) -> None: self._queue = asyncio.Queue() self._listener_conn = await self._connect() async def on_shutdown(self) -> None: await self._listener_conn.close() self._queue = None async def publish(self, data: bytes, channels: Iterable[str]) -> None: if self._queue is None: raise RuntimeError("Backend not yet initialized. Did you forget to call on_startup?") dec_data = data.decode("utf-8") conn = await self._connect() try: for channel in channels: await conn.execute("SELECT pg_notify($1, $2);", channel, dec_data) finally: await conn.close() async def subscribe(self, channels: Iterable[str]) -> None: for channel in set(channels) - self._subscribed_channels: await self._listener_conn.add_listener(channel, self._listener) # type: ignore[arg-type] self._subscribed_channels.add(channel) async def unsubscribe(self, channels: Iterable[str]) -> None: for channel in channels: await self._listener_conn.remove_listener(channel, self._listener) # type: ignore[arg-type] self._subscribed_channels = self._subscribed_channels - set(channels) async def stream_events(self) -> AsyncGenerator[tuple[str, bytes], None]: if self._queue is None: raise RuntimeError("Backend not yet initialized. Did you forget to call on_startup?") while True: channel, message = await self._queue.get() self._queue.task_done() # an UNLISTEN may be in transit while we're getting here, so we double-check # that we are actually supposed to deliver this message if channel in self._subscribed_channels: yield channel, message async def get_history(self, channel: str, limit: int | None = None) -> list[bytes]: raise NotImplementedError() def _listener(self, /, connection: asyncpg.Connection, pid: int, channel: str, payload: object) -> None: if not isinstance(payload, str): raise RuntimeError("Invalid data received") self._queue.put_nowait((channel, payload.encode("utf-8"))) # type: ignore[union-attr] litestar-2.16.0/litestar/channels/backends/base.py000066400000000000000000000024241500564371300221150ustar00rootroot00000000000000from __future__ import annotations from abc import ABC, abstractmethod from typing import AsyncGenerator, Iterable class ChannelsBackend(ABC): @abstractmethod async def on_startup(self) -> None: """Called by the plugin on application startup""" ... @abstractmethod async def on_shutdown(self) -> None: """Called by the plugin on application shutdown""" ... @abstractmethod async def publish(self, data: bytes, channels: Iterable[str]) -> None: """Publish the message ``data`` to all ``channels``""" ... @abstractmethod async def subscribe(self, channels: Iterable[str]) -> None: """Start listening for events on ``channels``""" ... @abstractmethod async def unsubscribe(self, channels: Iterable[str]) -> None: """Stop listening for events on ``channels``""" ... @abstractmethod def stream_events(self) -> AsyncGenerator[tuple[str, bytes], None]: """Return a generator, iterating over events of subscribed channels as they become available""" ... @abstractmethod async def get_history(self, channel: str, limit: int | None = None) -> list[bytes]: """Return the event history of ``channel``, at most ``limit`` entries""" ... litestar-2.16.0/litestar/channels/backends/memory.py000066400000000000000000000060401500564371300225110ustar00rootroot00000000000000from __future__ import annotations from asyncio import Queue from collections import defaultdict, deque from typing import Any, AsyncGenerator, Iterable from litestar.channels.backends.base import ChannelsBackend class MemoryChannelsBackend(ChannelsBackend): """An in-memory channels backend""" def __init__(self, history: int = 0) -> None: self._max_history_length = history self._channels: set[str] = set() self._queue: Queue[tuple[str, bytes]] | None = None self._history: defaultdict[str, deque[bytes]] = defaultdict(lambda: deque(maxlen=self._max_history_length)) async def on_startup(self) -> None: self._queue = Queue() async def on_shutdown(self) -> None: self._queue = None async def publish(self, data: bytes, channels: Iterable[str]) -> None: """Publish ``data`` to ``channels``. If a channel has not yet been subscribed to, this will be a no-op. Args: data: Data to publish channels: Channels to publish to Returns: None Raises: RuntimeError: If ``on_startup`` has not been called yet """ if not self._queue: raise RuntimeError("Backend not yet initialized. Did you forget to call on_startup?") for channel in channels: if channel not in self._channels: continue self._queue.put_nowait((channel, data)) if self._max_history_length: for channel in channels: self._history[channel].append(data) async def subscribe(self, channels: Iterable[str]) -> None: """Subscribe to ``channels``, and enable publishing to them""" self._channels.update(channels) async def unsubscribe(self, channels: Iterable[str]) -> None: """Unsubscribe from ``channels``""" self._channels -= set(channels) try: for channel in channels: del self._history[channel] except KeyError: pass async def stream_events(self) -> AsyncGenerator[tuple[str, Any], None]: """Return a generator, iterating over events of subscribed channels as they become available""" if self._queue is None: raise RuntimeError("Backend not yet initialized. Did you forget to call on_startup?") while True: channel, message = await self._queue.get() self._queue.task_done() # if a message is published to a channel and the channel is then # unsubscribed before retrieving that message from the stream, it can still # end up here, so we double-check if we still are interested in this message if channel in self._channels: yield channel, message async def get_history(self, channel: str, limit: int | None = None) -> list[bytes]: """Return the event history of ``channel``, at most ``limit`` entries""" history = list(self._history[channel]) if limit: history = history[-limit:] return history litestar-2.16.0/litestar/channels/backends/psycopg.py000066400000000000000000000042001500564371300226610ustar00rootroot00000000000000from __future__ import annotations from contextlib import AsyncExitStack from typing import Any, AsyncGenerator, Iterable from psycopg import AsyncConnection from psycopg.sql import SQL, Identifier from litestar.channels.backends.base import ChannelsBackend class PsycoPgChannelsBackend(ChannelsBackend): _listener_conn: AsyncConnection[Any] def __init__(self, pg_dsn: str) -> None: self._pg_dsn = pg_dsn self._subscribed_channels: set[str] = set() self._exit_stack = AsyncExitStack() async def on_startup(self) -> None: self._listener_conn = await AsyncConnection[Any].connect(self._pg_dsn, autocommit=True) await self._exit_stack.enter_async_context(self._listener_conn) async def on_shutdown(self) -> None: await self._exit_stack.aclose() async def publish(self, data: bytes, channels: Iterable[str]) -> None: dec_data = data.decode("utf-8") async with await AsyncConnection[Any].connect(self._pg_dsn, autocommit=True) as conn: for channel in channels: await conn.execute(SQL("NOTIFY {channel}, {data}").format(channel=Identifier(channel), data=dec_data)) async def subscribe(self, channels: Iterable[str]) -> None: for channel in set(channels) - self._subscribed_channels: await self._listener_conn.execute(SQL("LISTEN {channel}").format(channel=Identifier(channel))) self._subscribed_channels.add(channel) await self._listener_conn.commit() async def unsubscribe(self, channels: Iterable[str]) -> None: for channel in channels: await self._listener_conn.execute(SQL("UNLISTEN {channel}").format(channel=Identifier(channel))) await self._listener_conn.commit() self._subscribed_channels = self._subscribed_channels - set(channels) async def stream_events(self) -> AsyncGenerator[tuple[str, bytes], None]: async for notify in self._listener_conn.notifies(): yield notify.channel, notify.payload.encode("utf-8") async def get_history(self, channel: str, limit: int | None = None) -> list[bytes]: raise NotImplementedError() litestar-2.16.0/litestar/channels/backends/redis.py000066400000000000000000000262401500564371300223130ustar00rootroot00000000000000from __future__ import annotations import asyncio import sys if sys.version_info < (3, 9): import importlib_resources # pyright: ignore else: import importlib.resources as importlib_resources from abc import ABC from datetime import timedelta from typing import TYPE_CHECKING, Any, AsyncGenerator, Iterable, cast from litestar.channels.backends.base import ChannelsBackend if TYPE_CHECKING: from redis.asyncio import Redis from redis.asyncio.client import PubSub _resource_path = importlib_resources.files("litestar.channels.backends") _PUBSUB_PUBLISH_SCRIPT = (_resource_path / "_redis_pubsub_publish.lua").read_text() _FLUSHALL_STREAMS_SCRIPT = (_resource_path / "_redis_flushall_streams.lua").read_text() _XADD_EXPIRE_SCRIPT = (_resource_path / "_redis_xadd_expire.lua").read_text() class _LazyEvent: """A lazy proxy to asyncio.Event that only creates the event once it's accessed. It ensures that the Event is created within a running event loop. If it's not, there can be an issue where a future within the event itself is attached to a different loop. This happens in our tests and could also happen when a user creates an instance of the backend outside an event loop in their application. """ def __init__(self) -> None: self.__event: asyncio.Event | None = None @property def _event(self) -> asyncio.Event: if self.__event is None: self.__event = asyncio.Event() return self.__event def set(self) -> None: self._event.set() def clear(self) -> None: self._event.clear() async def wait(self) -> None: await self._event.wait() class RedisChannelsBackend(ChannelsBackend, ABC): def __init__(self, *, redis: Redis, key_prefix: str, stream_sleep_no_subscriptions: int) -> None: """Base redis channels backend. Args: redis: A :class:`redis.asyncio.Redis` instance key_prefix: Key prefix to use for storing data in redis stream_sleep_no_subscriptions: Amount of time in milliseconds to pause the :meth:`stream_events` generator, should no subscribers exist """ self._redis = redis self._key_prefix = key_prefix self._stream_sleep_no_subscriptions = stream_sleep_no_subscriptions def _make_key(self, channel: str) -> str: return f"{self._key_prefix}_{channel.upper()}" class RedisChannelsPubSubBackend(RedisChannelsBackend): def __init__( self, *, redis: Redis, stream_sleep_no_subscriptions: int = 1, key_prefix: str = "LITESTAR_CHANNELS" ) -> None: """Redis channels backend, `Pub/Sub `_. This backend provides low overhead and resource usage but no support for history. Args: redis: A :class:`redis.asyncio.Redis` instance key_prefix: Key prefix to use for storing data in redis stream_sleep_no_subscriptions: Amount of time in milliseconds to pause the :meth:`stream_events` generator, should no subscribers exist """ super().__init__( redis=redis, stream_sleep_no_subscriptions=stream_sleep_no_subscriptions, key_prefix=key_prefix ) self.__pub_sub: PubSub | None = None self._publish_script = self._redis.register_script(_PUBSUB_PUBLISH_SCRIPT) self._has_subscribed = _LazyEvent() @property def _pub_sub(self) -> PubSub: if self.__pub_sub is None: self.__pub_sub = self._redis.pubsub() return self.__pub_sub async def on_startup(self) -> None: # this method should not do anything in this case pass async def on_shutdown(self) -> None: await self._pub_sub.reset() async def subscribe(self, channels: Iterable[str]) -> None: """Subscribe to ``channels``, and enable publishing to them""" await self._pub_sub.subscribe(*channels) self._has_subscribed.set() async def unsubscribe(self, channels: Iterable[str]) -> None: """Stop listening for events on ``channels``""" await self._pub_sub.unsubscribe(*channels) # if we have no active subscriptions, or only subscriptions which are pending # to be unsubscribed we consider the backend to be unsubscribed from all # channels, so we reset the event if not self._pub_sub.channels.keys() - self._pub_sub.pending_unsubscribe_channels: self._has_subscribed.clear() async def publish(self, data: bytes, channels: Iterable[str]) -> None: """Publish ``data`` to ``channels`` .. note:: This operation is performed atomically, using a lua script """ await self._publish_script(keys=list(set(channels)), args=[data]) async def stream_events(self) -> AsyncGenerator[tuple[str, Any], None]: """Return a generator, iterating over events of subscribed channels as they become available. If no channels have been subscribed to yet via :meth:`subscribe`, sleep for ``stream_sleep_no_subscriptions`` milliseconds. """ while True: await self._has_subscribed.wait() message = await self._pub_sub.get_message( ignore_subscribe_messages=True, timeout=self._stream_sleep_no_subscriptions ) if message is None: continue channel: str = message["channel"].decode() data: bytes = message["data"] # redis handles the unsubscribing with a queue; Unsubscribing doesn't mean # the unsubscribe will happen immediately after requesting it, so we could # receive a message on a channel that, from a client's perspective, it's not # subscribed to anymore if channel.encode() in self._pub_sub.channels.keys() - self._pub_sub.pending_unsubscribe_channels: yield channel, data async def get_history(self, channel: str, limit: int | None = None) -> list[bytes]: """Not implemented""" raise NotImplementedError() class RedisChannelsStreamBackend(RedisChannelsBackend): def __init__( self, history: int, *, redis: Redis, stream_sleep_no_subscriptions: int = 1, cap_streams_approximate: bool = True, stream_ttl: int | timedelta = timedelta(seconds=60), key_prefix: str = "LITESTAR_CHANNELS", ) -> None: """Redis channels backend, `streams `_. Args: history: Amount of messages to keep. This will set a ``MAXLEN`` to the streams redis: A :class:`redis.asyncio.Redis` instance key_prefix: Key prefix to use for streams stream_sleep_no_subscriptions: Amount of time in milliseconds to pause the :meth:`stream_events` generator, should no subscribers exist cap_streams_approximate: Set the streams ``MAXLEN`` using the ``~`` approximation operator for improved performance stream_ttl: TTL of a stream in milliseconds or as a timedelta. A streams TTL will be set on each publishing operation using ``PEXPIRE`` """ super().__init__( redis=redis, stream_sleep_no_subscriptions=stream_sleep_no_subscriptions, key_prefix=key_prefix ) self._history_limit = history self._subscribed_channels: set[str] = set() self._cap_streams_approximate = cap_streams_approximate self._stream_ttl = stream_ttl if isinstance(stream_ttl, int) else int(stream_ttl.total_seconds() * 1000) self._flush_all_streams_script = self._redis.register_script(_FLUSHALL_STREAMS_SCRIPT) self._publish_script = self._redis.register_script(_XADD_EXPIRE_SCRIPT) self._has_subscribed_channels = _LazyEvent() async def on_startup(self) -> None: """Called on application startup""" async def on_shutdown(self) -> None: """Called on application shutdown""" async def subscribe(self, channels: Iterable[str]) -> None: """Subscribe to ``channels``""" self._subscribed_channels.update(channels) self._has_subscribed_channels.set() async def unsubscribe(self, channels: Iterable[str]) -> None: """Unsubscribe from ``channels``""" self._subscribed_channels -= set(channels) if not len(self._subscribed_channels): self._has_subscribed_channels.clear() async def publish(self, data: bytes, channels: Iterable[str]) -> None: """Publish ``data`` to ``channels``. .. note:: This operation is performed atomically, using a Lua script """ channels = set(channels) await self._publish_script( keys=[self._make_key(key) for key in channels], args=[ data, self._history_limit, self._stream_ttl, int(self._cap_streams_approximate), *channels, ], ) async def _get_subscribed_channels(self) -> set[str]: """Get subscribed channels. If no channels are currently subscribed, wait""" await self._has_subscribed_channels.wait() return self._subscribed_channels async def stream_events(self) -> AsyncGenerator[tuple[str, Any], None]: """Return a generator, iterating over events of subscribed channels as they become available. If no channels have been subscribed to yet via :meth:`subscribe`, sleep for ``stream_sleep_no_subscriptions`` milliseconds. """ stream_ids: dict[str, bytes] = {} while True: # We wait for subscribed channels, because we can't pass an empty dict to # xread and block for subscribers stream_keys = [self._make_key(c) for c in await self._get_subscribed_channels()] data: list[tuple[bytes, list[tuple[bytes, dict[bytes, bytes]]]]] = await self._redis.xread( {key: stream_ids.get(key, 0) for key in stream_keys}, block=self._stream_sleep_no_subscriptions ) if not data: continue for stream_key, channel_events in data: for event in channel_events: event_data = event[1][b"data"] channel_name = event[1][b"channel"].decode() stream_ids[stream_key.decode()] = event[0] yield channel_name, event_data async def get_history(self, channel: str, limit: int | None = None) -> list[bytes]: """Return the history of ``channels``, returning at most ``limit`` messages""" data: Iterable[tuple[bytes, dict[bytes, bytes]]] if limit: data = reversed(await self._redis.xrevrange(self._make_key(channel), count=limit)) else: data = await self._redis.xrange(self._make_key(channel)) return [event[b"data"] for _, event in data] async def flush_all(self) -> int: """Delete all stream keys with the ``key_prefix``. .. important:: This method is incompatible with redis clusters """ deleted_streams = await self._flush_all_streams_script(keys=[], args=[f"{self._key_prefix}*"]) return cast("int", deleted_streams) litestar-2.16.0/litestar/channels/plugin.py000066400000000000000000000367301500564371300207360ustar00rootroot00000000000000from __future__ import annotations import asyncio from asyncio import CancelledError, Queue, Task, create_task from contextlib import AbstractAsyncContextManager, asynccontextmanager, suppress from functools import partial from typing import TYPE_CHECKING, AsyncGenerator, Awaitable, Callable, Iterable import msgspec.json from litestar.di import Provide from litestar.exceptions import ImproperlyConfiguredException, LitestarException from litestar.handlers import WebsocketRouteHandler from litestar.plugins import InitPlugin from litestar.serialization import default_serializer from .subscriber import BacklogStrategy, EventCallback, Subscriber if TYPE_CHECKING: from types import TracebackType from litestar.channels.backends.base import ChannelsBackend from litestar.config.app import AppConfig from litestar.connection import WebSocket from litestar.types import LitestarEncodableType, TypeEncodersMap from litestar.types.asgi_types import WebSocketMode class ChannelsException(LitestarException): pass class ChannelsPlugin(InitPlugin, AbstractAsyncContextManager): def __init__( self, backend: ChannelsBackend, *, channels: Iterable[str] | None = None, arbitrary_channels_allowed: bool = False, create_ws_route_handlers: bool = False, ws_handler_send_history: int = 0, ws_handler_base_path: str = "/", ws_send_mode: WebSocketMode = "text", subscriber_max_backlog: int | None = None, subscriber_backlog_strategy: BacklogStrategy = "backoff", subscriber_class: type[Subscriber] = Subscriber, type_encoders: TypeEncodersMap | None = None, ) -> None: """Plugin to handle broadcasting to WebSockets with support for channels. This plugin is available as an injected dependency using the ``channels`` key. Args: backend: Backend to store data in channels: Channels to serve. If ``arbitrary_channels_allowed`` is ``False`` (the default), trying to subscribe to a channel not in ``channels`` will raise an exception arbitrary_channels_allowed: Allow the creation of channels on the fly create_ws_route_handlers: If ``True``, websocket route handlers will be created for all channels defined in ``channels``. If ``arbitrary_channels_allowed`` is ``True``, a single handler will be created instead, handling all channels. The handlers created will accept WebSocket connections and start sending received events for their respective channels. ws_handler_send_history: Amount of history entries to send from the generated websocket route handlers after a client has connected. A value of ``0`` indicates to not send a history ws_handler_base_path: Path prefix used for the generated route handlers ws_send_mode: Send mode to use for sending data through a :class:`WebSocket <.connection.WebSocket>`. This will be used when sending within generated route handlers or :meth:`Subscriber.run_in_background` subscriber_max_backlog: Maximum amount of unsent messages to be held in memory for a given subscriber. If that limit is reached, new messages will be treated accordingly to ``backlog_strategy`` subscriber_backlog_strategy: Define the behaviour if ``max_backlog`` is reached for a subscriber. ` `backoff`` will result in new messages being dropped until older ones have been processed. ``dropleft`` will drop older messages in favour of new ones. subscriber_class: A :class:`Subscriber` subclass to return from :meth:`subscribe` type_encoders: An additional mapping of type encoders used to encode data before sending """ self._backend = backend self._pub_queue: Queue[tuple[bytes, list[str]]] | None = None self._pub_task: Task | None = None self._sub_task: Task | None = None if not (channels or arbitrary_channels_allowed): raise ImproperlyConfiguredException("Must define either channels or set arbitrary_channels_allowed=True") # make the path absolute, so we can simply concatenate it later if not ws_handler_base_path.endswith("/"): ws_handler_base_path += "/" self._arbitrary_channels_allowed = arbitrary_channels_allowed self._create_route_handlers = create_ws_route_handlers self._handler_root_path = ws_handler_base_path self._socket_send_mode: WebSocketMode = ws_send_mode self._encode_json = msgspec.json.Encoder( enc_hook=partial(default_serializer, type_encoders=type_encoders) ).encode self._handler_should_send_history = bool(ws_handler_send_history) self._history_limit = None if ws_handler_send_history < 0 else ws_handler_send_history self._max_backlog = subscriber_max_backlog self._backlog_strategy: BacklogStrategy = subscriber_backlog_strategy self._subscriber_class = subscriber_class self._channels: dict[str, set[Subscriber]] = {channel: set() for channel in channels or []} def encode_data(self, data: LitestarEncodableType) -> bytes: """Encode data before storing it in the backend""" if isinstance(data, bytes): return data return data.encode() if isinstance(data, str) else self._encode_json(data) def on_app_init(self, app_config: AppConfig) -> AppConfig: """Plugin hook. Set up a ``channels`` dependency, add route handlers and register application hooks""" app_config.dependencies["channels"] = Provide(lambda: self, use_cache=True, sync_to_thread=False) app_config.lifespan.append(self) app_config.signature_namespace.update(ChannelsPlugin=ChannelsPlugin) if self._create_route_handlers: if self._arbitrary_channels_allowed: path = self._handler_root_path + "{channel_name:str}" route_handlers = [WebsocketRouteHandler(path)(self._ws_handler_func)] else: route_handlers = [ WebsocketRouteHandler(self._handler_root_path + channel_name)( self._create_ws_handler_func(channel_name) ) for channel_name in self._channels ] app_config.route_handlers.extend(route_handlers) return app_config def publish(self, data: LitestarEncodableType, channels: str | Iterable[str]) -> None: """Schedule ``data`` to be published to ``channels``. .. note:: This is a synchronous method that returns immediately. There are no guarantees that when this method returns the data will have been published to the backend. For that, use :meth:`wait_published` """ if isinstance(channels, str): channels = [channels] data = self.encode_data(data) try: self._pub_queue.put_nowait((data, list(channels))) # type: ignore[union-attr] except AttributeError as e: raise RuntimeError("Plugin not yet initialized. Did you forget to call on_startup?") from e async def wait_published(self, data: LitestarEncodableType, channels: str | Iterable[str]) -> None: """Publish ``data`` to ``channels``""" if isinstance(channels, str): channels = [channels] data = self.encode_data(data) await self._backend.publish(data, channels) async def subscribe(self, channels: str | Iterable[str], history: int | None = None) -> Subscriber: """Create a :class:`Subscriber`, providing a stream of all events in ``channels``. The created subscriber will be passive by default and has to be consumed manually, either by using :meth:`Subscriber.run_in_background` or iterating over events using :meth:`Subscriber.iter_events`. Args: channels: Channel(s) to subscribe to history: If a non-negative integer, add this amount of history entries from each channel to the subscriber's event stream. Note that this will wait until all history entries are fetched and pushed to the subscriber's stream. For more control use :meth:`put_subscriber_history`. Returns: A :class:`Subscriber` Raises: ChannelsException: If a channel in ``channels`` has not been declared on this backend and ``arbitrary_channels_allowed`` has not been set to ``True`` """ if isinstance(channels, str): channels = [channels] subscriber = self._subscriber_class( plugin=self, max_backlog=self._max_backlog, backlog_strategy=self._backlog_strategy, ) channels_to_subscribe = set() for channel in channels: if channel not in self._channels: if not self._arbitrary_channels_allowed: raise ChannelsException( f"Unknown channel: {channel!r}. Either explicitly defined the channel or set " "arbitrary_channels_allowed=True" ) self._channels[channel] = set() channel_subscribers = self._channels[channel] if not channel_subscribers: channels_to_subscribe.add(channel) channel_subscribers.add(subscriber) if channels_to_subscribe: await self._backend.subscribe(channels_to_subscribe) if history: await self.put_subscriber_history(subscriber=subscriber, limit=history, channels=channels) return subscriber async def unsubscribe(self, subscriber: Subscriber, channels: str | Iterable[str] | None = None) -> None: """Unsubscribe a :class:`Subscriber` from ``channels``. If the subscriber has a running sending task, it will be stopped. Args: channels: Channels to unsubscribe from. If ``None``, unsubscribe from all channels subscriber: :class:`Subscriber` to unsubscribe """ if channels is None: channels = list(self._channels.keys()) elif isinstance(channels, str): channels = [channels] channels_to_unsubscribe: set[str] = set() for channel in channels: channel_subscribers = self._channels[channel] try: channel_subscribers.remove(subscriber) except KeyError: # subscriber was not subscribed to this channel. This may happen if channels is None continue if not channel_subscribers: channels_to_unsubscribe.add(channel) if all(subscriber not in queues for queues in self._channels.values()): await subscriber.put(None) # this will stop any running task or generator by breaking the inner loop if subscriber.is_running: await subscriber.stop() if channels_to_unsubscribe: await self._backend.unsubscribe(channels_to_unsubscribe) @asynccontextmanager async def start_subscription( self, channels: str | Iterable[str], history: int | None = None ) -> AsyncGenerator[Subscriber, None]: """Create a :class:`Subscriber` and tie its subscriptions to a context manager; Upon exiting the context, :meth:`unsubscribe` will be called. Args: channels: Channel(s) to subscribe to history: If a non-negative integer, add this amount of history entries from each channel to the subscriber's event stream. Note that this will wait until all history entries are fetched and pushed to the subscriber's stream. For more control use :meth:`put_subscriber_history`. Returns: A :class:`Subscriber` """ subscriber = await self.subscribe(channels, history=history) try: yield subscriber finally: await self.unsubscribe(subscriber, channels) async def put_subscriber_history( self, subscriber: Subscriber, channels: str | Iterable[str], limit: int | None = None ) -> None: """Fetch the history of ``channels`` from the backend and put them in the subscriber's stream """ if isinstance(channels, str): channels = [channels] for channel in channels: history = await self._backend.get_history(channel, limit) for entry in history: await subscriber.put(entry) async def _ws_handler_func(self, channel_name: str, socket: WebSocket) -> None: await socket.accept() # the ternary operator triggers a mypy bug: https://github.com/python/mypy/issues/10740 on_event: EventCallback = socket.send_text if self._socket_send_mode == "text" else socket.send_bytes # type: ignore[assignment] async with self.start_subscription(channel_name) as subscriber: if self._handler_should_send_history: await self.put_subscriber_history(subscriber, channels=channel_name, limit=self._history_limit) # use the background task, so we can block on receive(), breaking the loop when a connection closes async with subscriber.run_in_background(on_event): while (await socket.receive())["type"] != "websocket.disconnect": continue def _create_ws_handler_func(self, channel_name: str) -> Callable[[WebSocket], Awaitable[None]]: async def ws_handler_func(socket: WebSocket) -> None: await self._ws_handler_func(channel_name=channel_name, socket=socket) return ws_handler_func async def _pub_worker(self) -> None: while self._pub_queue: data, channels = await self._pub_queue.get() await self._backend.publish(data, channels) self._pub_queue.task_done() async def _sub_worker(self) -> None: async for channel, payload in self._backend.stream_events(): for subscriber in self._channels.get(channel, []): subscriber.put_nowait(payload) async def _on_startup(self) -> None: await self._backend.on_startup() self._pub_queue = Queue() self._pub_task = create_task(self._pub_worker()) self._sub_task = create_task(self._sub_worker()) if self._channels: await self._backend.subscribe(list(self._channels)) async def _on_shutdown(self) -> None: if self._pub_queue: await self._pub_queue.join() self._pub_queue = None await asyncio.gather( *[ subscriber.stop(join=False) for subscribers in self._channels.values() for subscriber in subscribers if subscriber.is_running ] ) if self._sub_task: self._sub_task.cancel() with suppress(CancelledError): await self._sub_task self._sub_task = None if self._pub_task: self._pub_task.cancel() with suppress(CancelledError): await self._pub_task self._sub_task = None await self._backend.on_shutdown() async def __aenter__(self) -> ChannelsPlugin: await self._on_startup() return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: await self._on_shutdown() litestar-2.16.0/litestar/channels/subscriber.py000066400000000000000000000111471500564371300215760ustar00rootroot00000000000000from __future__ import annotations import asyncio from asyncio import CancelledError, Queue, QueueFull from collections import deque from contextlib import AsyncExitStack, asynccontextmanager, suppress from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Generic, Literal, TypeVar if TYPE_CHECKING: from litestar.channels import ChannelsPlugin T = TypeVar("T") BacklogStrategy = Literal["backoff", "dropleft"] EventCallback = Callable[[bytes], Awaitable[Any]] class AsyncDeque(Queue, Generic[T]): def __init__(self, maxsize: int | None) -> None: self._deque_maxlen = maxsize super().__init__() def _init(self, maxsize: int) -> None: self._queue: deque[T] = deque(maxlen=self._deque_maxlen) class Subscriber: """A wrapper around a stream of events published to subscribed channels""" def __init__( self, plugin: ChannelsPlugin, max_backlog: int | None = None, backlog_strategy: BacklogStrategy = "backoff", ) -> None: self._task: asyncio.Task | None = None self._plugin = plugin self._backend = plugin._backend self._queue: Queue[bytes | None] | AsyncDeque[bytes | None] if max_backlog and backlog_strategy == "dropleft": self._queue = AsyncDeque(maxsize=max_backlog or 0) else: self._queue = Queue(maxsize=max_backlog or 0) async def put(self, item: bytes | None) -> None: await self._queue.put(item) def put_nowait(self, item: bytes | None) -> bool: """Put an item in the subscriber's stream without waiting""" try: self._queue.put_nowait(item) return True except QueueFull: return False @property def qsize(self) -> int: return self._queue.qsize() async def iter_events(self) -> AsyncGenerator[bytes, None]: """Iterate over the stream of events. If no items are available, block until one becomes available """ while True: item = await self._queue.get() if item is None: self._queue.task_done() break yield item self._queue.task_done() @asynccontextmanager async def run_in_background(self, on_event: EventCallback, join: bool = True) -> AsyncGenerator[None, None]: """Start a task in the background that sends events from the subscriber's stream to ``socket`` as they become available. On exit, it will prevent the stream from accepting new events and wait until the currently enqueued ones are processed. Should the context be left with an exception, the task will be cancelled immediately. Args: on_event: Callback to invoke with the event data for every event join: If ``True``, wait for all items in the stream to be processed before stopping the worker. Note that an error occurring within the context will always lead to the immediate cancellation of the worker """ self._start_in_background(on_event=on_event) async with AsyncExitStack() as exit_stack: exit_stack.push_async_callback(self.stop, join=False) yield exit_stack.pop_all() await self.stop(join=join) async def _worker(self, on_event: EventCallback) -> None: async for event in self.iter_events(): await on_event(event) def _start_in_background(self, on_event: EventCallback) -> None: """Start a task in the background that sends events from the subscriber's stream to ``socket`` as they become available. Args: on_event: Callback to invoke with the event data for every event """ if self._task is not None: raise RuntimeError("Subscriber is already running") self._task = asyncio.create_task(self._worker(on_event)) @property def is_running(self) -> bool: """Return whether a sending task is currently running""" return self._task is not None async def stop(self, join: bool = False) -> None: """Stop a task was previously started with :meth:`run_in_background`. If the task is not yet done it will be cancelled and awaited Args: join: If ``True`` wait for all items to be processed before stopping the task """ if not self._task: return if join: await self._queue.join() if not self._task.done(): self._task.cancel() with suppress(CancelledError): await self._task self._task = None litestar-2.16.0/litestar/cli/000077500000000000000000000000001500564371300160315ustar00rootroot00000000000000litestar-2.16.0/litestar/cli/__init__.py000066400000000000000000000013671500564371300201510ustar00rootroot00000000000000"""Litestar CLI.""" from __future__ import annotations from importlib.util import find_spec if find_spec("rich_click") is not None: # pragma: no cover import rich_click rich_click.rich_click.USE_RICH_MARKUP = True rich_click.rich_click.USE_MARKDOWN = False rich_click.rich_click.SHOW_ARGUMENTS = True rich_click.rich_click.GROUP_ARGUMENTS_OPTIONS = True rich_click.rich_click.STYLE_ERRORS_SUGGESTION = "magenta italic" rich_click.rich_click.ERRORS_SUGGESTION = "" rich_click.rich_click.ERRORS_EPILOGUE = "" rich_click.rich_click.MAX_WIDTH = 120 rich_click.rich_click.SHOW_METAVARS_COLUMN = True rich_click.rich_click.APPEND_METAVARS_HELP = True from .main import litestar_group __all__ = ["litestar_group"] litestar-2.16.0/litestar/cli/_utils.py000066400000000000000000000530431500564371300177070ustar00rootroot00000000000000from __future__ import annotations import contextlib import importlib import inspect import os import re import sys from dataclasses import dataclass from datetime import datetime, timedelta, timezone from functools import wraps from importlib.util import find_spec from itertools import chain from os import getenv from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, Sequence, TypeVar, cast try: from rich_click import RichCommand as Command from rich_click import RichGroup as Group except ImportError: from click import Command, Group # type: ignore[assignment] from click import ClickException, Context, pass_context from rich import get_console from rich.table import Table from typing_extensions import ParamSpec, get_type_hints from litestar import Litestar, __version__ from litestar.middleware import DefineMiddleware from litestar.utils import get_name if sys.version_info >= (3, 10): from importlib.metadata import entry_points else: from importlib_metadata import entry_points if TYPE_CHECKING: from types import ModuleType from litestar.openapi import OpenAPIConfig from litestar.routes import ASGIRoute, HTTPRoute, WebSocketRoute from litestar.types import AnyCallable UVICORN_INSTALLED = find_spec("uvicorn") is not None JSBEAUTIFIER_INSTALLED = find_spec("jsbeautifier") is not None __all__ = ( "JSBEAUTIFIER_INSTALLED", "UVICORN_INSTALLED", "LitestarCLIException", "LitestarEnv", "LitestarExtensionGroup", "LitestarGroup", "LoadedApp", "show_app_info", ) P = ParamSpec("P") T = TypeVar("T") AUTODISCOVERY_FILE_NAMES = ["app", "application"] console = get_console() class LitestarCLIException(ClickException): """Base class for Litestar CLI exceptions.""" def __init__(self, message: str) -> None: """Initialize exception and style error message.""" super().__init__(message) @dataclass class LitestarEnv: """Information about the current Litestar environment variables.""" app_path: str app: Litestar cwd: Path host: str | None = None port: int | None = None is_app_factory: bool = False @classmethod def from_env(cls, app_path: str | None, app_dir: Path | None = None) -> LitestarEnv: """Load environment variables. If ``python-dotenv`` is installed, use it to populate environment first """ cwd = Path().cwd() if app_dir is None else app_dir cwd_str_path = str(cwd) if cwd_str_path not in sys.path: sys.path.append(cwd_str_path) with contextlib.suppress(ImportError): import dotenv dotenv.load_dotenv() app_path = app_path or getenv("LITESTAR_APP") app_name = getenv("LITESTAR_APP_NAME") or "Litestar" quiet_console = getenv("LITESTAR_QUIET_CONSOLE") or False if app_path and getenv("LITESTAR_APP") is None: os.environ["LITESTAR_APP"] = app_path if app_path: if not quiet_console and isatty(): console.print(f"Using {app_name} app from env: [bright_blue]{app_path!r}") loaded_app = _load_app_from_path(app_path) else: loaded_app = _autodiscover_app(cwd) port = getenv("LITESTAR_PORT") return cls( app_path=loaded_app.app_path, app=loaded_app.app, host=getenv("LITESTAR_HOST"), port=int(port) if port else None, is_app_factory=loaded_app.is_factory, cwd=cwd, ) @dataclass class LoadedApp: """Information about a loaded Litestar app.""" app: Litestar app_path: str is_factory: bool class LitestarGroup(Group): # pyright: ignore """:class:`click.Group` subclass that automatically injects ``app`` and ``env` kwargs into commands that request it. Use this as the ``cls`` for :class:`click.Group` if you're extending the internal CLI with a group. For ``command``s added directly to the root group this is not needed. """ def __init__( self, name: str | None = None, commands: dict[str, Command] | Sequence[Command] | None = None, **attrs: Any, ) -> None: """Init ``LitestarGroup``""" self.group_class = LitestarGroup super().__init__(name=name, commands=commands, **attrs) def add_command(self, cmd: Command, name: str | None = None) -> None: # type: ignore[override] """Add command. If necessary, inject ``app`` and ``env`` kwargs """ if cmd.callback: cmd.callback = _inject_args(cmd.callback) super().add_command(cmd) def command(self, *args: Any, **kwargs: Any) -> Callable[[AnyCallable], Command] | Command: # type: ignore[override] # For some reason, even when copying the overloads + signature from click 1:1, mypy goes haywire """Add a function as a command. If necessary, inject ``app`` and ``env`` kwargs """ def decorator(f: AnyCallable) -> Command: f = _inject_args(f) return cast("Command", Group.command(self, *args, **kwargs)(f)) return decorator class LitestarExtensionGroup(LitestarGroup): """``LitestarGroup`` subclass that will load Litestar-CLI extensions from the `litestar.commands` entry_point. This group class should not be used on any group besides the root ``litestar_group``. """ def __init__( self, name: str | None = None, commands: dict[str, Command] | Sequence[Command] | None = None, **attrs: Any, ) -> None: """Init ``LitestarExtensionGroup``""" super().__init__(name=name, commands=commands, **attrs) self._prepare_done = False self._preparsed_app_dir: str | None = None self._preparsed_app_path: Path | None = None for entry_point in entry_points(group="litestar.commands"): command = entry_point.load() _wrap_commands([command]) self.add_command(command, entry_point.name) def _prepare(self, ctx: Context) -> None: if self._prepare_done: return if isinstance(ctx.obj, LitestarEnv): env: LitestarEnv | None = ctx.obj else: try: app_path = ctx.params.get("app_path", self._preparsed_app_path) app_dir = ctx.params.get("app_dir", self._preparsed_app_dir) env = ctx.obj = LitestarEnv.from_env(app_path, app_dir) except LitestarCLIException: env = None if env: for plugin in env.app.plugins.cli: plugin.on_cli_init(self) self._prepare_done = True def make_context( # type: ignore[override] self, info_name: str | None, args: list[str], parent: Context | None = None, **extra: Any, ) -> Context: ctx = super().make_context(info_name, args, parent, **extra) self._prepare(ctx) return ctx def parse_args(self, ctx: Context, args: list[str]) -> list[str]: """Preparse launch arguments and save app_path & app_dir to slots. This block is triggered in any case, but its results are only used if the --help command is invoked. """ parser = self.make_parser(ctx) original_ignore_unknown_option = ctx.ignore_unknown_options ctx.ignore_unknown_options = True opts, remaining_args, order = parser.parse_args(list(args)) self._preparsed_app_path = opts.get("app_path", None) self._preparsed_app_dir = opts.get("app_dir", None) ctx.ignore_unknown_options = original_ignore_unknown_option return super().parse_args(ctx, args) def list_commands(self, ctx: Context) -> list[str]: self._prepare(ctx) return super().list_commands(ctx) def _inject_args(func: Callable[P, T]) -> Callable[P, T]: """Inject the app instance into a ``Command``""" params = inspect.signature(func).parameters @wraps(func) def wrapped(ctx: Context, /, *args: P.args, **kwargs: P.kwargs) -> T: needs_app = "app" in params needs_env = "env" in params if needs_env or needs_app: # only resolve this if actually requested. Commands that don't need an env or app should be able to run # without if not isinstance(ctx.obj, LitestarEnv): ctx.obj = ctx.obj() env = ctx.ensure_object(LitestarEnv) if needs_app: kwargs["app"] = env.app if needs_env: kwargs["env"] = env if "ctx" in params: kwargs["ctx"] = ctx return func(*args, **kwargs) return pass_context(wrapped) def _wrap_commands(commands: Iterable[Command]) -> None: for command in commands: if hasattr(command, "commands"): _wrap_commands(command.commands.values()) # pyright: ignore[reportGeneralTypeIssues] elif command.callback: command.callback = _inject_args(command.callback) def _bool_from_env(key: str, default: bool = False) -> bool: value = getenv(key) if not value: return default value = value.lower() return value in ("true", "1") def _validate_app_path(app_path: str) -> tuple[ModuleType, str]: try: module_path, app_name = app_path.split(":") except ValueError: console.print(f"[bold red] Invalid argument passed --app {app_path!r}: Expected 'module_path:app'") sys.exit(1) try: module = importlib.import_module(module_path) except ImportError as e: if e.name == module_path: console.print(f"[bold red] Invalid argument passed --app {app_path!r}: Module not found") sys.exit(1) else: raise (e) return module, app_name def _load_app_from_path(app_path: str) -> LoadedApp: module, app_name = _validate_app_path(app_path) app = getattr(module, app_name) is_factory = False if not isinstance(app, Litestar) and callable(app): app = app() is_factory = True return LoadedApp(app=app, app_path=app_path, is_factory=is_factory) def _path_to_dotted_path(path: Path) -> str: if path.stem == "__init__": path = path.parent return ".".join(path.with_suffix("").parts) def _arbitrary_autodiscovery_paths(base_dir: Path) -> Generator[Path, None, None]: yield from _autodiscovery_paths(base_dir, arbitrary=False) for path in base_dir.iterdir(): if path.name.startswith(".") or path.name.startswith("_"): continue if path.is_file() and path.suffix == ".py": yield path def _autodiscovery_paths(base_dir: Path, arbitrary: bool = True) -> Generator[Path, None, None]: for name in AUTODISCOVERY_FILE_NAMES: path = base_dir / name if path.exists() or path.with_suffix(".py").exists(): yield path if arbitrary and path.is_dir(): yield from _arbitrary_autodiscovery_paths(path) def _autodiscover_app(cwd: Path) -> LoadedApp: app_name = getenv("LITESTAR_APP_NAME") or "Litestar" quiet_console = getenv("LITESTAR_QUIET_CONSOLE") or False for file_path in _autodiscovery_paths(cwd): import_path = _path_to_dotted_path(file_path.relative_to(cwd)) module = importlib.import_module(import_path) for attr, value in chain( [("app", getattr(module, "app", None)), ("application", getattr(module, "application", None))], module.__dict__.items(), ): if isinstance(value, Litestar): app_string = f"{import_path}:{attr}" os.environ["LITESTAR_APP"] = app_string if not quiet_console and isatty(): console.print(f"Using {app_name} app from [bright_blue]{app_string}") return LoadedApp(app=value, app_path=app_string, is_factory=False) if hasattr(module, "create_app"): app_string = f"{import_path}:create_app" os.environ["LITESTAR_APP"] = app_string if not quiet_console and isatty(): console.print(f"Using {app_name} factory from [bright_blue]{app_string}") return LoadedApp(app=module.create_app(), app_path=app_string, is_factory=True) for attr, value in module.__dict__.items(): if not callable(value): continue return_annotation = ( get_type_hints(value, include_extras=True).get("return") if hasattr(value, "__annotations__") else None ) if not return_annotation: continue if return_annotation in ("Litestar", Litestar): app_string = f"{import_path}:{attr}" os.environ["LITESTAR_APP"] = app_string if not quiet_console and sys.stdout.isatty(): console.print(f"Using {app_name} factory from [bright_blue]{app_string}") return LoadedApp(app=value(), app_path=f"{app_string}", is_factory=True) raise LitestarCLIException(f"Could not find {app_name} instance or factory") def _format_is_enabled(value: Any) -> str: """Return a coloured string `"Enabled" if ``value`` is truthy, else "Disabled".""" return "[green]Enabled[/]" if value else "[red]Disabled[/]" def show_app_info(app: Litestar) -> None: # pragma: no cover """Display basic information about the application and its configuration.""" table = Table(show_header=False) table.add_column("title", style="cyan") table.add_column("value", style="bright_blue") table.add_row("Litestar version", f"{__version__.major}.{__version__.minor}.{__version__.patch}") table.add_row("Debug mode", _format_is_enabled(app.debug)) table.add_row("Python Debugger on exception", _format_is_enabled(app.pdb_on_exception)) table.add_row("CORS", _format_is_enabled(app.cors_config)) table.add_row("CSRF", _format_is_enabled(app.csrf_config)) if app.allowed_hosts: allowed_hosts = app.allowed_hosts table.add_row("Allowed hosts", ", ".join(allowed_hosts.allowed_hosts)) openapi_enabled = _format_is_enabled(app.openapi_config) if app.openapi_config: path = ( app.openapi_config.openapi_controller.path if app.openapi_config.openapi_controller else app.openapi_config.path or "/schema" ) openapi_enabled += f" path=[yellow]{path}" table.add_row("OpenAPI", openapi_enabled) table.add_row("Compression", app.compression_config.backend if app.compression_config else "[red]Disabled") if app.template_engine: table.add_row("Template engine", type(app.template_engine).__name__) if app.static_files_config: static_files_configs = app.static_files_config static_files_info = [ f"path=[yellow]{static_files.path}[/] dirs=[yellow]{', '.join(map(str, static_files.directories))}[/] " f"html_mode={_format_is_enabled(static_files.html_mode)}" for static_files in static_files_configs ] table.add_row("Static files", "\n".join(static_files_info)) middlewares = [] for middleware in app.middleware: updated_middleware = middleware.middleware if isinstance(middleware, DefineMiddleware) else middleware middlewares.append(get_name(updated_middleware)) if middlewares: table.add_row("Middlewares", ", ".join(middlewares)) console.print(table) def validate_ssl_file_paths(certfile_arg: str | None, keyfile_arg: str | None) -> tuple[str, str] | tuple[None, None]: """Validate whether given paths exist, are not directories and were both provided or none was. Return the resolved paths. Args: certfile_arg: path argument for the certificate file keyfile_arg: path argument for the key file Returns: tuple of resolved paths converted to str or tuple of None's if no argument was provided """ if certfile_arg is None and keyfile_arg is None: return (None, None) resolved_paths = [] for argname, arg in {"--ssl-certfile": certfile_arg, "--ssl-keyfile": keyfile_arg}.items(): if arg is None: raise LitestarCLIException(f"No value provided for {argname}") path = Path(arg).resolve() if path.is_dir(): raise LitestarCLIException(f"Path provided for {argname} is a directory: {path}") if not path.exists(): raise LitestarCLIException(f"File provided for {argname} was not found: {path}") resolved_paths.append(str(path)) return tuple(resolved_paths) # type: ignore[return-value] def create_ssl_files( certfile_arg: str | None, keyfile_arg: str | None, common_name: str = "localhost" ) -> tuple[str, str]: """Validate whether both files were provided, are not directories, their parent dirs exist and either both files exists or none does. If neither file exists, create a self-signed ssl certificate and a passwordless key at the location. Args: certfile_arg: path argument for the certificate file keyfile_arg: path argument for the key file common_name: the CN to be used as cert issuer and subject Returns: resolved paths of the found or generated files """ resolved_paths = [] for argname, arg in {"--ssl-certfile": certfile_arg, "--ssl-keyfile": keyfile_arg}.items(): if arg is None: raise LitestarCLIException(f"No value provided for {argname}") path = Path(arg).resolve() if path.is_dir(): raise LitestarCLIException(f"Path provided for {argname} is a directory: {path}") if not (parent_dir := path.parent).exists(): raise LitestarCLIException( f"Could not create file, parent directory for {argname} doesn't exist: {parent_dir}" ) resolved_paths.append(path) if (not resolved_paths[0].exists()) ^ (not resolved_paths[1].exists()): raise LitestarCLIException( "Both certificate and key file must exists or both must not exists when using --create-self-signed-cert" ) if (not resolved_paths[0].exists()) and (not resolved_paths[1].exists()): _generate_self_signed_cert(resolved_paths[0], resolved_paths[1], common_name) return (str(resolved_paths[0]), str(resolved_paths[1])) def _generate_self_signed_cert(certfile_path: Path, keyfile_path: Path, common_name: str) -> None: """Create a self-signed certificate using the cryptography modules at given paths""" try: from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID except ImportError as err: raise LitestarCLIException( "Cryptography must be installed when using --create-self-signed-cert\nPlease install the litestar[cryptography] extras" ) from err subject = x509.Name( [ x509.NameAttribute(NameOID.COMMON_NAME, common_name), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Development Certificate"), ] ) key = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend()) cert = ( x509.CertificateBuilder() .subject_name(subject) .issuer_name(subject) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(datetime.now(tz=timezone.utc)) .not_valid_after(datetime.now(tz=timezone.utc) + timedelta(days=365)) .add_extension(x509.SubjectAlternativeName([x509.DNSName(common_name)]), critical=False) .add_extension(x509.ExtendedKeyUsage([x509.OID_SERVER_AUTH]), critical=False) .sign(key, hashes.SHA256(), default_backend()) ) with certfile_path.open("wb") as cert_file: cert_file.write(cert.public_bytes(serialization.Encoding.PEM)) with keyfile_path.open("wb") as key_file: key_file.write( key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption(), ) ) def remove_routes_with_patterns( routes: list[HTTPRoute | ASGIRoute | WebSocketRoute], patterns: tuple[str, ...] ) -> list[HTTPRoute | ASGIRoute | WebSocketRoute]: regex_routes = [] valid_patterns = [] for pattern in patterns: try: check_pattern = re.compile(pattern) valid_patterns.append(check_pattern) except re.error as e: console.print(f"Error: {e}. Invalid regex pattern supplied: '{pattern}'. Omitting from querying results.") for route in routes: checked_pattern_route_matches = [] for pattern_compile in valid_patterns: matches = pattern_compile.match(route.path) checked_pattern_route_matches.append(matches) if not any(checked_pattern_route_matches): regex_routes.append(route) return regex_routes def remove_default_schema_routes( routes: list[HTTPRoute | ASGIRoute | WebSocketRoute], openapi_config: OpenAPIConfig ) -> list[HTTPRoute | ASGIRoute | WebSocketRoute]: schema_path = ( (openapi_config.path or "/schema") if openapi_config.openapi_controller is None else openapi_config.openapi_controller.path ) return remove_routes_with_patterns(routes, (schema_path,)) def isatty() -> bool: """Detect if a terminal is TTY enabled. This is a convenience wrapper around the built in system methods. This allows for easier testing of TTY/non-TTY modes. """ return sys.stdout.isatty() litestar-2.16.0/litestar/cli/commands/000077500000000000000000000000001500564371300176325ustar00rootroot00000000000000litestar-2.16.0/litestar/cli/commands/__init__.py000066400000000000000000000000001500564371300217310ustar00rootroot00000000000000litestar-2.16.0/litestar/cli/commands/core.py000066400000000000000000000275321500564371300211450ustar00rootroot00000000000000from __future__ import annotations import inspect import multiprocessing import os import subprocess import sys from contextlib import AbstractContextManager, ExitStack, contextmanager from typing import TYPE_CHECKING, Any, Iterator try: import rich_click as click except ImportError: import click # type: ignore[no-redef] from rich.tree import Tree from litestar.app import DEFAULT_OPENAPI_CONFIG from litestar.cli._utils import ( UVICORN_INSTALLED, LitestarEnv, console, create_ssl_files, isatty, remove_default_schema_routes, remove_routes_with_patterns, show_app_info, validate_ssl_file_paths, ) from litestar.routes import ASGIRoute, HTTPRoute, WebSocketRoute from litestar.utils.helpers import unwrap_partial __all__ = ("info_command", "routes_command", "run_command") if TYPE_CHECKING: from litestar import Litestar @contextmanager def _server_lifespan(app: Litestar) -> Iterator[None]: """Context manager handling the ASGI server lifespan. It will be entered just before the ASGI server is started through the CLI. """ with ExitStack() as exit_stack: for manager in app._server_lifespan_managers: if not isinstance(manager, AbstractContextManager): manager = manager(app) # type: ignore[assignment] exit_stack.enter_context(manager) # type: ignore[arg-type] yield def _convert_uvicorn_args(args: dict[str, Any]) -> list[str]: process_args = [] for arg, value in args.items(): if isinstance(value, bool): if value: process_args.append(f"--{arg}") elif isinstance(value, tuple): process_args.extend(f"--{arg}={item}" for item in value) else: process_args.append(f"--{arg}={value}") return process_args def _run_uvicorn_in_subprocess( *, env: LitestarEnv, host: str | None, port: int | None, workers: int | None, reload: bool, reload_dirs: tuple[str, ...] | None, reload_include: tuple[str, ...] | None, reload_exclude: tuple[str, ...] | None, fd: int | None, uds: str | None, certfile_path: str | None, keyfile_path: str | None, ) -> None: process_args: dict[str, Any] = { "reload": reload, "host": host, "port": port, "workers": workers, "factory": env.is_app_factory, } if fd is not None: process_args["fd"] = fd if uds is not None: process_args["uds"] = uds if reload_dirs: process_args["reload-dir"] = reload_dirs if reload_include: process_args["reload-include"] = reload_include if reload_exclude: process_args["reload-exclude"] = reload_exclude if certfile_path is not None: process_args["ssl-certfile"] = certfile_path if keyfile_path is not None: process_args["ssl-keyfile"] = keyfile_path subprocess.run( # noqa: S603 [sys.executable, "-m", "uvicorn", env.app_path, *_convert_uvicorn_args(process_args)], check=True, ) class CommaSplittedPath(click.Path): """A Click Path that splits the input string by commas. .. versionadded:: 2.8.0 """ envvar_list_splitter = "," @click.command(name="version") @click.option("-s", "--short", help="Exclude release level and serial information", is_flag=True, default=False) def version_command(short: bool) -> None: """Show the currently installed Litestar version.""" from litestar import __version__ click.echo(__version__.formatted(short=short)) @click.command(name="info") def info_command(app: Litestar) -> None: """Show information about the detected Litestar app.""" show_app_info(app) @click.command(name="run") @click.option("-r", "--reload", help="Reload server on changes", default=False, is_flag=True, envvar="LITESTAR_RELOAD") @click.option( "-R", "--reload-dir", help="Directories to watch for file changes", type=CommaSplittedPath(), multiple=True, envvar="LITESTAR_RELOAD_DIRS", ) @click.option( "-I", "--reload-include", help="Glob patterns for files to include when watching for file changes", type=CommaSplittedPath(), multiple=True, envvar="LITESTAR_RELOAD_INCLUDES", ) @click.option( "-E", "--reload-exclude", help="Glob patterns for files to exclude when watching for file changes", type=CommaSplittedPath(), multiple=True, envvar="LITESTAR_RELOAD_EXCLUDES", ) @click.option( "-p", "--port", help="Serve under this port", type=int, default=8000, show_default=True, envvar="LITESTAR_PORT" ) @click.option( "-W", "--wc", "--web-concurrency", help="The number of HTTP workers to launch", type=click.IntRange(min=1, max=multiprocessing.cpu_count() + 1), show_default=True, default=1, envvar=["LITESTAR_WEB_CONCURRENCY", "WEB_CONCURRENCY"], ) @click.option( "-H", "--host", help="Server under this host", default="127.0.0.1", show_default=True, envvar="LITESTAR_HOST" ) @click.option( "-F", "--fd", "--file-descriptor", help="Bind to a socket from this file descriptor.", type=int, default=None, show_default=True, envvar="LITESTAR_FILE_DESCRIPTOR", ) @click.option( "-U", "--uds", "--unix-domain-socket", help="Bind to a UNIX domain socket.", default=None, show_default=True, envvar="LITESTAR_UNIX_DOMAIN_SOCKET", ) @click.option("-d", "--debug", help="Run app in debug mode", is_flag=True, envvar="LITESTAR_DEBUG") @click.option("-P", "--pdb", "--use-pdb", help="Drop into PDB on an exception", is_flag=True, envvar="LITESTAR_PDB") @click.option("--ssl-certfile", help="Location of the SSL cert file", default=None, envvar="LITESTAR_SSL_CERT_PATH") @click.option("--ssl-keyfile", help="Location of the SSL key file", default=None, envvar="LITESTAR_SSL_KEY_PATH") @click.option( "--create-self-signed-cert", help="If certificate and key are not found at specified locations, create a self-signed certificate and a key", is_flag=True, envvar="LITESTAR_CREATE_SELF_SIGNED_CERT", ) def run_command( reload: bool, port: int, wc: int, host: str, fd: int | None, uds: str | None, debug: bool, reload_dir: tuple[str, ...], reload_include: tuple[str, ...], reload_exclude: tuple[str, ...], pdb: bool, ssl_certfile: str | None, ssl_keyfile: str | None, create_self_signed_cert: bool, ctx: click.Context, ) -> None: """Run a Litestar app. (requires 'uvicorn' to be installed). The application will be automatically discovered, or can be set as an option to the main 'litestar' command. Run 'litestar --help' for more information about app autodiscovery """ if debug: os.environ["LITESTAR_DEBUG"] = "1" if pdb: os.environ["LITESTAR_PDB"] = "1" quiet_console = os.getenv("LITESTAR_QUIET_CONSOLE") or False if not UVICORN_INSTALLED: console.print( r"uvicorn is not installed. Please install the standard group, litestar\[standard], to use this command." ) sys.exit(1) if callable(ctx.obj): ctx.obj = ctx.obj() else: if debug: ctx.obj.app.debug = True if pdb: ctx.obj.app.pdb_on_exception = True env: LitestarEnv = ctx.obj app = env.app reload = reload or bool(reload_dir) or bool(reload_include) or bool(reload_exclude) workers = wc certfile_path, keyfile_path = ( create_ssl_files(ssl_certfile, ssl_keyfile, host) if create_self_signed_cert else validate_ssl_file_paths(ssl_certfile, ssl_keyfile) ) if not quiet_console and isatty(): console.rule("[yellow]Starting server process", align="left") show_app_info(app) with _server_lifespan(app): if workers == 1 and not reload: import uvicorn # A guard statement at the beginning of this function prevents uvicorn from being unbound # See "reportUnboundVariable in: # https://microsoft.github.io/pyright/#/configuration?id=type-check-diagnostics-settings uvicorn.run( # pyright: ignore app=env.app_path, host=host, port=port, fd=fd, uds=uds, factory=env.is_app_factory, ssl_certfile=certfile_path, ssl_keyfile=keyfile_path, ) else: # invoke uvicorn in a subprocess to be able to use the --reload flag. see # https://github.com/litestar-org/litestar/issues/1191 and https://github.com/encode/uvicorn/issues/1045 if sys.gettrace() is not None: console.print( "[yellow]Debugger detected. Breakpoints might not work correctly inside route handlers when running" " with the --reload or --workers options[/]" ) _run_uvicorn_in_subprocess( env=env, host=host, port=port, workers=workers, reload=reload, reload_dirs=reload_dir, reload_include=reload_include, reload_exclude=reload_exclude, fd=fd, uds=uds, certfile_path=certfile_path, keyfile_path=keyfile_path, ) @click.command(name="routes") @click.option("--schema", help="Include schema routes", is_flag=True, default=False) @click.option("--exclude", help="routes to exclude via regex", type=str, is_flag=False, multiple=True) def routes_command(app: Litestar, exclude: tuple[str, ...], schema: bool) -> None: # pragma: no cover """Display information about the application's routes.""" sorted_routes = sorted(app.routes, key=lambda r: r.path) if not schema: openapi_config = app.openapi_config or DEFAULT_OPENAPI_CONFIG sorted_routes = remove_default_schema_routes(sorted_routes, openapi_config) if exclude is not None: sorted_routes = remove_routes_with_patterns(sorted_routes, exclude) console.print(_RouteTree(sorted_routes)) class _RouteTree(Tree): def __init__(self, routes: list[HTTPRoute | ASGIRoute | WebSocketRoute]) -> None: super().__init__("", hide_root=True) self._routes = routes self._build() def _build(self) -> None: for route in self._routes: if isinstance(route, HTTPRoute): self._handle_http_route(route) elif isinstance(route, WebSocketRoute): self._handle_websocket_route(route) else: self._handle_asgi_route(route) def _handle_asgi_like_route(self, route: ASGIRoute | WebSocketRoute, route_type: str) -> None: branch = self.add(f"[green]{route.path}[/green] ({route_type})") branch.add(f"[blue]{route.route_handler.name or route.route_handler.handler_name}[/blue]") def _handle_asgi_route(self, route: ASGIRoute) -> None: self._handle_asgi_like_route(route, route_type="ASGI") def _handle_websocket_route(self, route: WebSocketRoute) -> None: self._handle_asgi_like_route(route, route_type="WS") def _handle_http_route(self, route: HTTPRoute) -> None: branch = self.add(f"[green]{route.path}[/green] (HTTP)") for handler in route.route_handlers: handler_info = [ f"[blue]{handler.name or handler.handler_name}[/blue]", ] if inspect.iscoroutinefunction(unwrap_partial(handler.fn)): handler_info.append("[magenta]async[/magenta]") else: handler_info.append("[yellow]sync[/yellow]") handler_info.append(f'[cyan]{", ".join(sorted(handler.http_methods))}[/cyan]') if len(handler.paths) > 1: for path in handler.paths: branch.add(" ".join([f"[green]{path}[green]", *handler_info])) else: branch.add(" ".join(handler_info)) litestar-2.16.0/litestar/cli/commands/schema.py000066400000000000000000000056121500564371300214500ustar00rootroot00000000000000from pathlib import Path import msgspec from click import Path as ClickPath try: import rich_click as click except ImportError: import click # type: ignore[no-redef] from yaml import dump as dump_yaml from litestar import Litestar from litestar._openapi.typescript_converter.converter import ( convert_openapi_to_typescript, ) from litestar.cli._utils import JSBEAUTIFIER_INSTALLED, LitestarCLIException, LitestarGroup from litestar.serialization import encode_json, get_serializer __all__ = ("generate_openapi_schema", "generate_typescript_specs", "schema_group") @click.group(cls=LitestarGroup, name="schema") def schema_group() -> None: """Manage server-side OpenAPI schemas.""" def _generate_openapi_schema(app: Litestar, output: Path) -> None: """Generate an OpenAPI Schema.""" serializer = get_serializer(app.type_encoders) if output.suffix in (".yml", ".yaml"): content = dump_yaml( msgspec.to_builtins(app.openapi_schema.to_schema(), enc_hook=serializer), default_flow_style=False, encoding="utf-8", ) else: content = msgspec.json.format( encode_json(app.openapi_schema.to_schema(), serializer=serializer), indent=4, ) try: output.write_bytes(content) except OSError as e: # pragma: no cover raise LitestarCLIException(f"failed to write schema to path {output}") from e @schema_group.command("openapi") # type: ignore[misc] @click.option( "--output", help="output file path", type=ClickPath(dir_okay=False, path_type=Path), default=Path("openapi_schema.json"), show_default=True, ) def generate_openapi_schema(app: Litestar, output: Path) -> None: """Generate an OpenAPI Schema.""" _generate_openapi_schema(app, output) @schema_group.command("typescript") # type: ignore[misc] @click.option( "--output", help="output file path", type=ClickPath(dir_okay=False, path_type=Path), default=Path("api-specs.ts"), show_default=True, ) @click.option("--namespace", help="namespace to use for the typescript specs", type=str, default="API") def generate_typescript_specs(app: Litestar, output: Path, namespace: str) -> None: """Generate TypeScript specs from the OpenAPI schema.""" if JSBEAUTIFIER_INSTALLED: # pragma: no cover from jsbeautifier import Beautifier beautifier = Beautifier() else: beautifier = None try: specs = convert_openapi_to_typescript(app.openapi_schema, namespace) # beautifier will be defined if JSBEAUTIFIER_INSTALLED is True specs_output = ( beautifier.beautify(specs.write()) if JSBEAUTIFIER_INSTALLED and beautifier else specs.write() # pyright: ignore ) output.write_text(specs_output) except OSError as e: # pragma: no cover raise LitestarCLIException(f"failed to write schema to path {output}") from e litestar-2.16.0/litestar/cli/commands/sessions.py000066400000000000000000000044511500564371300220560ustar00rootroot00000000000000try: import rich_click as click except ImportError: import click # type: ignore[no-redef] from rich.prompt import Confirm from litestar import Litestar from litestar.cli._utils import LitestarCLIException, LitestarGroup, console from litestar.middleware import DefineMiddleware from litestar.middleware.session import SessionMiddleware from litestar.middleware.session.server_side import ServerSideSessionBackend from litestar.utils import is_class_and_subclass __all__ = ("clear_sessions_command", "delete_session_command", "get_session_backend", "sessions_group") def get_session_backend(app: Litestar) -> ServerSideSessionBackend: """Get the session backend used by a ``Litestar`` app.""" for middleware in app.middleware: if isinstance(middleware, DefineMiddleware): if not is_class_and_subclass(middleware.middleware, SessionMiddleware): continue backend = middleware.kwargs["backend"] if not isinstance(backend, ServerSideSessionBackend): raise LitestarCLIException("Only server-side backends are supported") return backend raise LitestarCLIException("Session middleware not installed") @click.group(cls=LitestarGroup, name="sessions") def sessions_group() -> None: """Manage server-side sessions.""" @sessions_group.command("delete") # type: ignore[misc] @click.argument("session-id") def delete_session_command(session_id: str, app: Litestar) -> None: """Delete a specific session.""" import anyio backend = get_session_backend(app) store = backend.config.get_store_from_app(app) if Confirm.ask(f"Delete session {session_id!r}?"): anyio.run(backend.delete, session_id, store) console.print(f"[green]Deleted session {session_id!r}") @sessions_group.command("clear") # type: ignore[misc] def clear_sessions_command(app: Litestar) -> None: """Delete all sessions.""" import anyio backend = get_session_backend(app) store = backend.config.get_store_from_app(app) if not hasattr(store, "delete_all"): raise LitestarCLIException(f"{type(store)} does not support clearing all sessions") if Confirm.ask("[red]Delete all sessions?"): anyio.run(store.delete_all) # pyright: ignore console.print("[green]All active sessions deleted") litestar-2.16.0/litestar/cli/main.py000066400000000000000000000040441500564371300173310ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path try: import rich_click as click except ImportError: import click # type: ignore[no-redef] from click import Path as ClickPath from ._utils import LitestarEnv, LitestarExtensionGroup from .commands import core, schema, sessions __all__ = ("litestar_group",) @click.group(cls=LitestarExtensionGroup, context_settings={"help_option_names": ["-h", "--help"]}) @click.option("--app", "app_path", help="Module path to a Litestar application") @click.option( "--app-dir", help="Look for APP in the specified directory, by adding this to the PYTHONPATH. Defaults to the current working directory.", default=None, type=ClickPath(dir_okay=True, file_okay=False, path_type=Path), show_default=False, ) @click.pass_context def litestar_group(ctx: click.Context, app_path: str | None, app_dir: Path | None = None) -> None: """Litestar CLI. The application to will be automatically discovered if it's in one of these canonical paths: 'app.py', 'asgi.py', 'application.py' or 'app/__init__.py'. When auto-discovering application factories, functions with the name 'create_app' are considered, or functions that are annotated as returning a 'Litestar' instance. Alternatively, the application can be specified explicitly via the '--app' option ('litestar --app=.:') or the 'LITESTAR_APP' environment variable of the same name. """ if ctx.obj is None: # env has not been loaded yet, so we can lazy load it ctx.obj = lambda: LitestarEnv.from_env(app_path, app_dir=app_dir) # add sub commands here litestar_group.add_command(core.info_command) # pyright: ignore litestar_group.add_command(core.run_command) # pyright: ignore litestar_group.add_command(core.routes_command) # pyright: ignore litestar_group.add_command(core.version_command) # pyright: ignore litestar_group.add_command(sessions.sessions_group) # pyright: ignore litestar_group.add_command(schema.schema_group) # pyright: ignore litestar-2.16.0/litestar/concurrency.py000066400000000000000000000064471500564371300202010ustar00rootroot00000000000000from __future__ import annotations import asyncio import contextvars from functools import partial from typing import TYPE_CHECKING, Callable, TypeVar import sniffio from typing_extensions import ParamSpec if TYPE_CHECKING: from concurrent.futures import ThreadPoolExecutor import trio T = TypeVar("T") P = ParamSpec("P") __all__ = ( "get_asyncio_executor", "get_trio_capacity_limiter", "set_asyncio_executor", "set_trio_capacity_limiter", "sync_to_thread", ) class _State: EXECUTOR: ThreadPoolExecutor | None = None LIMITER: trio.CapacityLimiter | None = None async def _run_sync_asyncio(fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: ctx = contextvars.copy_context() bound_fn = partial(ctx.run, fn, *args, **kwargs) return await asyncio.get_running_loop().run_in_executor(get_asyncio_executor(), bound_fn) # pyright: ignore async def _run_sync_trio(fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: import trio return await trio.to_thread.run_sync(partial(fn, *args, **kwargs), limiter=get_trio_capacity_limiter()) async def sync_to_thread(fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: """Run the synchronous callable ``fn`` asynchronously in a worker thread. When called from asyncio, uses :meth:`asyncio.loop.run_in_executor` to run the callable. No executor is specified by default so the current loop's executor is used. A specific executor can be set using :func:`~litestar.concurrency.set_asyncio_executor`. This does not affect the loop's default executor. When called from trio, uses :func:`trio.to_thread.run_sync` to run the callable. No capacity limiter is specified by default, but one can be set using :func:`~litestar.concurrency.set_trio_capacity_limiter`. This does not affect trio's default capacity limiter. """ if (library := sniffio.current_async_library()) == "asyncio": return await _run_sync_asyncio(fn, *args, **kwargs) if library == "trio": return await _run_sync_trio(fn, *args, **kwargs) raise RuntimeError("Unsupported async library or not in async context") def set_asyncio_executor(executor: ThreadPoolExecutor | None) -> None: """Set the executor in which synchronous callables will be run within an asyncio context """ try: sniffio.current_async_library() except sniffio.AsyncLibraryNotFoundError: pass else: raise RuntimeError("Cannot set executor from running loop") _State.EXECUTOR = executor def get_asyncio_executor() -> ThreadPoolExecutor | None: """Get the executor in which synchronous callables will be run within an asyncio context """ return _State.EXECUTOR def set_trio_capacity_limiter(limiter: trio.CapacityLimiter | None) -> None: """Set the capacity limiter used when running synchronous callable within a trio context """ try: sniffio.current_async_library() except sniffio.AsyncLibraryNotFoundError: pass else: raise RuntimeError("Cannot set limiter while in async context") _State.LIMITER = limiter def get_trio_capacity_limiter() -> trio.CapacityLimiter | None: """Get the capacity limiter used when running synchronous callable within a trio context """ return _State.LIMITER litestar-2.16.0/litestar/config/000077500000000000000000000000001500564371300165275ustar00rootroot00000000000000litestar-2.16.0/litestar/config/__init__.py000066400000000000000000000000001500564371300206260ustar00rootroot00000000000000litestar-2.16.0/litestar/config/allowed_hosts.py000066400000000000000000000033231500564371300217510ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING from litestar.exceptions import ImproperlyConfiguredException __all__ = ("AllowedHostsConfig",) if TYPE_CHECKING: from litestar.types import Scopes @dataclass class AllowedHostsConfig: """Configuration for allowed hosts protection. To enable allowed hosts protection, pass an instance of this class to the :class:`Litestar ` constructor using the ``allowed_hosts`` key. """ allowed_hosts: list[str] = field(default_factory=lambda: ["*"]) """A list of trusted hosts. Use ``*.`` to allow all hosts, or prefix domains with ``*.`` to allow all sub domains. """ exclude: str | list[str] | None = field(default=None) """A pattern or list of patterns to skip in the Allowed Hosts middleware.""" exclude_opt_key: str | None = field(default=None) """An identifier to use on routes to disable hosts check for a particular route.""" scopes: Scopes | None = field(default=None) """ASGI scopes processed by the middleware, if None both ``http`` and ``websocket`` will be processed.""" www_redirect: bool = field(default=True) """A boolean dictating whether to redirect requests that start with ``www.`` and otherwise match a trusted host.""" def __post_init__(self) -> None: """Ensure that the trusted hosts have correct domain wildcards.""" for host in self.allowed_hosts: if host != "*" and "*" in host and not host.startswith("*."): raise ImproperlyConfiguredException( "domain wildcards can only appear in the beginning of the domain, e.g. ``*.example.com``" ) litestar-2.16.0/litestar/config/app.py000066400000000000000000000314131500564371300176630ustar00rootroot00000000000000from __future__ import annotations import enum import pdb # noqa: T100 from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable from litestar.config.allowed_hosts import AllowedHostsConfig from litestar.config.response_cache import ResponseCacheConfig from litestar.datastructures import State from litestar.events.emitter import SimpleEventEmitter from litestar.types.empty import Empty if TYPE_CHECKING: from contextlib import AbstractAsyncContextManager from litestar import Litestar, Response from litestar.config.compression import CompressionConfig from litestar.config.cors import CORSConfig from litestar.config.csrf import CSRFConfig from litestar.connection import Request, WebSocket from litestar.datastructures import CacheControlHeader, ETag from litestar.di import Provide from litestar.dto import AbstractDTO from litestar.events.emitter import BaseEventEmitterBackend from litestar.events.listener import EventListener from litestar.logging.config import BaseLoggingConfig from litestar.openapi.config import OpenAPIConfig from litestar.openapi.spec import SecurityRequirement from litestar.plugins import PluginProtocol from litestar.static_files.config import StaticFilesConfig from litestar.stores.base import Store from litestar.stores.registry import StoreRegistry from litestar.types import ( AfterExceptionHookHandler, AfterRequestHookHandler, AfterResponseHookHandler, AnyCallable, BeforeMessageSendHookHandler, BeforeRequestHookHandler, ControllerRouterHandler, Debugger, ExceptionHandlersMap, Guard, Middleware, ParametersMap, ResponseCookies, ResponseHeaders, TypeEncodersMap, ) from litestar.types.callable_types import LifespanHook from litestar.types.composite_types import TypeDecodersSequence from litestar.types.empty import EmptyType from litestar.types.internal_types import TemplateConfigType __all__ = ( "AppConfig", "ExperimentalFeatures", ) @dataclass class AppConfig: """The parameters provided to the ``Litestar`` app are used to instantiate an instance, and then the instance is passed to any callbacks registered to ``on_app_init`` in the order they are provided. The final attribute values are used to instantiate the application object. """ after_exception: list[AfterExceptionHookHandler] = field(default_factory=list) """An application level :class:`exception hook handler <.types.AfterExceptionHookHandler>` or list thereof. This hook is called after an exception occurs. In difference to exception handlers, it is not meant to return a response - only to process the exception (e.g. log it, send it to Sentry etc.). """ after_request: AfterRequestHookHandler | None = field(default=None) """A sync or async function executed after the route handler function returned and the response object has been resolved. Receives the response object which may be any subclass of :class:`Response <.response.Response>`. """ after_response: AfterResponseHookHandler | None = field(default=None) """A sync or async function called after the response has been awaited. It receives the :class:`Request <.connection.Request>` object and should not return any values. """ allowed_hosts: list[str] | AllowedHostsConfig | None = field(default=None) """If set enables the builtin allowed hosts middleware.""" before_request: BeforeRequestHookHandler | None = field(default=None) """A sync or async function called immediately before calling the route handler. Receives the :class:`Request <.connection.Request>` instance and any non-``None`` return value is used for the response, bypassing the route handler. """ before_send: list[BeforeMessageSendHookHandler] = field(default_factory=list) """An application level :class:`before send hook handler <.types.BeforeMessageSendHookHandler>` or list thereof. This hook is called when the ASGI send function is called. """ cache_control: CacheControlHeader | None = field(default=None) """A ``cache-control`` header of type :class:`CacheControlHeader <.datastructures.CacheControlHeader>` to add to route handlers of this app. Can be overridden by route handlers. """ compression_config: CompressionConfig | None = field(default=None) """Configures compression behaviour of the application, this enabled a builtin or user defined Compression middleware. """ cors_config: CORSConfig | None = field(default=None) """If set this enables the builtin CORS middleware.""" csrf_config: CSRFConfig | None = field(default=None) """If set this enables the builtin CSRF middleware.""" debug: bool = field(default=False) """If ``True``, app errors rendered as HTML with a stack trace.""" dependencies: dict[str, Provide | AnyCallable] = field(default_factory=dict) """A string keyed dictionary of dependency :class:`Provider <.di.Provide>` instances.""" dto: type[AbstractDTO] | None | EmptyType = field(default=Empty) """:class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data.""" etag: ETag | None = field(default=None) """An ``etag`` header of type :class:`ETag <.datastructures.ETag>` to add to route handlers of this app. Can be overridden by route handlers. """ event_emitter_backend: type[BaseEventEmitterBackend] = field(default=SimpleEventEmitter) """A subclass of :class:`BaseEventEmitterBackend <.events.emitter.BaseEventEmitterBackend>`.""" exception_handlers: ExceptionHandlersMap = field(default_factory=dict) """A dictionary that maps handler functions to status codes and/or exception types.""" guards: list[Guard] = field(default_factory=list) """A list of :class:`Guard <.types.Guard>` callables.""" include_in_schema: bool | EmptyType = field(default=Empty) """A boolean flag dictating whether the route handler should be documented in the OpenAPI schema""" lifespan: list[Callable[[Litestar], AbstractAsyncContextManager] | AbstractAsyncContextManager] = field( default_factory=list ) """A list of callables returning async context managers, wrapping the lifespan of the ASGI application""" listeners: list[EventListener] = field(default_factory=list) """A list of :class:`EventListener <.events.listener.EventListener>`.""" logging_config: BaseLoggingConfig | None = field(default=None) """An instance of :class:`BaseLoggingConfig <.logging.config.BaseLoggingConfig>` subclass.""" middleware: list[Middleware] = field(default_factory=list) """A list of :class:`Middleware <.types.Middleware>`.""" on_shutdown: list[LifespanHook] = field(default_factory=list) """A list of :class:`LifespanHook <.types.LifespanHook>` called during application shutdown.""" on_startup: list[LifespanHook] = field(default_factory=list) """A list of :class:`LifespanHook <.types.LifespanHook>` called during application startup.""" openapi_config: OpenAPIConfig | None = field(default=None) """Defaults to :data:`DEFAULT_OPENAPI_CONFIG `""" opt: dict[str, Any] = field(default_factory=dict) """A string keyed dictionary of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope `. Can be overridden by routers and router handlers. """ parameters: ParametersMap = field(default_factory=dict) """A mapping of :class:`Parameter <.params.Parameter>` definitions available to all application paths.""" path: str = field(default="") """A base path that prefixed to all route handlers, controllers and routers associated with the application instance. .. versionadded:: 2.8.0 """ pdb_on_exception: bool = field(default=False) """Drop into the PDB on an exception""" debugger_module: Debugger = field(default=pdb) """A `pdb`-like debugger module that supports the `post_mortem()` protocol. This module will be used when `pdb_on_exception` is set to True.""" plugins: list[PluginProtocol] = field(default_factory=list) """List of :class:`SerializationPluginProtocol <.plugins.SerializationPluginProtocol>`.""" request_class: type[Request] | None = field(default=None) """An optional subclass of :class:`Request <.connection.Request>` to use for http connections.""" request_max_body_size: int | None | EmptyType = Empty """Maximum allowed size of the request body in bytes. If this size is exceeded, a '413 - Request Entity Too Large' error response is returned.""" response_class: type[Response] | None = field(default=None) """A custom subclass of :class:`Response <.response.Response>` to be used as the app's default response.""" response_cookies: ResponseCookies = field(default_factory=list) """A list of :class:`Cookie <.datastructures.Cookie>`.""" response_headers: ResponseHeaders = field(default_factory=list) """A string keyed dictionary mapping :class:`ResponseHeader <.datastructures.ResponseHeader>`.""" response_cache_config: ResponseCacheConfig = field(default_factory=ResponseCacheConfig) """Configures caching behavior of the application.""" return_dto: type[AbstractDTO] | None | EmptyType = field(default=Empty) """:class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. """ route_handlers: list[ControllerRouterHandler] = field(default_factory=list) """A required list of route handlers, which can include instances of :class:`Router <.router.Router>`, subclasses of :class:`Controller <.controller.Controller>` or any function decorated by the route handler decorators. """ security: list[SecurityRequirement] = field(default_factory=list) """A list of dictionaries that will be added to the schema of all route handlers in the application. See :data:`SecurityRequirement <.openapi.spec.SecurityRequirement>` for details. """ signature_namespace: dict[str, Any] = field(default_factory=dict) """A mapping of names to types for use in forward reference resolution during signature modelling.""" signature_types: list[Any] = field(default_factory=list) """A sequence of types for use in forward reference resolution during signature modelling. These types will be added to the signature namespace using their ``__name__`` attribute. """ state: State = field(default_factory=State) """A :class:`State` <.datastructures.State>` instance holding application state.""" static_files_config: list[StaticFilesConfig] = field(default_factory=list) """An instance or list of :class:`StaticFilesConfig <.static_files.StaticFilesConfig>`.""" stores: StoreRegistry | dict[str, Store] | None = None """Central registry of :class:`Store <.stores.base.Store>` to be made available and be used throughout the application. Can be either a dictionary mapping strings to :class:`Store <.stores.base.Store>` instances, or an instance of :class:`StoreRegistry <.stores.registry.StoreRegistry>`. """ tags: list[str] = field(default_factory=list) """A list of string tags that will be appended to the schema of all route handlers under the application.""" template_config: TemplateConfigType | None = field(default=None) """An instance of :class:`TemplateConfig <.template.TemplateConfig>`.""" type_decoders: TypeDecodersSequence | None = field(default=None) """A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization.""" type_encoders: TypeEncodersMap | None = field(default=None) """A mapping of types to callables that transform them into types supported for serialization.""" websocket_class: type[WebSocket] | None = field(default=None) """An optional subclass of :class:`WebSocket <.connection.WebSocket>` to use for websocket connections.""" multipart_form_part_limit: int = field(default=1000) """The maximal number of allowed parts in a multipart/formdata request. This limit is intended to protect from DoS attacks.""" experimental_features: list[ExperimentalFeatures] | None = None def __post_init__(self) -> None: """Normalize the allowed hosts to be a config or None. Returns: Optional config. """ if self.allowed_hosts and isinstance(self.allowed_hosts, list): self.allowed_hosts = AllowedHostsConfig(allowed_hosts=self.allowed_hosts) class ExperimentalFeatures(str, enum.Enum): DTO_CODEGEN = "DTO_CODEGEN" """Enable DTO codegen.""" FUTURE = "FUTURE" """Enable future features that may be considered breaking or changing.""" litestar-2.16.0/litestar/config/compression.py000066400000000000000000000072071500564371300214500ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Literal from litestar.exceptions import ImproperlyConfiguredException from litestar.middleware.compression import CompressionMiddleware from litestar.middleware.compression.gzip_facade import GzipCompression if TYPE_CHECKING: from litestar.middleware.compression.facade import CompressionFacade __all__ = ("CompressionConfig",) @dataclass class CompressionConfig: """Configuration for response compression. To enable response compression, pass an instance of this class to the :class:`Litestar <.app.Litestar>` constructor using the ``compression_config`` key. """ backend: Literal["gzip", "brotli"] | str """The backend to use. If the value given is `gzip` or `brotli`, then the builtin gzip and brotli compression is used. """ minimum_size: int = field(default=500) """Minimum response size (bytes) to enable compression, affects all backends.""" gzip_compress_level: int = field(default=9) """Range ``[0-9]``, see :doc:`python:library/gzip`.""" brotli_quality: int = field(default=5) """Range ``[0-11]``, Controls the compression-speed vs compression-density tradeoff. The higher the quality, the slower the compression. """ brotli_mode: Literal["generic", "text", "font"] = "text" """``MODE_GENERIC``, ``MODE_TEXT`` (for UTF-8 format text input, default) or ``MODE_FONT`` (for WOFF 2.0).""" brotli_lgwin: int = field(default=22) """Base 2 logarithm of size. Range is 10 to 24. Defaults to 22. """ brotli_lgblock: Literal[0, 16, 17, 18, 19, 20, 21, 22, 23, 24] = 0 """Base 2 logarithm of the maximum input block size. Range is ``16`` to ``24``. If set to ``0``, the value will be set based on the quality. Defaults to ``0``. """ brotli_gzip_fallback: bool = True """Use GZIP if Brotli is not supported.""" middleware_class: type[CompressionMiddleware] = CompressionMiddleware """Middleware class to use, should be a subclass of :class:`CompressionMiddleware`.""" exclude: str | list[str] | None = None """A pattern or list of patterns to skip in the compression middleware.""" exclude_opt_key: str | None = None """An identifier to use on routes to disable compression for a particular route.""" compression_facade: type[CompressionFacade] = GzipCompression """The compression facade to use for the actual compression.""" backend_config: Any = None """Configuration specific to the backend.""" gzip_fallback: bool = True """Use GZIP as a fallback if the provided backend is not supported by the client.""" def __post_init__(self) -> None: if self.minimum_size <= 0: raise ImproperlyConfiguredException("minimum_size must be greater than 0") if self.backend == "gzip": if self.gzip_compress_level < 0 or self.gzip_compress_level > 9: raise ImproperlyConfiguredException("gzip_compress_level must be a value between 0 and 9") elif self.backend == "brotli": # Brotli is not guaranteed to be installed. from litestar.middleware.compression.brotli_facade import BrotliCompression if self.brotli_quality < 0 or self.brotli_quality > 11: raise ImproperlyConfiguredException("brotli_quality must be a value between 0 and 11") if self.brotli_lgwin < 10 or self.brotli_lgwin > 24: raise ImproperlyConfiguredException("brotli_lgwin must be a value between 10 and 24") self.gzip_fallback = self.brotli_gzip_fallback self.compression_facade = BrotliCompression litestar-2.16.0/litestar/config/cors.py000066400000000000000000000120721500564371300200510ustar00rootroot00000000000000from __future__ import annotations import re from dataclasses import dataclass, field from functools import cached_property from typing import TYPE_CHECKING, Literal, Pattern from litestar.constants import DEFAULT_ALLOWED_CORS_HEADERS __all__ = ("CORSConfig",) if TYPE_CHECKING: from litestar.types import Method @dataclass class CORSConfig: """Configuration for CORS (Cross-Origin Resource Sharing). To enable CORS, pass an instance of this class to the :class:`Litestar ` constructor using the 'cors_config' key. """ allow_origins: list[str] = field(default_factory=lambda: ["*"]) """List of origins that are allowed. Can use '*' in any component of the path, e.g. 'domain.*'. Sets the 'Access-Control-Allow-Origin' header. """ allow_methods: list[Literal["*"] | Method] = field(default_factory=lambda: ["*"]) """List of allowed HTTP methods. Sets the 'Access-Control-Allow-Methods' header. """ allow_headers: list[str] = field(default_factory=lambda: ["*"]) """List of allowed headers. Sets the 'Access-Control-Allow-Headers' header. """ allow_credentials: bool = field(default=False) """Boolean dictating whether or not to set the 'Access-Control-Allow-Credentials' header.""" allow_origin_regex: str | None = field(default=None) """Regex to match origins against.""" expose_headers: list[str] = field(default_factory=list) """List of headers that are exposed via the 'Access-Control-Expose-Headers' header.""" max_age: int = field(default=600) """Response caching TTL in seconds, defaults to 600. Sets the 'Access-Control-Max-Age' header. """ def __post_init__(self) -> None: self.allow_headers = [v.lower() for v in self.allow_headers] @cached_property def allowed_origins_regex(self) -> Pattern[str]: """Get or create a compiled regex for allowed origins. Returns: A compiled regex of the allowed path. """ origins = self.allow_origins if self.allow_origin_regex: origins.append(self.allow_origin_regex) return re.compile("|".join([origin.replace("*.", r".*\.") for origin in origins])) @cached_property def is_allow_all_origins(self) -> bool: """Get a cached boolean flag dictating whether all origins are allowed. Returns: Boolean dictating whether all origins are allowed. """ return "*" in self.allow_origins @cached_property def is_allow_all_methods(self) -> bool: """Get a cached boolean flag dictating whether all methods are allowed. Returns: Boolean dictating whether all methods are allowed. """ return "*" in self.allow_methods @cached_property def is_allow_all_headers(self) -> bool: """Get a cached boolean flag dictating whether all headers are allowed. Returns: Boolean dictating whether all headers are allowed. """ return "*" in self.allow_headers @cached_property def preflight_headers(self) -> dict[str, str]: """Get cached pre-flight headers. Returns: A dictionary of headers to set on the response object. """ headers: dict[str, str] = {"Access-Control-Max-Age": str(self.max_age)} if self.is_allow_all_origins: headers["Access-Control-Allow-Origin"] = "*" else: headers["Vary"] = "Origin" if self.allow_credentials: headers["Access-Control-Allow-Credentials"] = str(self.allow_credentials).lower() if not self.is_allow_all_headers: headers["Access-Control-Allow-Headers"] = ", ".join( sorted(set(self.allow_headers) | DEFAULT_ALLOWED_CORS_HEADERS) # pyright: ignore ) if self.allow_methods: headers["Access-Control-Allow-Methods"] = ", ".join( sorted( {"DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"} if self.is_allow_all_methods else set(self.allow_methods) ) ) return headers @cached_property def simple_headers(self) -> dict[str, str]: """Get cached simple headers. Returns: A dictionary of headers to set on the response object. """ simple_headers = {} if self.is_allow_all_origins: simple_headers["Access-Control-Allow-Origin"] = "*" if self.allow_credentials: simple_headers["Access-Control-Allow-Credentials"] = "true" if self.expose_headers: simple_headers["Access-Control-Expose-Headers"] = ", ".join(sorted(set(self.expose_headers))) return simple_headers def is_origin_allowed(self, origin: str) -> bool: """Check whether a given origin is allowed. Args: origin: An origin header value. Returns: Boolean determining whether an origin is allowed. """ return bool(self.is_allow_all_origins or self.allowed_origins_regex.fullmatch(origin)) litestar-2.16.0/litestar/config/csrf.py000066400000000000000000000033661500564371300200460ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING, Literal __all__ = ("CSRFConfig",) if TYPE_CHECKING: from litestar.types import Method @dataclass class CSRFConfig: """Configuration for CSRF (Cross Site Request Forgery) protection. To enable CSRF protection, pass an instance of this class to the :class:`Litestar ` constructor using the 'csrf_config' key. """ secret: str """A string that is used to create an HMAC to sign the CSRF token.""" cookie_name: str = field(default="csrftoken") """The CSRF cookie name.""" cookie_path: str = field(default="/") """The CSRF cookie path.""" header_name: str = field(default="x-csrftoken") """The header that will be expected in each request.""" cookie_secure: bool = field(default=False) """A boolean value indicating whether to set the ``Secure`` attribute on the cookie.""" cookie_httponly: bool = field(default=False) """A boolean value indicating whether to set the ``HttpOnly`` attribute on the cookie.""" cookie_samesite: Literal["lax", "strict", "none"] = field(default="lax") """The value to set in the ``SameSite`` attribute of the cookie.""" cookie_domain: str | None = field(default=None) """Specifies which hosts can receive the cookie.""" safe_methods: set[Method] = field(default_factory=lambda: {"GET", "HEAD", "OPTIONS"}) """A set of "safe methods" that can set the cookie.""" exclude: str | list[str] | None = field(default=None) """A pattern or list of patterns to skip in the CSRF middleware.""" exclude_from_csrf_key: str = "exclude_from_csrf" """An identifier to use on routes to disable CSRF for a particular route.""" litestar-2.16.0/litestar/config/response_cache.py000066400000000000000000000056411500564371300220700ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable, final from urllib.parse import urlencode from litestar.status_codes import ( HTTP_200_OK, HTTP_300_MULTIPLE_CHOICES, HTTP_301_MOVED_PERMANENTLY, HTTP_308_PERMANENT_REDIRECT, ) if TYPE_CHECKING: from litestar import Litestar from litestar.connection import Request from litestar.stores.base import Store from litestar.types import CacheKeyBuilder, HTTPScope __all__ = ("CACHE_FOREVER", "ResponseCacheConfig", "default_cache_key_builder") @final class CACHE_FOREVER: # noqa: N801 """Sentinel value indicating that a cached response should be stored without an expiration, explicitly skipping the default expiration """ def default_cache_key_builder(request: Request[Any, Any, Any]) -> str: """Given a request object, returns a cache key by combining the request method and path with the sorted query params. Args: request: request used to generate cache key. Returns: A combination of url path and query parameters """ query_params: list[tuple[str, Any]] = list(request.query_params.dict().items()) query_params.sort(key=lambda x: x[0]) return request.method + request.url.path + urlencode(query_params, doseq=True) def default_do_cache_predicate(_: HTTPScope, status_code: int) -> bool: """Given a status code, returns a boolean indicating whether the response should be cached. Args: _: ASGI scope. status_code: status code of the response. Returns: A boolean indicating whether the response should be cached. """ return HTTP_200_OK <= status_code < HTTP_300_MULTIPLE_CHOICES or status_code in ( HTTP_301_MOVED_PERMANENTLY, HTTP_308_PERMANENT_REDIRECT, ) @dataclass class ResponseCacheConfig: """Configuration for response caching. To enable response caching, pass an instance of this class to :class:`Litestar <.app.Litestar>` using the ``response_cache_config`` key. """ default_expiration: int | None = 60 """Default cache expiration in seconds used when a route handler is configured with ``cache=True``.""" key_builder: CacheKeyBuilder = field(default=default_cache_key_builder) """:class:`CacheKeyBuilder <.types.CacheKeyBuilder>`. Defaults to :func:`default_cache_key_builder`.""" store: str = "response_cache" """Name of the :class:`Store <.stores.base.Store>` to use.""" cache_response_filter: Callable[[HTTPScope, int], bool] = field(default=default_do_cache_predicate) """A callable that receives connection scope and a status code, and returns a boolean indicating whether the response should be cached.""" def get_store_from_app(self, app: Litestar) -> Store: """Get the store defined in :attr:`store` from an :class:`Litestar <.app.Litestar>` instance.""" return app.stores.get(self.store) litestar-2.16.0/litestar/connection/000077500000000000000000000000001500564371300174215ustar00rootroot00000000000000litestar-2.16.0/litestar/connection/__init__.py000066400000000000000000000036021500564371300215330ustar00rootroot00000000000000"""Some code in this module was adapted from https://github.com/encode/starlette/blob/master/starlette/requests.py and https://github.com/encode/starlette/blob/master/starlette/websockets.py. Copyright © 2018, [Encode OSS Ltd](https://www.encode.io/). All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ from litestar.connection.base import ASGIConnection from litestar.connection.request import Request from litestar.connection.websocket import WebSocket __all__ = ("ASGIConnection", "Request", "WebSocket") litestar-2.16.0/litestar/connection/base.py000066400000000000000000000264531500564371300207170ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast from litestar._parsers import parse_cookie_string, parse_query_string from litestar.datastructures.headers import Headers from litestar.datastructures.multi_dicts import MultiDict from litestar.datastructures.state import State from litestar.datastructures.url import URL, Address, make_absolute_url from litestar.exceptions import ImproperlyConfiguredException from litestar.types.empty import Empty from litestar.utils.empty import value_or_default from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from typing import NoReturn from litestar.app import Litestar from litestar.types import DataContainerType, EmptyType from litestar.types.asgi_types import Message, Receive, Scope, Send from litestar.types.protocols import Logger __all__ = ("ASGIConnection", "empty_receive", "empty_send") UserT = TypeVar("UserT") AuthT = TypeVar("AuthT") HandlerT = TypeVar("HandlerT") StateT = TypeVar("StateT", bound=State) async def empty_receive() -> NoReturn: # pragma: no cover """Raise a ``RuntimeError``. Serves as a placeholder ``send`` function. Raises: RuntimeError """ raise RuntimeError() async def empty_send(_: Message) -> NoReturn: # pragma: no cover """Raise a ``RuntimeError``. Serves as a placeholder ``send`` function. Args: _: An ASGI message Raises: RuntimeError """ raise RuntimeError() class ASGIConnection(Generic[HandlerT, UserT, AuthT, StateT]): """The base ASGI connection container.""" __slots__ = ( "_base_url", "_connection_state", "_cookies", "_parsed_query", "_server_extensions", "_url", "receive", "scope", "send", ) scope: Scope """The ASGI scope attached to the connection.""" receive: Receive """The ASGI receive function.""" send: Send """The ASGI send function.""" def __init__(self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send) -> None: """Initialize ``ASGIConnection``. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. """ self.scope = scope self.receive = receive self.send = send self._connection_state = ScopeState.from_scope(scope) self._base_url: URL | EmptyType = Empty self._url: URL | EmptyType = Empty self._parsed_query: tuple[tuple[str, str], ...] | EmptyType = Empty self._cookies: dict[str, str] | EmptyType = Empty self._server_extensions = scope.get("extensions") or {} # extensions may be None @property def app(self) -> Litestar: """Return the ``app`` for this connection. Returns: The :class:`Litestar ` application instance """ return self.scope["litestar_app"] @property def route_handler(self) -> HandlerT: """Return the ``route_handler`` for this connection. Returns: The target route handler instance. """ return cast("HandlerT", self.scope["route_handler"]) @property def state(self) -> StateT: """Return the ``State`` of this connection. Returns: A State instance constructed from the scope["state"] value. """ return cast("StateT", State(self.scope.get("state"))) @property def url(self) -> URL: """Return the URL of this connection's ``Scope``. Returns: A URL instance constructed from the request's scope. """ if self._url is Empty: if (url := self._connection_state.url) is not Empty: self._url = url else: self._connection_state.url = self._url = URL.from_scope(self.scope) return self._url @property def base_url(self) -> URL: """Return the base URL of this connection's ``Scope``. Returns: A URL instance constructed from the request's scope, representing only the base part (host + domain + prefix) of the request. """ if self._base_url is Empty: if (base_url := self._connection_state.base_url) is not Empty: self._base_url = base_url else: scope = cast( "Scope", { **self.scope, "path": "/", "query_string": b"", "root_path": self.scope.get("app_root_path") or self.scope.get("root_path", ""), }, ) self._connection_state.base_url = self._base_url = URL.from_scope(scope) return self._base_url @property def headers(self) -> Headers: """Return the headers of this connection's ``Scope``. Returns: A Headers instance with the request's scope["headers"] value. """ return Headers.from_scope(self.scope) @property def query_params(self) -> MultiDict[Any]: """Return the query parameters of this connection's ``Scope``. Returns: A normalized dict of query parameters. Multiple values for the same key are returned as a list. """ if self._parsed_query is Empty: if (parsed_query := self._connection_state.parsed_query) is not Empty: self._parsed_query = parsed_query else: self._connection_state.parsed_query = self._parsed_query = parse_query_string( self.scope.get("query_string", b"") ) return MultiDict(self._parsed_query) @property def path_params(self) -> dict[str, Any]: """Return the ``path_params`` of this connection's ``Scope``. Returns: A string keyed dictionary of path parameter values. """ return self.scope["path_params"] @property def cookies(self) -> dict[str, str]: """Return the ``cookies`` of this connection's ``Scope``. Returns: Returns any cookies stored in the header as a parsed dictionary. """ if self._cookies is Empty: if (cookies := self._connection_state.cookies) is not Empty: self._cookies = cookies else: self._connection_state.cookies = self._cookies = ( parse_cookie_string(cookie_header) if (cookie_header := self.headers.get("cookie")) else {} ) return self._cookies @property def client(self) -> Address | None: """Return the ``client`` data of this connection's ``Scope``. Returns: A two tuple of the host name and port number. """ client = self.scope.get("client") return Address(*client) if client else None @property def auth(self) -> AuthT: """Return the ``auth`` data of this connection's ``Scope``. Raises: ImproperlyConfiguredException: If ``auth`` is not set in scope via an ``AuthMiddleware``, raises an exception Returns: A type correlating to the generic variable Auth. """ if "auth" not in self.scope: raise ImproperlyConfiguredException("'auth' is not defined in scope, install an AuthMiddleware to set it") return cast("AuthT", self.scope["auth"]) @property def user(self) -> UserT: """Return the ``user`` data of this connection's ``Scope``. Raises: ImproperlyConfiguredException: If ``user`` is not set in scope via an ``AuthMiddleware``, raises an exception Returns: A type correlating to the generic variable User. """ if "user" not in self.scope: raise ImproperlyConfiguredException("'user' is not defined in scope, install an AuthMiddleware to set it") return cast("UserT", self.scope["user"]) @property def session(self) -> dict[str, Any]: """Return the session for this connection if a session was previously set in the ``Scope`` Returns: A dictionary representing the session value - if existing. Raises: ImproperlyConfiguredException: if session is not set in scope. """ if "session" not in self.scope: raise ImproperlyConfiguredException( "'session' is not defined in scope, install a SessionMiddleware to set it" ) return cast("dict[str, Any]", self.scope["session"]) @property def logger(self) -> Logger: """Return the ``Logger`` instance for this connection. Returns: A ``Logger`` instance. Raises: ImproperlyConfiguredException: if ``log_config`` has not been passed to the Litestar constructor. """ return self.app.get_logger() def set_session(self, value: dict[str, Any] | DataContainerType | EmptyType) -> None: """Set the session in the connection's ``Scope``. If the :class:`SessionMiddleware <.middleware.session.base.SessionMiddleware>` is enabled, the session will be added to the response as a cookie header. Args: value: Dictionary or pydantic model instance for the session data. Returns: None """ self.scope["session"] = value def clear_session(self) -> None: """Remove the session from the connection's ``Scope``. If the :class:`Litestar SessionMiddleware <.middleware.session.base.SessionMiddleware>` is enabled, this will cause the session data to be cleared. Returns: None. """ self.scope["session"] = Empty self._connection_state.session_id = Empty def get_session_id(self) -> str | None: return value_or_default(value=self._connection_state.session_id, default=None) def url_for(self, name: str, **path_parameters: Any) -> str: """Return the url for a given route handler name. Args: name: The ``name`` of the request route handler. **path_parameters: Values for path parameters in the route Raises: NoRouteMatchFoundException: If route with ``name`` does not exist, path parameters are missing or have a wrong type. Returns: A string representing the absolute url of the route handler. """ litestar_instance = self.scope["litestar_app"] url_path = litestar_instance.route_reverse(name, **path_parameters) return make_absolute_url(url_path, self.base_url) def url_for_static_asset(self, name: str, file_path: str) -> str: """Receives a static files handler name, an asset file path and returns resolved absolute url to the asset. Args: name: A static handler unique name. file_path: a string containing path to an asset. Raises: NoRouteMatchFoundException: If static files handler with ``name`` does not exist. Returns: A string representing absolute url to the asset. """ litestar_instance = self.scope["litestar_app"] url_path = litestar_instance.url_for_static_asset(name, file_path) return make_absolute_url(url_path, self.base_url) litestar-2.16.0/litestar/connection/request.py000066400000000000000000000304341500564371300214670ustar00rootroot00000000000000from __future__ import annotations import math import warnings from typing import TYPE_CHECKING, Any, AsyncGenerator, Generic, cast from litestar._multipart import parse_content_header, parse_multipart_form from litestar._parsers import parse_url_encoded_form_data from litestar.connection.base import ( ASGIConnection, AuthT, StateT, UserT, empty_receive, empty_send, ) from litestar.datastructures.headers import Accept from litestar.datastructures.multi_dicts import FormMultiDict from litestar.enums import ASGIExtension, RequestEncodingType from litestar.exceptions import ( ClientException, InternalServerException, LitestarException, LitestarWarning, ) from litestar.exceptions.http_exceptions import RequestEntityTooLarge from litestar.serialization import decode_json, decode_msgpack from litestar.types import Empty, HTTPReceiveMessage __all__ = ("Request",) if TYPE_CHECKING: from litestar.handlers.http_handlers import HTTPRouteHandler # noqa: F401 from litestar.types.asgi_types import HTTPScope, Method, Receive, Scope, Send from litestar.types.empty import EmptyType SERVER_PUSH_HEADERS = { "accept", "accept-encoding", "accept-language", "cache-control", "user-agent", } class Request(Generic[UserT, AuthT, StateT], ASGIConnection["HTTPRouteHandler", UserT, AuthT, StateT]): """The Litestar Request class.""" __slots__ = ( "_accept", "_body", "_content_length", "_content_type", "_form", "_json", "_msgpack", "is_connected", "supports_push_promise", ) scope: HTTPScope # pyright: ignore """The ASGI scope attached to the connection.""" receive: Receive """The ASGI receive function.""" send: Send """The ASGI send function.""" def __init__(self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send) -> None: """Initialize ``Request``. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. """ super().__init__(scope, receive, send) self.is_connected: bool = True self._body: bytes | EmptyType = self._connection_state.body self._form: FormMultiDict | EmptyType = Empty self._json: Any = Empty self._msgpack: Any = Empty self._content_type: tuple[str, dict[str, str]] | EmptyType = Empty self._accept: Accept | EmptyType = Empty self._content_length: int | None | EmptyType = Empty self.supports_push_promise = ASGIExtension.SERVER_PUSH in self._server_extensions @property def method(self) -> Method: """Return the request method. Returns: The request :class:`Method ` """ return self.scope["method"] @property def content_type(self) -> tuple[str, dict[str, str]]: """Parse the request's 'Content-Type' header, returning the header value and any options as a dictionary. Returns: A tuple with the parsed value and a dictionary containing any options send in it. """ if self._content_type is Empty: if (content_type := self._connection_state.content_type) is not Empty: self._content_type = content_type else: self._content_type = self._connection_state.content_type = parse_content_header( self.headers.get("Content-Type", "") ) return self._content_type @property def accept(self) -> Accept: """Parse the request's 'Accept' header, returning an :class:`Accept ` instance. Returns: An :class:`Accept ` instance, representing the list of acceptable media types. """ if self._accept is Empty: if (accept := self._connection_state.accept) is not Empty: self._accept = accept else: self._accept = self._connection_state.accept = Accept(self.headers.get("Accept", "*/*")) return self._accept async def json(self) -> Any: """Retrieve the json request body from the request. Returns: An arbitrary value """ if self._json is Empty: if (json_ := self._connection_state.json) is not Empty: self._json = json_ else: body = await self.body() self._json = self._connection_state.json = decode_json( body or b"null", type_decoders=self.route_handler.resolve_type_decoders() ) return self._json async def msgpack(self) -> Any: """Retrieve the MessagePack request body from the request. Returns: An arbitrary value """ if self._msgpack is Empty: if (msgpack := self._connection_state.msgpack) is not Empty: self._msgpack = msgpack else: body = await self.body() self._msgpack = self._connection_state.msgpack = decode_msgpack( body or b"\xc0", type_decoders=self.route_handler.resolve_type_decoders() ) return self._msgpack @property def content_length(self) -> int | None: cached_content_length = self._content_length if cached_content_length is not Empty: return cached_content_length content_length_header = self.headers.get("content-length") try: content_length = self._content_length = ( int(content_length_header) if content_length_header is not None else None ) except ValueError: raise ClientException(f"Invalid content-length: {content_length_header!r}") from None return content_length async def stream(self) -> AsyncGenerator[bytes, None]: """Return an async generator that streams chunks of bytes. Returns: An async generator. Raises: RuntimeError: if the stream is already consumed """ if self._body is Empty: if not self.is_connected: raise InternalServerException("stream consumed") announced_content_length = self.content_length # setting this to 'math.inf' as a micro-optimisation; Comparing against a # float is slightly faster than checking if a value is 'None' and then # comparing it to an int. since we expect a limit to be set most of the # time, this is a bit more efficient max_content_length = self.route_handler.resolve_request_max_body_size() or math.inf # if the 'content-length' header is set, and exceeds the limit, we can bail # out early before reading anything if announced_content_length is not None and announced_content_length > max_content_length: raise RequestEntityTooLarge total_bytes_streamed: int = 0 while event := cast("HTTPReceiveMessage", await self.receive()): if event["type"] == "http.request": body = event["body"] if body: total_bytes_streamed += len(body) # if a 'content-length' header was set, check if we have # received more bytes than specified. in most cases this should # be caught before it hits the application layer and an ASGI # server (e.g. uvicorn) will not allow this, but since it's not # forbidden according to the HTTP or ASGI spec, we err on the # side of caution and still perform this check. # # uvicorn documented behaviour for this case: # https://github.com/encode/uvicorn/blob/fe3910083e3990695bc19c2ef671dd447262ae18/docs/server-behavior.md?plain=1#L11 if announced_content_length: if total_bytes_streamed > announced_content_length: raise ClientException("Malformed request") # we don't have a 'content-length' header, likely a chunked # transfer. we don't really care and simply check if we have # received more bytes than allowed elif total_bytes_streamed > max_content_length: raise RequestEntityTooLarge yield body if not event.get("more_body", False): break if event["type"] == "http.disconnect": raise InternalServerException("client disconnected prematurely") self.is_connected = False yield b"" else: yield self._body yield b"" return async def body(self) -> bytes: """Return the body of the request. Returns: A byte-string representing the body of the request. """ if self._body is Empty: if (body := self._connection_state.body) is not Empty: self._body = body else: self._body = self._connection_state.body = b"".join([c async for c in self.stream()]) return self._body async def form(self) -> FormMultiDict: """Retrieve form data from the request. If the request is either a 'multipart/form-data' or an 'application/x-www-form- urlencoded', return a FormMultiDict instance populated with the values sent in the request, otherwise, an empty instance. Returns: A FormMultiDict instance """ if self._form is Empty: if (form_data := self._connection_state.form) is Empty: content_type, options = self.content_type if content_type == RequestEncodingType.MULTI_PART: form_data = await parse_multipart_form( stream=self.stream(), boundary=options.get("boundary", "").encode(), multipart_form_part_limit=self.app.multipart_form_part_limit, ) elif content_type == RequestEncodingType.URL_ENCODED: form_data = parse_url_encoded_form_data( # type: ignore[assignment] await self.body(), ) else: form_data = {} self._connection_state.form = form_data # pyright: ignore self._form = FormMultiDict.from_form_data(cast("dict[str, Any]", form_data)) return self._form async def send_push_promise(self, path: str, raise_if_unavailable: bool = False) -> None: """Send a push promise. This method requires the `http.response.push` extension to be sent from the ASGI server. Args: path: Path to send the promise to. raise_if_unavailable: Raise an exception if server push is not supported by the server Returns: None """ if not self.supports_push_promise: if raise_if_unavailable: raise LitestarException("Attempted to send a push promise but the server does not support it") warnings.warn( "Attempted to send a push promise but the server does not support it. In a future version, this will " "raise an exception. To enable this behaviour in the current version, set raise_if_unavailable=True. " "To prevent this behaviour, make sure that the server you are using supports the 'http.response.push' " "ASGI extension, or check this dynamically via " ":attr:`~litestar.connection.Request.supports_push_promise`", stacklevel=2, category=LitestarWarning, ) return raw_headers = [ (header_name.encode("latin-1"), value.encode("latin-1")) for header_name in (self.headers.keys() & SERVER_PUSH_HEADERS) for value in self.headers.getall(header_name, []) ] await self.send({"type": "http.response.push", "path": path, "headers": raw_headers}) litestar-2.16.0/litestar/connection/websocket.py000066400000000000000000000261031500564371300217630ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, AsyncGenerator, Generic, Literal, cast, overload from litestar.connection.base import ( ASGIConnection, AuthT, StateT, UserT, empty_receive, empty_send, ) from litestar.datastructures.headers import Headers from litestar.exceptions import WebSocketDisconnect from litestar.serialization import decode_json, decode_msgpack, default_serializer, encode_json, encode_msgpack from litestar.status_codes import WS_1000_NORMAL_CLOSURE __all__ = ("WebSocket",) if TYPE_CHECKING: from litestar.handlers.websocket_handlers import WebsocketRouteHandler # noqa: F401 from litestar.types import Message, Serializer, WebSocketScope from litestar.types.asgi_types import ( Receive, ReceiveMessage, Scope, Send, WebSocketAcceptEvent, WebSocketCloseEvent, WebSocketDisconnectEvent, WebSocketMode, WebSocketReceiveEvent, WebSocketSendEvent, ) DISCONNECT_MESSAGE = "connection is disconnected" class WebSocket(Generic[UserT, AuthT, StateT], ASGIConnection["WebsocketRouteHandler", UserT, AuthT, StateT]): """The Litestar WebSocket class.""" __slots__ = ("connection_state",) scope: WebSocketScope # pyright: ignore """The ASGI scope attached to the connection.""" receive: Receive """The ASGI receive function.""" send: Send """The ASGI send function.""" def __init__(self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send) -> None: """Initialize ``WebSocket``. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. """ super().__init__(scope, self.receive_wrapper(receive), self.send_wrapper(send)) self.connection_state: Literal["init", "connect", "receive", "disconnect"] = "init" def receive_wrapper(self, receive: Receive) -> Receive: """Wrap ``receive`` to set 'self.connection_state' and validate events. Args: receive: The ASGI receive function. Returns: An ASGI receive function. """ async def wrapped_receive() -> ReceiveMessage: if self.connection_state == "disconnect": raise WebSocketDisconnect(detail=DISCONNECT_MESSAGE) message = await receive() if message["type"] == "websocket.connect": self.connection_state = "connect" elif message["type"] == "websocket.receive": self.connection_state = "receive" else: self.connection_state = "disconnect" return message return wrapped_receive def send_wrapper(self, send: Send) -> Send: """Wrap ``send`` to ensure that state is not disconnected. Args: send: The ASGI send function. Returns: An ASGI send function. """ async def wrapped_send(message: Message) -> None: if self.connection_state == "disconnect": raise WebSocketDisconnect(detail=DISCONNECT_MESSAGE) # pragma: no cover await send(message) return wrapped_send async def accept( self, subprotocols: str | None = None, headers: Headers | dict[str, Any] | list[tuple[bytes, bytes]] | None = None, ) -> None: """Accept the incoming connection. This method should be called before receiving data. Args: subprotocols: Websocket sub-protocol to use. headers: Headers to set on the data sent. Returns: None """ if self.connection_state == "init": await self.receive() _headers: list[tuple[bytes, bytes]] = headers if isinstance(headers, list) else [] if isinstance(headers, dict): _headers = Headers(headers=headers).to_header_list() if isinstance(headers, Headers): _headers = headers.to_header_list() event: WebSocketAcceptEvent = { "type": "websocket.accept", "subprotocol": subprotocols, "headers": _headers, } await self.send(event) async def close(self, code: int = WS_1000_NORMAL_CLOSURE, reason: str | None = None) -> None: """Send an 'websocket.close' event. Args: code: Status code. reason: Reason for closing the connection Returns: None """ event: WebSocketCloseEvent = {"type": "websocket.close", "code": code, "reason": reason or ""} await self.send(event) @overload async def receive_data(self, mode: Literal["text"]) -> str: ... @overload async def receive_data(self, mode: Literal["binary"]) -> bytes: ... async def receive_data(self, mode: WebSocketMode) -> str | bytes: """Receive an 'websocket.receive' event and returns the data stored on it. Args: mode: The respective event key to use. Returns: The event's data. """ if self.connection_state == "init": await self.accept() event = cast("WebSocketReceiveEvent | WebSocketDisconnectEvent", await self.receive()) if event["type"] == "websocket.disconnect": raise WebSocketDisconnect(detail="disconnect event", code=event["code"]) return event.get("text") or "" if mode == "text" else event.get("bytes") or b"" @overload def iter_data(self, mode: Literal["text"]) -> AsyncGenerator[str, None]: ... @overload def iter_data(self, mode: Literal["binary"]) -> AsyncGenerator[bytes, None]: ... async def iter_data(self, mode: WebSocketMode = "text") -> AsyncGenerator[str | bytes, None]: """Continuously receive data and yield it Args: mode: Socket mode to use. Either ``text`` or ``binary`` """ try: while True: yield await self.receive_data(mode) except WebSocketDisconnect: pass async def receive_text(self) -> str: """Receive data as text. Returns: A string. """ return await self.receive_data(mode="text") async def receive_bytes(self) -> bytes: """Receive data as bytes. Returns: A byte-string. """ return await self.receive_data(mode="binary") async def receive_json(self, mode: WebSocketMode = "text") -> Any: """Receive data and decode it as JSON. Args: mode: Either ``text`` or ``binary``. Returns: An arbitrary value """ data = await self.receive_data(mode=mode) return decode_json(value=data, type_decoders=self.route_handler.resolve_type_decoders()) async def receive_msgpack(self) -> Any: """Receive data and decode it as MessagePack. Note that since MessagePack is a binary format, this method will always receive data in ``binary`` mode. Returns: An arbitrary value """ data = await self.receive_data(mode="binary") return decode_msgpack(value=data, type_decoders=self.route_handler.resolve_type_decoders()) async def iter_json(self, mode: WebSocketMode = "text") -> AsyncGenerator[Any, None]: """Continuously receive data and yield it, decoding it as JSON in the process. Args: mode: Socket mode to use. Either ``text`` or ``binary`` """ async for data in self.iter_data(mode): yield decode_json(value=data, type_decoders=self.route_handler.resolve_type_decoders()) async def iter_msgpack(self) -> AsyncGenerator[Any, None]: """Continuously receive data and yield it, decoding it as MessagePack in the process. Note that since MessagePack is a binary format, this method will always receive data in ``binary`` mode. """ async for data in self.iter_data(mode="binary"): yield decode_msgpack(value=data, type_decoders=self.route_handler.resolve_type_decoders()) async def send_data(self, data: str | bytes, mode: WebSocketMode = "text", encoding: str = "utf-8") -> None: """Send a 'websocket.send' event. Args: data: Data to send. mode: The respective event key to use. encoding: Encoding to use when converting bytes / str. Returns: None """ if self.connection_state == "init": # pragma: no cover await self.accept() event: WebSocketSendEvent = {"type": "websocket.send", "bytes": None, "text": None} if mode == "binary": event["bytes"] = data if isinstance(data, bytes) else data.encode(encoding) else: event["text"] = data if isinstance(data, str) else data.decode(encoding) await self.send(event) @overload async def send_text(self, data: bytes, encoding: str = "utf-8") -> None: ... @overload async def send_text(self, data: str) -> None: ... async def send_text(self, data: str | bytes, encoding: str = "utf-8") -> None: """Send data using the ``text`` key of the send event. Args: data: Data to send encoding: Encoding to use for binary data. Returns: None """ await self.send_data(data=data, encoding=encoding) @overload async def send_bytes(self, data: bytes) -> None: ... @overload async def send_bytes(self, data: str, encoding: str = "utf-8") -> None: ... async def send_bytes(self, data: str | bytes, encoding: str = "utf-8") -> None: """Send data using the ``bytes`` key of the send event. Args: data: Data to send encoding: Encoding to use for binary data. Returns: None """ await self.send_data(data=data, mode="binary", encoding=encoding) async def send_json( self, data: Any, mode: WebSocketMode = "text", encoding: str = "utf-8", serializer: Serializer = default_serializer, ) -> None: """Send data as JSON. Args: data: A value to serialize. mode: Either ``text`` or ``binary``. encoding: Encoding to use for binary data. serializer: A serializer function. Returns: None """ await self.send_data(data=encode_json(data, serializer), mode=mode, encoding=encoding) async def send_msgpack( self, data: Any, encoding: str = "utf-8", serializer: Serializer = default_serializer, ) -> None: """Send data as MessagePack. Note that since MessagePack is a binary format, this method will always send data in ``binary`` mode. Args: data: A value to serialize. encoding: Encoding to use for binary data. serializer: A serializer function. Returns: None """ await self.send_data(data=encode_msgpack(data, serializer), mode="binary", encoding=encoding) litestar-2.16.0/litestar/constants.py000066400000000000000000000051131500564371300176500ustar00rootroot00000000000000from __future__ import annotations from dataclasses import MISSING from inspect import Signature from typing import Any, Final from uuid import uuid4 from msgspec import UnsetType from litestar.enums import MediaType from litestar.types import Empty from litestar.utils.deprecation import warn_deprecation DEFAULT_ALLOWED_CORS_HEADERS: Final = {"Accept", "Accept-Language", "Content-Language", "Content-Type"} DEFAULT_CHUNK_SIZE: Final = 1024 * 128 # 128KB HTTP_DISCONNECT: Final = "http.disconnect" HTTP_RESPONSE_BODY: Final = "http.response.body" HTTP_RESPONSE_START: Final = "http.response.start" ONE_MEGABYTE: Final = 1024 * 1024 OPENAPI_JSON_HANDLER_NAME: Final = f"{uuid4().hex}_litestar_openapi_json" OPENAPI_NOT_INITIALIZED: Final = "Litestar has not been instantiated with OpenAPIConfig" REDIRECT_STATUS_CODES: Final = {301, 302, 303, 307, 308} REDIRECT_ALLOWED_MEDIA_TYPES: Final = {MediaType.TEXT, MediaType.HTML, MediaType.JSON} RESERVED_KWARGS: Final = {"state", "headers", "cookies", "request", "socket", "data", "query", "scope", "body"} SKIP_VALIDATION_NAMES: Final = {"request", "socket", "scope", "receive", "send"} UNDEFINED_SENTINELS: Final = {Signature.empty, Empty, Ellipsis, MISSING, UnsetType} WEBSOCKET_CLOSE: Final = "websocket.close" WEBSOCKET_DISCONNECT: Final = "websocket.disconnect" # deprecated constants _SCOPE_STATE_CSRF_TOKEN_KEY = "csrf_token" # noqa: S105 # possible hardcoded password _SCOPE_STATE_DEPENDENCY_CACHE: Final = "dependency_cache" _SCOPE_STATE_NAMESPACE: Final = "__litestar__" _SCOPE_STATE_RESPONSE_COMPRESSED: Final = "response_compressed" _SCOPE_STATE_DO_CACHE: Final = "do_cache" _SCOPE_STATE_IS_CACHED: Final = "is_cached" _deprecated_names = { "SCOPE_STATE_CSRF_TOKEN_KEY": _SCOPE_STATE_CSRF_TOKEN_KEY, "SCOPE_STATE_DEPENDENCY_CACHE": _SCOPE_STATE_DEPENDENCY_CACHE, "SCOPE_STATE_NAMESPACE": _SCOPE_STATE_NAMESPACE, "SCOPE_STATE_RESPONSE_COMPRESSED": _SCOPE_STATE_RESPONSE_COMPRESSED, "SCOPE_STATE_DO_CACHE": _SCOPE_STATE_DO_CACHE, "SCOPE_STATE_IS_CACHED": _SCOPE_STATE_IS_CACHED, } def __getattr__(name: str) -> Any: if name in _deprecated_names: warn_deprecation( deprecated_name=f"litestar.constants.{name}", version="2.4", kind="import", removal_in="3.0", info=f"'{name}' from 'litestar.constants' is deprecated and will be removed in 3.0. " "Direct access to Litestar scope state is not recommended.", ) return globals()["_deprecated_names"][name] raise AttributeError(f"module {__name__} has no attribute {name}") # pragma: no cover litestar-2.16.0/litestar/contrib/000077500000000000000000000000001500564371300167225ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/__init__.py000066400000000000000000000000001500564371300210210ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/attrs/000077500000000000000000000000001500564371300200575ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/attrs/__init__.py000066400000000000000000000017131500564371300221720ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING, Any from litestar.utils import warn_deprecation __all__ = ("AttrsSchemaPlugin", "is_attrs_class") def __getattr__(attr_name: str) -> object: if attr_name in __all__: from litestar.plugins.attrs import AttrsSchemaPlugin, is_attrs_class warn_deprecation( deprecated_name=f"litestar.contrib.attrs.{attr_name}", version="2.13.0", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.attrs' is deprecated, please " f"import it from 'litestar.plugins.attrs' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from litestar.plugins.attrs import AttrsSchemaPlugin, is_attrs_class litestar-2.16.0/litestar/contrib/attrs/attrs_schema_plugin.py000066400000000000000000000017631500564371300244730ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING, Any from litestar.utils import warn_deprecation __all__ = ("AttrsSchemaPlugin", "is_attrs_class") def __getattr__(attr_name: str) -> object: if attr_name in __all__: from litestar.plugins.attrs import AttrsSchemaPlugin, is_attrs_class warn_deprecation( deprecated_name=f"litestar.contrib.attrs.attrs_schema_plugin.{attr_name}", version="2.13.0", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.attrs.attrs_schema_plugin' is deprecated, please " f"import it from 'litestar.plugins.attrs' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from litestar.plugins.attrs import AttrsSchemaPlugin, is_attrs_class litestar-2.16.0/litestar/contrib/htmx/000077500000000000000000000000001500564371300177025ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/htmx/__init__.py000066400000000000000000000000001500564371300220010ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/htmx/_utils.py000066400000000000000000000026141500564371300215560ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation if TYPE_CHECKING: from litestar_htmx._utils import ( # noqa: TC004 HTMXHeaders, get_headers, get_location_headers, get_push_url_header, get_redirect_header, get_refresh_header, get_replace_url_header, get_reswap_header, get_retarget_header, get_trigger_event_headers, ) __all__ = ( "HTMXHeaders", "get_headers", "get_location_headers", "get_push_url_header", "get_redirect_header", "get_refresh_header", "get_replace_url_header", "get_reswap_header", "get_retarget_header", "get_trigger_event_headers", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: from litestar_htmx import _utils as utils module = "litestar.plugins.htmx._utils" value = globals()[attr_name] = getattr(utils, attr_name) warn_deprecation( deprecated_name=f"litestar.contrib.htmx._utils.{attr_name}", version="2.13", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.htmx._utils' is deprecated, please import it from '{module}' instead", ) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") litestar-2.16.0/litestar/contrib/htmx/request.py000066400000000000000000000016331500564371300217470ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation if TYPE_CHECKING: from litestar_htmx import ( # noqa: TC004 HTMXDetails, HTMXRequest, ) __all__ = ("HTMXDetails", "HTMXRequest") def __getattr__(attr_name: str) -> object: if attr_name in __all__: import litestar_htmx module = "litestar.plugins.htmx" value = globals()[attr_name] = getattr(litestar_htmx, attr_name) warn_deprecation( deprecated_name=f"litestar.contrib.htmx.request.{attr_name}", version="2.13", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.htmx.request' is deprecated, please import it from '{module}' instead", ) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") litestar-2.16.0/litestar/contrib/htmx/response.py000066400000000000000000000023361500564371300221160ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation if TYPE_CHECKING: from litestar_htmx import ( # noqa: TC004 ClientRedirect, ClientRefresh, HTMXTemplate, HXLocation, HXStopPolling, PushUrl, ReplaceUrl, Reswap, Retarget, TriggerEvent, ) __all__ = ( "ClientRedirect", "ClientRefresh", "HTMXTemplate", "HXLocation", "HXStopPolling", "PushUrl", "ReplaceUrl", "Reswap", "Retarget", "TriggerEvent", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: import litestar_htmx module = "litestar.plugins.htmx" value = globals()[attr_name] = getattr(litestar_htmx, attr_name) warn_deprecation( deprecated_name=f"litestar.contrib.htmx.response.{attr_name}", version="2.13", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.htmx.response' is deprecated, please import it from '{module}' instead", ) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") litestar-2.16.0/litestar/contrib/htmx/types.py000066400000000000000000000016721500564371300214260ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation if TYPE_CHECKING: from litestar_htmx import HtmxHeaderType, LocationType, TriggerEventType # noqa: TC004 __all__ = ( "HtmxHeaderType", "LocationType", "TriggerEventType", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: import litestar_htmx module = "litestar.plugins.htmx" value = globals()[attr_name] = getattr(litestar_htmx, attr_name) warn_deprecation( deprecated_name=f"litestar.contrib.htmx.types.{attr_name}", version="2.13", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.htmx.types' is deprecated, please import it from '{module}' instead", ) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") litestar-2.16.0/litestar/contrib/jinja.py000066400000000000000000000075511500564371300203770ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Mapping, TypeVar from typing_extensions import ParamSpec from litestar.exceptions import ImproperlyConfiguredException, MissingDependencyException, TemplateNotFoundException from litestar.template.base import ( TemplateCallableType, TemplateEngineProtocol, csrf_token, url_for, url_for_static_asset, ) try: from jinja2 import Environment, FileSystemLoader, pass_context from jinja2 import TemplateNotFound as JinjaTemplateNotFound except ImportError as e: raise MissingDependencyException("jinja2", extra="jinja") from e if TYPE_CHECKING: from pathlib import Path from jinja2 import Template as JinjaTemplate __all__ = ("JinjaTemplateEngine",) P = ParamSpec("P") T = TypeVar("T") class JinjaTemplateEngine(TemplateEngineProtocol["JinjaTemplate", Mapping[str, Any]]): """The engine instance.""" def __init__( self, directory: Path | list[Path] | None = None, engine_instance: Environment | None = None, ) -> None: """Jinja-based TemplateEngine. Args: directory: Direct path or list of directory paths from which to serve templates. engine_instance: A jinja Environment instance. """ if directory and engine_instance: raise ImproperlyConfiguredException("You must provide either a directory or a jinja2 Environment instance.") if directory: loader = FileSystemLoader(searchpath=directory) self.engine = Environment(loader=loader, autoescape=True) elif engine_instance: self.engine = engine_instance self.register_template_callable(key="url_for_static_asset", template_callable=url_for_static_asset) self.register_template_callable(key="csrf_token", template_callable=csrf_token) self.register_template_callable(key="url_for", template_callable=url_for) def get_template(self, template_name: str) -> JinjaTemplate: """Retrieve a template by matching its name (dotted path) with files in the directory or directories provided. Args: template_name: A dotted path Returns: JinjaTemplate instance Raises: TemplateNotFoundException: if no template is found. """ try: return self.engine.get_template(name=template_name) except JinjaTemplateNotFound as exc: raise TemplateNotFoundException(template_name=template_name) from exc def register_template_callable( self, key: str, template_callable: TemplateCallableType[Mapping[str, Any], P, T] ) -> None: """Register a callable on the template engine. Args: key: The callable key, i.e. the value to use inside the template to call the callable. template_callable: A callable to register. Returns: None """ self.engine.globals[key] = pass_context(template_callable) def render_string(self, template_string: str, context: Mapping[str, Any]) -> str: """Render a template from a string with the given context. Args: template_string: The template string to render. context: A dictionary of variables to pass to the template. Returns: The rendered template as a string. """ template = self.engine.from_string(template_string) return template.render(context) @classmethod def from_environment(cls, jinja_environment: Environment) -> JinjaTemplateEngine: """Create a JinjaTemplateEngine from an existing jinja Environment instance. Args: jinja_environment (jinja2.environment.Environment): A jinja Environment instance. Returns: JinjaTemplateEngine instance """ return cls(directory=None, engine_instance=jinja_environment) litestar-2.16.0/litestar/contrib/jwt/000077500000000000000000000000001500564371300175265ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/jwt/__init__.py000066400000000000000000000014501500564371300216370ustar00rootroot00000000000000from litestar.contrib.jwt.jwt_auth import ( BaseJWTAuth, JWTAuth, JWTCookieAuth, OAuth2Login, OAuth2PasswordBearerAuth, ) from litestar.contrib.jwt.jwt_token import Token from litestar.contrib.jwt.middleware import ( JWTAuthenticationMiddleware, JWTCookieAuthenticationMiddleware, ) from litestar.utils import warn_deprecation __all__ = ( "BaseJWTAuth", "JWTAuth", "JWTAuthenticationMiddleware", "JWTCookieAuth", "JWTCookieAuthenticationMiddleware", "OAuth2Login", "OAuth2PasswordBearerAuth", "Token", ) warn_deprecation( deprecated_name="litestar.contrib.jwt", version="2.3.2", kind="import", removal_in="3.0", info="importing from 'litestar.contrib.jwt' is deprecated, please import from 'litestar.security.jwt' instead", ) litestar-2.16.0/litestar/contrib/jwt/jwt_auth.py000066400000000000000000000003701500564371300217250ustar00rootroot00000000000000from __future__ import annotations from litestar.security.jwt.auth import BaseJWTAuth, JWTAuth, JWTCookieAuth, OAuth2Login, OAuth2PasswordBearerAuth __all__ = ("BaseJWTAuth", "JWTAuth", "JWTCookieAuth", "OAuth2Login", "OAuth2PasswordBearerAuth") litestar-2.16.0/litestar/contrib/jwt/jwt_token.py000066400000000000000000000001501500564371300221000ustar00rootroot00000000000000from __future__ import annotations from litestar.security.jwt.token import Token __all__ = ("Token",) litestar-2.16.0/litestar/contrib/jwt/middleware.py000066400000000000000000000003401500564371300222120ustar00rootroot00000000000000from __future__ import annotations from litestar.security.jwt.middleware import JWTAuthenticationMiddleware, JWTCookieAuthenticationMiddleware __all__ = ("JWTAuthenticationMiddleware", "JWTCookieAuthenticationMiddleware") litestar-2.16.0/litestar/contrib/mako.py000066400000000000000000000124261500564371300202300ustar00rootroot00000000000000from __future__ import annotations from functools import partial from typing import TYPE_CHECKING, Any, Mapping, TypeVar from typing_extensions import ParamSpec from litestar.exceptions import ImproperlyConfiguredException, MissingDependencyException, TemplateNotFoundException from litestar.template.base import ( TemplateCallableType, TemplateEngineProtocol, TemplateProtocol, csrf_token, url_for, url_for_static_asset, ) try: from mako.exceptions import TemplateLookupException as MakoTemplateNotFound # type: ignore[import-untyped] from mako.lookup import TemplateLookup # type: ignore[import-untyped] from mako.template import Template as _MakoTemplate # type: ignore[import-untyped] except ImportError as e: raise MissingDependencyException("mako") from e if TYPE_CHECKING: from pathlib import Path __all__ = ("MakoTemplate", "MakoTemplateEngine") P = ParamSpec("P") T = TypeVar("T") class MakoTemplate(TemplateProtocol): """Mako template, implementing ``TemplateProtocol``""" def __init__(self, template: _MakoTemplate, template_callables: list[tuple[str, TemplateCallableType]]) -> None: """Initialize a template. Args: template: Base ``MakoTemplate`` used by the underlying mako-engine template_callables: List of callables passed to the template """ super().__init__() self.template = template self.template_callables = template_callables def render(self, *args: Any, **kwargs: Any) -> str: """Render a template. Args: args: Positional arguments passed to the engines ``render`` function kwargs: Keyword arguments passed to the engines ``render`` function Returns: Rendered template as a string """ for callable_key, template_callable in self.template_callables: kwargs_copy = {**kwargs} kwargs[callable_key] = partial(template_callable, kwargs_copy) return str(self.template.render(*args, **kwargs)) class MakoTemplateEngine(TemplateEngineProtocol[MakoTemplate, Mapping[str, Any]]): """Mako-based TemplateEngine.""" def __init__(self, directory: Path | list[Path] | None = None, engine_instance: Any | None = None) -> None: """Initialize template engine. Args: directory: Direct path or list of directory paths from which to serve templates. engine_instance: A mako TemplateLookup instance. """ if directory and engine_instance: raise ImproperlyConfiguredException("You must provide either a directory or a mako TemplateLookup.") if directory: self.engine = TemplateLookup( directories=directory if isinstance(directory, (list, tuple)) else [directory], default_filters=["h"] ) elif engine_instance: self.engine = engine_instance self._template_callables: list[tuple[str, TemplateCallableType]] = [] self.register_template_callable(key="url_for_static_asset", template_callable=url_for_static_asset) self.register_template_callable(key="csrf_token", template_callable=csrf_token) self.register_template_callable(key="url_for", template_callable=url_for) def get_template(self, template_name: str) -> MakoTemplate: """Retrieve a template by matching its name (dotted path) with files in the directory or directories provided. Args: template_name: A dotted path Returns: MakoTemplate instance Raises: TemplateNotFoundException: if no template is found. """ try: return MakoTemplate( template=self.engine.get_template(template_name), template_callables=self._template_callables ) except MakoTemplateNotFound as exc: raise TemplateNotFoundException(template_name=template_name) from exc def register_template_callable( self, key: str, template_callable: TemplateCallableType[Mapping[str, Any], P, T] ) -> None: """Register a callable on the template engine. Args: key: The callable key, i.e. the value to use inside the template to call the callable. template_callable: A callable to register. Returns: None """ self._template_callables.append((key, template_callable)) def render_string(self, template_string: str, context: Mapping[str, Any]) -> str: # pyright: ignore """Render a template from a string with the given context. Args: template_string: The template string to render. context: A dictionary of variables to pass to the template. Returns: The rendered template as a string. """ template = _MakoTemplate(template_string) # noqa: S702 return template.render(**context) # type: ignore[no-any-return] @classmethod def from_template_lookup(cls, template_lookup: TemplateLookup) -> MakoTemplateEngine: """Create a template engine from an existing mako TemplateLookup instance. Args: template_lookup: A mako TemplateLookup instance. Returns: MakoTemplateEngine instance """ return cls(directory=None, engine_instance=template_lookup) litestar-2.16.0/litestar/contrib/minijinja.py000066400000000000000000000171421500564371300212510ustar00rootroot00000000000000from __future__ import annotations import functools from pathlib import Path from typing import TYPE_CHECKING, Any, Mapping, Protocol, TypeVar, cast from typing_extensions import ParamSpec from litestar.exceptions import ImproperlyConfiguredException, MissingDependencyException, TemplateNotFoundException from litestar.template.base import ( TemplateCallableType, TemplateEngineProtocol, TemplateProtocol, csrf_token, url_for, url_for_static_asset, ) from litestar.utils.deprecation import warn_deprecation try: from minijinja import Environment # type:ignore[import-untyped] from minijinja import TemplateError as MiniJinjaTemplateNotFound except ImportError as e: raise MissingDependencyException("minijinja") from e if TYPE_CHECKING: from typing import Callable C = TypeVar("C", bound="Callable") def pass_state(func: C) -> C: ... else: from minijinja import pass_state __all__ = ( "MiniJinjaTemplateEngine", "StateProtocol", ) P = ParamSpec("P") T = TypeVar("T") class StateProtocol(Protocol): auto_escape: str | None current_block: str | None env: Environment name: str def lookup(self, key: str) -> Any | None: ... def _transform_state(func: TemplateCallableType[Mapping[str, Any], P, T]) -> TemplateCallableType[StateProtocol, P, T]: """Transform a template callable to receive a ``StateProtocol`` instance as first argument. This is for wrapping callables like ``url_for()`` that receive a mapping as first argument so they can be used with minijinja which passes a ``StateProtocol`` instance as first argument. """ @functools.wraps(func) @pass_state def wrapped(state: StateProtocol, /, *args: P.args, **kwargs: P.kwargs) -> T: template_context = {"request": state.lookup("request"), "csrf_input": state.lookup("csrf_input")} return func(template_context, *args, **kwargs) return wrapped class MiniJinjaTemplate(TemplateProtocol): """Initialize a template. Args: template: Base ``MiniJinjaTemplate`` used by the underlying minijinja engine """ def __init__(self, engine: Environment, template_name: str) -> None: super().__init__() self.engine = engine self.template_name = template_name def render(self, *args: Any, **kwargs: Any) -> str: """Render a template. Args: args: Positional arguments passed to the engines ``render`` function kwargs: Keyword arguments passed to the engines ``render`` function Returns: Rendered template as a string """ try: return str(self.engine.render_template(self.template_name, *args, **kwargs)) except MiniJinjaTemplateNotFound as err: raise TemplateNotFoundException(template_name=self.template_name) from err class MiniJinjaTemplateEngine(TemplateEngineProtocol["MiniJinjaTemplate", StateProtocol]): """The engine instance.""" def __init__(self, directory: Path | list[Path] | None = None, engine_instance: Environment | None = None) -> None: """Minijinja based TemplateEngine. Args: directory: Direct path or list of directory paths from which to serve templates. engine_instance: A Minijinja Environment instance. """ if directory and engine_instance: raise ImproperlyConfiguredException( "You must provide either a directory or a minijinja Environment instance." ) if directory: def _loader(name: str) -> str: """Load a template from a directory. Args: name: The name of the template Returns: The template as a string Raises: TemplateNotFoundException: if no template is found. """ directories = directory if isinstance(directory, list) else [directory] for d in directories: template_path = Path(d) / name # pyright: ignore[reportGeneralTypeIssues] if template_path.exists(): return template_path.read_text() raise TemplateNotFoundException(template_name=name) self.engine = Environment(loader=_loader) elif engine_instance: self.engine = engine_instance else: raise ImproperlyConfiguredException( "You must provide either a directory or a minijinja Environment instance." ) self.register_template_callable("url_for", _transform_state(url_for)) self.register_template_callable("csrf_token", _transform_state(csrf_token)) self.register_template_callable("url_for_static_asset", _transform_state(url_for_static_asset)) def get_template(self, template_name: str) -> MiniJinjaTemplate: """Retrieve a template by matching its name (dotted path) with files in the directory or directories provided. Args: template_name: A dotted path Returns: MiniJinjaTemplate instance Raises: TemplateNotFoundException: if no template is found. """ return MiniJinjaTemplate(self.engine, template_name) def register_template_callable( self, key: str, template_callable: TemplateCallableType[StateProtocol, P, T], ) -> None: """Register a callable on the template engine. Args: key: The callable key, i.e. the value to use inside the template to call the callable. template_callable: A callable to register. Returns: None """ def is_decorated(func: Callable) -> bool: return hasattr(func, "__wrapped__") or func.__name__ not in globals() if not is_decorated(template_callable): template_callable = _transform_state(template_callable) # type: ignore[arg-type] # pragma: no cover self.engine.add_global(key, pass_state(template_callable)) def render_string(self, template_string: str, context: Mapping[str, Any]) -> str: """Render a template from a string with the given context. Args: template_string: The template string to render. context: A dictionary of variables to pass to the template. Returns: The rendered template as a string. """ return self.engine.render_str(template_string, **context) # type: ignore[no-any-return] @classmethod def from_environment(cls, minijinja_environment: Environment) -> MiniJinjaTemplateEngine: """Create a MiniJinjaTemplateEngine from an existing minijinja Environment instance. Args: minijinja_environment (Environment): A minijinja Environment instance. Returns: MiniJinjaTemplateEngine instance """ return cls(directory=None, engine_instance=minijinja_environment) @pass_state def _minijinja_from_state(func: Callable, state: StateProtocol, *args: Any, **kwargs: Any) -> str: # pragma: no cover template_context = {"request": state.lookup("request"), "csrf_input": state.lookup("csrf_input")} return cast(str, func(template_context, *args, **kwargs)) def __getattr__(name: str) -> Any: if name == "minijinja_from_state": warn_deprecation( "2.3.0", "minijinja_from_state", "import", removal_in="3.0.0", alternative="Use a callable that receives the minijinja State object as first argument.", ) return _minijinja_from_state raise AttributeError(f"module {__name__!r} has no attribute {name!r}") litestar-2.16.0/litestar/contrib/minijnja.py000066400000000000000000000006051500564371300210740ustar00rootroot00000000000000from __future__ import annotations from typing import Any from litestar.utils.deprecation import warn_deprecation from . import minijinja as _minijinja def __getattr__(name: str) -> Any: warn_deprecation( "2.3.0", "contrib.minijnja", "import", removal_in="3.0.0", alternative="contrib.minijinja", ) return getattr(_minijinja, name) litestar-2.16.0/litestar/contrib/opentelemetry/000077500000000000000000000000001500564371300216165ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/opentelemetry/__init__.py000066400000000000000000000004021500564371300237230ustar00rootroot00000000000000from .config import OpenTelemetryConfig from .middleware import OpenTelemetryInstrumentationMiddleware from .plugin import OpenTelemetryPlugin __all__ = ( "OpenTelemetryConfig", "OpenTelemetryInstrumentationMiddleware", "OpenTelemetryPlugin", ) litestar-2.16.0/litestar/contrib/opentelemetry/_utils.py000066400000000000000000000017111500564371300234670ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from litestar.exceptions import MissingDependencyException __all__ = ("get_route_details_from_scope",) try: import opentelemetry # noqa: F401 except ImportError as e: raise MissingDependencyException("opentelemetry") from e from opentelemetry.semconv.trace import SpanAttributes if TYPE_CHECKING: from litestar.types import Scope def get_route_details_from_scope(scope: Scope) -> tuple[str, dict[Any, str]]: """Retrieve the span name and attributes from the ASGI scope. Args: scope: The ASGI scope instance. Returns: A tuple of the span name and a dict of attrs. """ path = scope.get("path", "").strip() method = str(scope.get("method", "")).strip() if method and path: # http return f"{method} {path}", {SpanAttributes.HTTP_ROUTE: f"{method} {path}"} return path, {SpanAttributes.HTTP_ROUTE: path} # websocket litestar-2.16.0/litestar/contrib/opentelemetry/config.py000066400000000000000000000102131500564371300234320ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable from litestar.contrib.opentelemetry._utils import get_route_details_from_scope from litestar.contrib.opentelemetry.middleware import ( OpenTelemetryInstrumentationMiddleware, ) from litestar.exceptions import MissingDependencyException from litestar.middleware.base import DefineMiddleware __all__ = ("OpenTelemetryConfig",) try: import opentelemetry # noqa: F401 except ImportError as e: raise MissingDependencyException("opentelemetry") from e from opentelemetry.trace import Span, TracerProvider # pyright: ignore if TYPE_CHECKING: from opentelemetry.metrics import Meter, MeterProvider from litestar.types import Scope, Scopes OpenTelemetryHookHandler = Callable[[Span, dict], None] @dataclass class OpenTelemetryConfig: """Configuration class for the OpenTelemetry middleware. Consult the [OpenTelemetry ASGI documentation](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/asgi/asgi.html) for more info about the configuration options. """ scope_span_details_extractor: Callable[[Scope], tuple[str, dict[str, Any]]] = field( default=get_route_details_from_scope ) """Callback which should return a string and a tuple, representing the desired default span name and a dictionary with any additional span attributes to set. """ server_request_hook_handler: OpenTelemetryHookHandler | None = field(default=None) """Optional callback which is called with the server span and ASGI scope object for every incoming request.""" client_request_hook_handler: OpenTelemetryHookHandler | None = field(default=None) """Optional callback which is called with the internal span and an ASGI scope which is sent as a dictionary for when the method receive is called. """ client_response_hook_handler: OpenTelemetryHookHandler | None = field(default=None) """Optional callback which is called with the internal span and an ASGI event which is sent as a dictionary for when the method send is called. """ meter_provider: MeterProvider | None = field(default=None) """Optional meter provider to use. If omitted the current globally configured one is used. """ tracer_provider: TracerProvider | None = field(default=None) """Optional tracer provider to use. If omitted the current globally configured one is used. """ meter: Meter | None = field(default=None) """Optional meter to use. If omitted the provided meter provider or the global one will be used. """ exclude: str | list[str] | None = field(default=None) """A pattern or list of patterns to skip in the Allowed Hosts middleware.""" exclude_opt_key: str | None = field(default=None) """An identifier to use on routes to disable hosts check for a particular route.""" exclude_urls_env_key: str = "LITESTAR" """Key to use when checking whether a list of excluded urls is passed via ENV. OpenTelemetry supports excluding urls by passing an env in the format '{exclude_urls_env_key}_EXCLUDED_URLS'. With the default being ``LITESTAR_EXCLUDED_URLS``. """ scopes: Scopes | None = field(default=None) """ASGI scopes processed by the middleware, if None both ``http`` and ``websocket`` will be processed.""" middleware_class: type[OpenTelemetryInstrumentationMiddleware] = field( default=OpenTelemetryInstrumentationMiddleware ) """The middleware class to use. Should be a subclass of OpenTelemetry InstrumentationMiddleware][litestar.contrib.opentelemetry.OpenTelemetryInstrumentationMiddleware]. """ @property def middleware(self) -> DefineMiddleware: """Create an instance of :class:`DefineMiddleware ` that wraps with. [OpenTelemetry InstrumentationMiddleware][litestar.contrib.opentelemetry.OpenTelemetryInstrumentationMiddleware] or a subclass of this middleware. Returns: An instance of ``DefineMiddleware``. """ return DefineMiddleware(self.middleware_class, config=self) litestar-2.16.0/litestar/contrib/opentelemetry/middleware.py000066400000000000000000000043571500564371300243160ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar.exceptions import MissingDependencyException from litestar.middleware.base import AbstractMiddleware __all__ = ("OpenTelemetryInstrumentationMiddleware",) try: import opentelemetry # noqa: F401 except ImportError as e: raise MissingDependencyException("opentelemetry") from e from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.util.http import get_excluded_urls if TYPE_CHECKING: from litestar.contrib.opentelemetry import OpenTelemetryConfig from litestar.types import ASGIApp, Receive, Scope, Send class OpenTelemetryInstrumentationMiddleware(AbstractMiddleware): """OpenTelemetry Middleware.""" def __init__(self, app: ASGIApp, config: OpenTelemetryConfig) -> None: """Middleware that adds OpenTelemetry instrumentation to the application. Args: app: The ``next`` ASGI app to call. config: An instance of :class:`OpenTelemetryConfig <.contrib.opentelemetry.OpenTelemetryConfig>` """ super().__init__(app=app, scopes=config.scopes, exclude=config.exclude, exclude_opt_key=config.exclude_opt_key) self.open_telemetry_middleware = OpenTelemetryMiddleware( app=app, client_request_hook=config.client_request_hook_handler, # type: ignore[arg-type] client_response_hook=config.client_response_hook_handler, # type: ignore[arg-type] default_span_details=config.scope_span_details_extractor, excluded_urls=get_excluded_urls(config.exclude_urls_env_key), meter=config.meter, meter_provider=config.meter_provider, server_request_hook=config.server_request_hook_handler, tracer_provider=config.tracer_provider, ) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI callable. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ await self.open_telemetry_middleware(scope, receive, send) # type: ignore[arg-type] # pyright: ignore[reportGeneralTypeIssues] litestar-2.16.0/litestar/contrib/opentelemetry/plugin.py000066400000000000000000000037411500564371300234730ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar.contrib.opentelemetry.config import OpenTelemetryConfig from litestar.contrib.opentelemetry.middleware import OpenTelemetryInstrumentationMiddleware from litestar.middleware.base import DefineMiddleware from litestar.plugins import InitPlugin if TYPE_CHECKING: from litestar.config.app import AppConfig from litestar.types.composite_types import Middleware class OpenTelemetryPlugin(InitPlugin): """OpenTelemetry Plugin.""" __slots__ = ("_middleware", "config") def __init__(self, config: OpenTelemetryConfig | None = None) -> None: self.config = config or OpenTelemetryConfig() self._middleware: DefineMiddleware | None = None super().__init__() @property def middleware(self) -> DefineMiddleware: if self._middleware: return self._middleware return DefineMiddleware(OpenTelemetryInstrumentationMiddleware, config=self.config) def on_app_init(self, app_config: AppConfig) -> AppConfig: app_config.middleware, _middleware = self._pop_otel_middleware(app_config.middleware) return app_config @staticmethod def _pop_otel_middleware(middlewares: list[Middleware]) -> tuple[list[Middleware], DefineMiddleware | None]: """Get the OpenTelemetry middleware if it is enabled in the application. Remove the middleware from the list of middlewares if it is found. """ otel_middleware: DefineMiddleware | None = None other_middlewares = [] for middleware in middlewares: if ( isinstance(middleware, DefineMiddleware) and isinstance(middleware.middleware, type) and issubclass(middleware.middleware, OpenTelemetryInstrumentationMiddleware) ): otel_middleware = middleware else: other_middlewares.append(middleware) return other_middlewares, otel_middleware litestar-2.16.0/litestar/contrib/piccolo.py000066400000000000000000000073771500564371300207420ustar00rootroot00000000000000from __future__ import annotations import warnings from dataclasses import replace from decimal import Decimal from typing import Any, Generator, Generic, List, Optional, TypeVar from msgspec import Meta from typing_extensions import Annotated from litestar.dto import AbstractDTO, DTOField, Mark from litestar.dto.data_structures import DTOFieldDefinition from litestar.exceptions import LitestarWarning, MissingDependencyException from litestar.types import Empty from litestar.typing import FieldDefinition from litestar.utils import warn_deprecation try: from piccolo.columns import Column, column_types from piccolo.table import Table except ImportError as e: raise MissingDependencyException("piccolo") from e T = TypeVar("T", bound=Table) __all__ = ("PiccoloDTO",) def __getattr__(name: str) -> Any: warn_deprecation( deprecated_name=f"litestar.contrib.piccolo.{name}", version="2.3.2", kind="import", removal_in="3.0.0", info="importing from 'litestar.contrib.piccolo' is deprecated and will be removed in 3.0, please import from 'litestar_piccolo' package directly instead", ) return getattr(name, name) def _parse_piccolo_type(column: Column, extra: dict[str, Any]) -> FieldDefinition: is_optional = not column._meta.required if isinstance(column, (column_types.Decimal, column_types.Numeric)): column_type: Any = Decimal meta = Meta(extra=extra) elif isinstance(column, (column_types.Email, column_types.Varchar)): column_type = str if is_optional: meta = Meta(extra=extra) warnings.warn( f"Dropping max_length constraint for column {column!r} because the " "column is optional", category=LitestarWarning, stacklevel=2, ) else: meta = Meta(max_length=column.length, extra=extra) elif isinstance(column, column_types.Array): column_type = List[column.base_column.value_type] # type: ignore[name-defined] meta = Meta(extra=extra) elif isinstance(column, (column_types.JSON, column_types.JSONB)): column_type = str meta = Meta(extra={**extra, "format": "json"}) elif isinstance(column, column_types.Text): column_type = str meta = Meta(extra={**extra, "format": "text-area"}) else: column_type = column.value_type meta = Meta(extra=extra) if is_optional: column_type = Optional[column_type] return FieldDefinition.from_annotation(Annotated[column_type, meta]) def _create_column_extra(column: Column) -> dict[str, Any]: extra: dict[str, Any] = {} if column._meta.help_text: extra["description"] = column._meta.help_text if column._meta.get_choices_dict(): extra["enum"] = column._meta.get_choices_dict() return extra class PiccoloDTO(AbstractDTO[T], Generic[T]): @classmethod def generate_field_definitions(cls, model_type: type[Table]) -> Generator[DTOFieldDefinition, None, None]: for column in model_type._meta.columns: mark = Mark.WRITE_ONLY if column._meta.secret else Mark.READ_ONLY if column._meta.primary_key else None yield replace( DTOFieldDefinition.from_field_definition( field_definition=_parse_piccolo_type(column, _create_column_extra(column)), dto_field=DTOField(mark=mark), model_name=model_type.__name__, default_factory=None, ), default=Empty if column._meta.required else None, name=column._meta.name, ) @classmethod def detect_nested_field(cls, field_definition: FieldDefinition) -> bool: return field_definition.is_subclass_of(Table) litestar-2.16.0/litestar/contrib/prometheus/000077500000000000000000000000001500564371300211155ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/prometheus/__init__.py000066400000000000000000000022041500564371300232240ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ("PrometheusConfig", "PrometheusController", "PrometheusMiddleware") def __getattr__(attr_name: str) -> object: if attr_name in __all__: from litestar.plugins.prometheus import ( PrometheusConfig, PrometheusController, PrometheusMiddleware, ) warn_deprecation( deprecated_name=f"litestar.contrib.prometheus.{attr_name}", version="2.13.0", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.prometheus' is deprecated, please " f"import it from 'litestar.plugins.prometheus' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from litestar.plugins.prometheus import ( PrometheusConfig, PrometheusController, PrometheusMiddleware, ) litestar-2.16.0/litestar/contrib/prometheus/config.py000066400000000000000000000016711500564371300227410ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ("PrometheusConfig",) def __getattr__(attr_name: str) -> object: if attr_name in __all__: from litestar.plugins.prometheus import PrometheusConfig warn_deprecation( deprecated_name=f"litestar.contrib.prometheus.config.{attr_name}", version="2.13.0", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.prometheus.config' is deprecated, please " f"import it from 'litestar.plugins.prometheus' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from litestar.plugins.prometheus import PrometheusConfig litestar-2.16.0/litestar/contrib/prometheus/controller.py000066400000000000000000000017151500564371300236560ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ("PrometheusController",) def __getattr__(attr_name: str) -> object: if attr_name in __all__: from litestar.plugins.prometheus import PrometheusController warn_deprecation( deprecated_name=f"litestar.contrib.prometheus.controller.{attr_name}", version="2.13.0", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.prometheus.controller' is deprecated, please " f"import it from 'litestar.plugins.prometheus' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from litestar.plugins.prometheus import PrometheusController litestar-2.16.0/litestar/contrib/prometheus/middleware.py000066400000000000000000000017151500564371300236100ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ("PrometheusMiddleware",) def __getattr__(attr_name: str) -> object: if attr_name in __all__: from litestar.plugins.prometheus import PrometheusMiddleware warn_deprecation( deprecated_name=f"litestar.contrib.prometheus.middleware.{attr_name}", version="2.13.0", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.prometheus.middleware' is deprecated, please " f"import it from 'litestar.plugins.prometheus' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from litestar.plugins.prometheus import PrometheusMiddleware litestar-2.16.0/litestar/contrib/pydantic/000077500000000000000000000000001500564371300205355ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/pydantic/__init__.py000066400000000000000000000024211500564371300226450ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING, Any from litestar.utils import warn_deprecation __all__ = ( "PydanticDIPlugin", "PydanticDTO", "PydanticInitPlugin", "PydanticPlugin", "PydanticSchemaPlugin", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: from litestar.plugins.pydantic import ( PydanticDIPlugin, PydanticDTO, PydanticInitPlugin, PydanticPlugin, PydanticSchemaPlugin, ) warn_deprecation( deprecated_name=f"litestar.contrib.pydantic.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.pydantic' is deprecated, please " f"import it from 'litestar.plugins.pydantic' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from litestar.plugins.pydantic import ( PydanticDIPlugin, PydanticDTO, PydanticInitPlugin, PydanticPlugin, PydanticSchemaPlugin, ) litestar-2.16.0/litestar/contrib/pydantic/config.py000066400000000000000000000000001500564371300223420ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/pydantic/pydantic_di_plugin.py000066400000000000000000000020271500564371300247550ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING # pragma: no cover from litestar.utils import warn_deprecation # pragma: no cover __all__ = ("PydanticDIPlugin",) # pragma: no cover def __getattr__(attr_name: str) -> object: # pragma: no cover if attr_name in __all__: from litestar.plugins.pydantic.plugins.di import PydanticDIPlugin warn_deprecation( deprecated_name=f"litestar.contrib.pydantic.pydantic_di_plugin.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.pydantic.pydantic_di_plugin' is deprecated, please " f"import it from 'litestar.plugins.pydantic' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") if TYPE_CHECKING: from litestar.plugins.pydantic.plugins.di import PydanticDIPlugin litestar-2.16.0/litestar/contrib/pydantic/pydantic_dto_factory.py000066400000000000000000000017761500564371300253320ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING # pragma: no cover from litestar.utils import warn_deprecation # pragma: no cover __all__ = ("PydanticDTO",) # pragma: no cover def __getattr__(attr_name: str) -> object: # pragma: no cover if attr_name in __all__: from litestar.plugins.pydantic.dto import PydanticDTO warn_deprecation( deprecated_name=f"litestar.contrib.pydantic.pydantic_dto_factory.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.pydantic.pydantic_dto_factory' is deprecated, please " f"import it from 'litestar.plugins.pydantic' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") if TYPE_CHECKING: from litestar.plugins.pydantic.dto import PydanticDTO litestar-2.16.0/litestar/contrib/pydantic/pydantic_init_plugin.py000066400000000000000000000020451500564371300253240ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING # pragma: no cover from litestar.utils import warn_deprecation # pragma: no cover __all__ = ("PydanticInitPlugin",) # pragma: no cover def __getattr__(attr_name: str) -> object: # pragma: no cover if attr_name in __all__: from litestar.plugins.pydantic.plugins.init import PydanticInitPlugin warn_deprecation( deprecated_name=f"litestar.contrib.pydantic.pydantic_init_plugin.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.pydantic.pydantic_init_plugin' is deprecated, please " f"import it from 'litestar.plugins.pydantic' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") if TYPE_CHECKING: from litestar.plugins.pydantic.plugins.init import PydanticInitPlugin litestar-2.16.0/litestar/contrib/pydantic/pydantic_schema_plugin.py000066400000000000000000000020631500564371300256210ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING # pragma: no cover from litestar.utils import warn_deprecation # pragma: no cover __all__ = ("PydanticSchemaPlugin",) # pragma: no cover def __getattr__(attr_name: str) -> object: # pragma: no cover if attr_name in __all__: from litestar.plugins.pydantic.plugins.schema import PydanticSchemaPlugin warn_deprecation( deprecated_name=f"litestar.contrib.pydantic.pydantic_schema_plugin.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.pydantic.pydantic_schema_plugin' is deprecated, please " f"import it from 'litestar.plugins.pydantic' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") if TYPE_CHECKING: from litestar.plugins.pydantic.plugins.schema import PydanticSchemaPlugin litestar-2.16.0/litestar/contrib/pydantic/utils.py000066400000000000000000000025301500564371300222470ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "get_model_info", "is_pydantic_constrained_field", "is_pydantic_model_class", "is_pydantic_undefined", "is_pydantic_v2", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: from litestar.plugins.pydantic.utils import ( get_model_info, is_pydantic_constrained_field, is_pydantic_model_class, is_pydantic_undefined, is_pydantic_v2, ) warn_deprecation( deprecated_name=f"litestar.contrib.pydantic.utils.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.pydantic.utils' is deprecated, please " f"import it from 'litestar.plugins.pydantic.utils' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") if TYPE_CHECKING: from litestar.plugins.pydantic.utils import ( get_model_info, is_pydantic_constrained_field, is_pydantic_model_class, is_pydantic_undefined, is_pydantic_v2, ) litestar-2.16.0/litestar/contrib/repository/000077500000000000000000000000001500564371300211415ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/repository/__init__.py000066400000000000000000000012771500564371300232610ustar00rootroot00000000000000from litestar.utils import warn_deprecation def __getattr__(attr_name: str) -> object: from litestar import repository if attr_name in repository.__all__: warn_deprecation( deprecated_name=f"litestar.contrib.repository.{attr_name}", version="2.1", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.repository' is deprecated, please" f"import it from 'litestar.repository.{attr_name}' instead", ) value = globals()[attr_name] = getattr(repository, attr_name) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") litestar-2.16.0/litestar/contrib/repository/abc/000077500000000000000000000000001500564371300216665ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/repository/abc/__init__.py000066400000000000000000000013011500564371300237720ustar00rootroot00000000000000from litestar.utils import warn_deprecation def __getattr__(attr_name: str) -> object: from litestar.repository import abc if attr_name in abc.__all__: warn_deprecation( deprecated_name=f"litestar.contrib.repository.abc.{attr_name}", version="2.1", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.repository.abc' is deprecated, please" f"import it from 'litestar.repository.abc.{attr_name}' instead", ) value = globals()[attr_name] = getattr(abc, attr_name) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") litestar-2.16.0/litestar/contrib/repository/exceptions.py000066400000000000000000000013531500564371300236760ustar00rootroot00000000000000from litestar.utils import warn_deprecation def __getattr__(attr_name: str) -> object: from litestar.repository import exceptions if attr_name in exceptions.__all__: warn_deprecation( deprecated_name=f"litestar.repository.contrib.exceptions.{attr_name}", version="2.1", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.repository.exceptions' is deprecated, please" f"import it from 'litestar.repository.exceptions.{attr_name}' instead", ) value = globals()[attr_name] = getattr(exceptions, attr_name) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") litestar-2.16.0/litestar/contrib/repository/filters.py000066400000000000000000000013311500564371300231610ustar00rootroot00000000000000from litestar.utils import warn_deprecation def __getattr__(attr_name: str) -> object: from litestar.repository import filters if attr_name in filters.__all__: warn_deprecation( deprecated_name=f"litestar.repository.contrib.filters.{attr_name}", version="2.1", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.repository.filters' is deprecated, please" f"import it from 'litestar.repository.filters.{attr_name}' instead", ) value = globals()[attr_name] = getattr(filters, attr_name) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") litestar-2.16.0/litestar/contrib/repository/handlers.py000066400000000000000000000013371500564371300233170ustar00rootroot00000000000000from litestar.utils import warn_deprecation def __getattr__(attr_name: str) -> object: from litestar.repository import handlers if attr_name in handlers.__all__: warn_deprecation( deprecated_name=f"litestar.repository.contrib.handlers.{attr_name}", version="2.1", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.repository.handlers' is deprecated, please" f"import it from 'litestar.repository.handlers.{attr_name}' instead", ) value = globals()[attr_name] = getattr(handlers, attr_name) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") litestar-2.16.0/litestar/contrib/repository/testing.py000066400000000000000000000014211500564371300231660ustar00rootroot00000000000000from litestar.utils import warn_deprecation def __getattr__(attr_name: str) -> object: from litestar.repository.testing import generic_mock_repository if attr_name in generic_mock_repository.__all__: warn_deprecation( deprecated_name=f"litestar.repository.contrib.testing.{attr_name}", version="2.1", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.repository.testing' is deprecated, please" f"import it from 'litestar.repository.testing.{attr_name}' instead", ) value = globals()[attr_name] = getattr(generic_mock_repository, attr_name) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") litestar-2.16.0/litestar/contrib/sqlalchemy/000077500000000000000000000000001500564371300210645ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/sqlalchemy/__init__.py000066400000000000000000000040541500564371300232000ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "ModelT", "SQLAlchemyAsyncRepository", "SQLAlchemySyncRepository", "wrap_sqlalchemy_exception", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: if attr_name in ("SQLAlchemyAsyncRepository", "SQLAlchemySyncRepository", "ModelT"): module = "litestar.plugins.sqlalchemy.repository" from advanced_alchemy.extensions.litestar import ( repository, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] ) value = globals()[attr_name] = getattr(repository, attr_name) elif attr_name == "wrap_sqlalchemy_exception": module = "litestar.plugins.sqlalchemy.exceptions" from advanced_alchemy.extensions.litestar import ( exceptions, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] ) value = globals()[attr_name] = getattr(exceptions, attr_name) else: # pragma: no cover raise RuntimeError(f"Unhandled module attribute: {attr_name!r}") warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy' is deprecated, please " f"import it from '{module}' instead", ) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.exceptions import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] wrap_sqlalchemy_exception, ) from advanced_alchemy.repository import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] ModelT, SQLAlchemyAsyncRepository, SQLAlchemySyncRepository, ) litestar-2.16.0/litestar/contrib/sqlalchemy/base.py000066400000000000000000000064231500564371300223550ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # pyright: reportUnusedImport=false """Application ORM configuration.""" from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "AuditColumns", "BigIntAuditBase", "BigIntBase", "BigIntPrimaryKey", "CommonTableAttributes", "ModelProtocol", "UUIDAuditBase", "UUIDBase", "UUIDPrimaryKey", "create_registry", "orm_registry", "touch_updated_timestamp", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: if attr_name == "touch_updated_timestamp": try: # v0.6.0+ from advanced_alchemy._listeners import touch_updated_timestamp # pyright: ignore except ImportError: from advanced_alchemy.base import touch_updated_timestamp # type: ignore[no-redef,attr-defined] warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.base.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.base' is deprecated, please" f"see the 'Advanced Alchemy' documentation for more details on how to use '{attr_name}' instead", ) value = globals()[attr_name] = locals()[attr_name] # pyright: ignore[reportUnknownVariableType] return value # pyright: ignore[reportUnknownVariableType] from advanced_alchemy.base import ( # pyright: ignore[reportMissingImports] BigIntAuditBase, BigIntBase, CommonTableAttributes, ModelProtocol, UUIDAuditBase, UUIDBase, create_registry, orm_registry, ) from advanced_alchemy.mixins import ( # pyright: ignore[reportMissingImports] AuditColumns, BigIntPrimaryKey, UUIDPrimaryKey, ) warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.base.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.base' is deprecated, please" f"import it from 'litestar.plugins.sqlalchemy.base.{attr_name}' instead", ) value = globals()[attr_name] = locals()[attr_name] # pyright: ignore[reportUnknownVariableType] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: try: # v0.6.0+ from advanced_alchemy._listeners import touch_updated_timestamp # pyright: ignore except ImportError: from advanced_alchemy.base import touch_updated_timestamp # type: ignore[no-redef,attr-defined] from advanced_alchemy.base import ( # pyright: ignore[reportMissingImports] BigIntAuditBase, BigIntBase, CommonTableAttributes, ModelProtocol, UUIDAuditBase, UUIDBase, create_registry, orm_registry, ) from advanced_alchemy.mixins import ( # pyright: ignore[reportMissingImports] AuditColumns, BigIntPrimaryKey, UUIDPrimaryKey, ) litestar-2.16.0/litestar/contrib/sqlalchemy/dto.py000066400000000000000000000024551500564371300222320ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # pyright: reportUnusedImport=false """SQLAlchemy DTO configuration.""" from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ("SQLAlchemyDTO", "SQLAlchemyDTOConfig") def __getattr__(attr_name: str) -> object: if attr_name in __all__: from advanced_alchemy.extensions.litestar.dto import ( SQLAlchemyDTO, # pyright: ignore[reportMissingImports] SQLAlchemyDTOConfig, # pyright: ignore[reportMissingImports] ) warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.dto' is deprecated, please " f"import it from 'litestar.plugins.sqlalchemy.dto' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.extensions.litestar.dto import ( SQLAlchemyDTO, # pyright: ignore[reportMissingImports] SQLAlchemyDTOConfig, # pyright: ignore[reportMissingImports] ) litestar-2.16.0/litestar/contrib/sqlalchemy/plugins/000077500000000000000000000000001500564371300225455ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/sqlalchemy/plugins/__init__.py000066400000000000000000000044551500564371300246660ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # pyright: reportUnusedImport=false from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "AsyncSessionConfig", "EngineConfig", "GenericSQLAlchemyConfig", "GenericSessionConfig", "SQLAlchemyAsyncConfig", "SQLAlchemyInitPlugin", "SQLAlchemyPlugin", "SQLAlchemySerializationPlugin", "SQLAlchemySyncConfig", "SyncSessionConfig", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: if attr_name in ("GenericSQLAlchemyConfig", "GenericSessionConfig"): module = "litestar.plugins.sqlalchemy.config" from advanced_alchemy.config import ( # pyright: ignore[reportMissingImports] GenericSessionConfig, GenericSQLAlchemyConfig, ) value = globals()[attr_name] = locals()[attr_name] else: module = "litestar.plugins.sqlalchemy" from advanced_alchemy.extensions.litestar import ( # pyright: ignore[reportMissingImports] AsyncSessionConfig, EngineConfig, SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin, SQLAlchemyPlugin, SQLAlchemySerializationPlugin, SQLAlchemySyncConfig, SyncSessionConfig, ) value = globals()[attr_name] = locals()[attr_name] warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.plugins.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins' is deprecated, please " f"import it from '{module}' instead", ) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.config import GenericSessionConfig, GenericSQLAlchemyConfig from advanced_alchemy.extensions.litestar import ( AsyncSessionConfig, EngineConfig, SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin, SQLAlchemyPlugin, SQLAlchemySerializationPlugin, SQLAlchemySyncConfig, SyncSessionConfig, ) litestar-2.16.0/litestar/contrib/sqlalchemy/plugins/init/000077500000000000000000000000001500564371300235105ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/sqlalchemy/plugins/init/__init__.py000066400000000000000000000042371500564371300256270ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # pyright: reportUnusedImport=false from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "AsyncSessionConfig", "EngineConfig", "GenericSQLAlchemyConfig", "GenericSessionConfig", "SQLAlchemyAsyncConfig", "SQLAlchemyInitPlugin", "SQLAlchemySyncConfig", "SyncSessionConfig", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: if attr_name in ("GenericSQLAlchemyConfig", "GenericSessionConfig"): module = "advanced_alchemy.config" from advanced_alchemy.config import ( # pyright: ignore[reportMissingImports] GenericSessionConfig, GenericSQLAlchemyConfig, ) value = globals()[attr_name] = locals()[attr_name] else: module = "litestar.plugins.sqlalchemy" from advanced_alchemy.extensions.litestar import ( AsyncSessionConfig, EngineConfig, SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin, SQLAlchemySyncConfig, SyncSessionConfig, ) value = globals()[attr_name] = locals()[attr_name] warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated, please " f"import it from '{module}' instead", ) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.config import ( # pyright: ignore[reportMissingImports] GenericSessionConfig, GenericSQLAlchemyConfig, ) from advanced_alchemy.extensions.litestar import ( # pyright: ignore[reportMissingImports] AsyncSessionConfig, EngineConfig, SQLAlchemyAsyncConfig, SQLAlchemyInitPlugin, SQLAlchemySyncConfig, SyncSessionConfig, ) litestar-2.16.0/litestar/contrib/sqlalchemy/plugins/init/config/000077500000000000000000000000001500564371300247555ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/sqlalchemy/plugins/init/config/__init__.py000066400000000000000000000046601500564371300270740ustar00rootroot00000000000000# ruff: noqa: TC004,F401 from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "AsyncSessionConfig", "EngineConfig", "GenericSQLAlchemyConfig", "GenericSessionConfig", "SQLAlchemyAsyncConfig", "SQLAlchemySyncConfig", "SyncSessionConfig", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: if attr_name in ("GenericSQLAlchemyConfig", "GenericSessionConfig"): module = "litestar.plugins.sqlalchemy.config" from advanced_alchemy.config import ( # pyright: ignore[reportMissingImports] GenericSessionConfig, # pyright: ignore[reportUnusedImport] GenericSQLAlchemyConfig, # pyright: ignore[reportUnusedImport] ) value = globals()[attr_name] = locals()[attr_name] else: module = "litestar.plugins.sqlalchemy" from advanced_alchemy.extensions.litestar import ( # pyright: ignore[reportMissingImports] AsyncSessionConfig, # pyright: ignore[reportUnusedImport] EngineConfig, # pyright: ignore[reportUnusedImport] SQLAlchemyAsyncConfig, # pyright: ignore[reportUnusedImport] SQLAlchemySyncConfig, # pyright: ignore[reportUnusedImport] SyncSessionConfig, # pyright: ignore[reportUnusedImport] ) value = globals()[attr_name] = locals()[attr_name] warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.config.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init.config' is deprecated, please " f"import it from '{module}' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.config import ( # pyright: ignore[reportMissingImports] GenericSessionConfig, GenericSQLAlchemyConfig, ) from advanced_alchemy.extensions.litestar import ( # pyright: ignore[reportMissingImports] AsyncSessionConfig, EngineConfig, SQLAlchemyAsyncConfig, SQLAlchemySyncConfig, SyncSessionConfig, ) litestar-2.16.0/litestar/contrib/sqlalchemy/plugins/init/config/asyncio.py000066400000000000000000000052331500564371300267770ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # pyright: reportUnusedImport=false from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "AlembicAsyncConfig", "AsyncSessionConfig", "SQLAlchemyAsyncConfig", "autocommit_before_send_handler", "default_before_send_handler", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: if attr_name == "SQLAlchemyAsyncConfig": from advanced_alchemy.extensions.litestar.plugins.init.config.asyncio import ( SQLAlchemyAsyncConfig as _SQLAlchemyAsyncConfig, ) from sqlalchemy.ext.asyncio import AsyncEngine from litestar.contrib.sqlalchemy.plugins.init.config.compat import ( _CreateEngineMixin, # pyright: ignore[reportPrivateUsage] ) class SQLAlchemyAsyncConfig(_SQLAlchemyAsyncConfig, _CreateEngineMixin[AsyncEngine]): ... module = "litestar.plugins.sqlalchemy" value = globals()[attr_name] = SQLAlchemyAsyncConfig elif attr_name in {"default_before_send_handler", "autocommit_before_send_handler"}: module = "litestar.plugins.sqlalchemy.plugins.init.config.asyncio" from advanced_alchemy.extensions.litestar.plugins.init.config.asyncio import ( autocommit_before_send_handler, default_before_send_handler, ) value = globals()[attr_name] = locals()[attr_name] else: module = "litestar.plugins.sqlalchemy" from advanced_alchemy.extensions.litestar import ( AlembicAsyncConfig, AsyncSessionConfig, ) value = globals()[attr_name] = locals()[attr_name] warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.config.asyncio.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init.config.asyncio' is deprecated, please " f"import it from '{module}' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.extensions.litestar import ( AlembicAsyncConfig, AsyncSessionConfig, SQLAlchemyAsyncConfig, ) from advanced_alchemy.extensions.litestar.plugins.init.config.asyncio import ( autocommit_before_send_handler, default_before_send_handler, ) litestar-2.16.0/litestar/contrib/sqlalchemy/plugins/init/config/common.py000066400000000000000000000041421500564371300266200ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "SESSION_SCOPE_KEY", "SESSION_TERMINUS_ASGI_EVENTS", "GenericAlembicConfig", "GenericSQLAlchemyConfig", "GenericSessionConfig", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: if attr_name in ("GenericSQLAlchemyConfig", "GenericSessionConfig", "GenericAlembicConfig"): module = "litestar.plugins.sqlalchemy.config" from advanced_alchemy.config.common import ( # pyright: ignore[reportMissingImports] GenericAlembicConfig, # pyright: ignore[reportUnusedImport] GenericSessionConfig, # pyright: ignore[reportUnusedImport] GenericSQLAlchemyConfig, # pyright: ignore[reportUnusedImport] ) else: from advanced_alchemy.extensions.litestar.plugins.init.config.common import ( # pyright: ignore[reportMissingImports] SESSION_SCOPE_KEY, # pyright: ignore[reportUnusedImport] SESSION_TERMINUS_ASGI_EVENTS, # pyright: ignore[reportUnusedImport] ) module = "litestar.plugins.sqlalchemy.plugins.init.config.common" warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.config.common.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init.config.common' is deprecated, please " f"import it from '{module}' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.config.common import GenericAlembicConfig, GenericSessionConfig, GenericSQLAlchemyConfig from advanced_alchemy.extensions.litestar.plugins.init.config.common import ( SESSION_SCOPE_KEY, SESSION_TERMINUS_ASGI_EVENTS, ) litestar-2.16.0/litestar/contrib/sqlalchemy/plugins/init/config/compat.py000066400000000000000000000012651500564371300266160ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Generic, Protocol, TypeVar from litestar.utils.deprecation import deprecated if TYPE_CHECKING: from sqlalchemy import Engine from sqlalchemy.ext.asyncio import AsyncEngine EngineT_co = TypeVar("EngineT_co", bound="Engine | AsyncEngine", covariant=True) class HasGetEngine(Protocol[EngineT_co]): def get_engine(self) -> EngineT_co: ... class _CreateEngineMixin(Generic[EngineT_co]): # pyright: ignore[reportUnusedClass] @deprecated(version="2.1.1", removal_in="3.0.0", alternative="get_engine()") def create_engine(self: HasGetEngine[EngineT_co]) -> EngineT_co: return self.get_engine() litestar-2.16.0/litestar/contrib/sqlalchemy/plugins/init/config/engine.py000066400000000000000000000017731500564371300266040ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ("EngineConfig",) def __getattr__(attr_name: str) -> object: if attr_name in __all__: from advanced_alchemy.extensions.litestar import EngineConfig module = "litestar.plugins.sqlalchemy" warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.config.engine.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init.config.engine' is deprecated, please " f"import it from '{module}' instead", ) value = globals()[attr_name] = EngineConfig return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.extensions.litestar import EngineConfig litestar-2.16.0/litestar/contrib/sqlalchemy/plugins/init/config/sync.py000066400000000000000000000052431500564371300263070ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "AlembicSyncConfig", "SQLAlchemySyncConfig", "SyncSessionConfig", "autocommit_before_send_handler", "default_before_send_handler", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: if attr_name == "SQLAlchemySyncConfig": from advanced_alchemy.extensions.litestar.plugins.init.config.sync import ( SQLAlchemySyncConfig as _SQLAlchemySyncConfig, ) from sqlalchemy import Engine from litestar.contrib.sqlalchemy.plugins.init.config.compat import ( _CreateEngineMixin, # pyright: ignore[reportPrivateUsage] ) class SQLAlchemySyncConfig(_SQLAlchemySyncConfig, _CreateEngineMixin[Engine]): ... module = "litestar.plugins.sqlalchemy" value = globals()[attr_name] = SQLAlchemySyncConfig elif attr_name in {"default_before_send_handler", "autocommit_before_send_handler"}: module = "litestar.plugins.sqlalchemy.plugins.init.config.sync" from advanced_alchemy.extensions.litestar.plugins.init.config.sync import ( autocommit_before_send_handler, # pyright: ignore[reportUnusedImport] default_before_send_handler, # pyright: ignore[reportUnusedImport] ) value = globals()[attr_name] = locals()[attr_name] else: module = "litestar.plugins.sqlalchemy" from advanced_alchemy.extensions.litestar import ( AlembicSyncConfig, # pyright: ignore[reportUnusedImport] SyncSessionConfig, # pyright: ignore[reportUnusedImport] ) value = globals()[attr_name] = locals()[attr_name] warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.config.sync.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init.config.sync' is deprecated, please " f"import it from '{module}' instead", ) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.extensions.litestar import ( AlembicSyncConfig, SQLAlchemySyncConfig, SyncSessionConfig, ) from advanced_alchemy.extensions.litestar.plugins.init.config.sync import ( autocommit_before_send_handler, default_before_send_handler, ) litestar-2.16.0/litestar/contrib/sqlalchemy/plugins/init/plugin.py000066400000000000000000000020051500564371300253550ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # pyright: reportUnusedImport=false from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ("SQLAlchemyInitPlugin",) def __getattr__(attr_name: str) -> object: if attr_name in __all__: warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.plugins.init.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated, please " f"import it from 'litestar.plugins.sqlalchemy' instead", ) from advanced_alchemy.extensions.litestar import SQLAlchemyInitPlugin value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.extensions.litestar import SQLAlchemyInitPlugin litestar-2.16.0/litestar/contrib/sqlalchemy/plugins/serialization.py000066400000000000000000000020211500564371300257670ustar00rootroot00000000000000# ruff: noqa: TC004 from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ("SQLAlchemySerializationPlugin",) def __getattr__(attr_name: str) -> object: if attr_name in __all__: warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.plugins.serialization.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.plugins.serialization' is deprecated, please " "import it from 'litestar.plugins.sqlalchemy' instead", ) from advanced_alchemy.extensions.litestar import SQLAlchemySerializationPlugin value = globals()[attr_name] = SQLAlchemySerializationPlugin return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.extensions.litestar import SQLAlchemySerializationPlugin litestar-2.16.0/litestar/contrib/sqlalchemy/repository/000077500000000000000000000000001500564371300233035ustar00rootroot00000000000000litestar-2.16.0/litestar/contrib/sqlalchemy/repository/__init__.py000066400000000000000000000042361500564371300254210ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # pyright: reportUnusedImport=false from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "ModelT", "SQLAlchemyAsyncRepository", "SQLAlchemySyncRepository", "wrap_sqlalchemy_exception", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: if attr_name in ("SQLAlchemyAsyncRepository", "SQLAlchemySyncRepository", "ModelT"): module = "litestar.plugins.sqlalchemy.repository" from advanced_alchemy.repository import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImport] ModelT, SQLAlchemyAsyncRepository, SQLAlchemySyncRepository, ) elif attr_name == "wrap_sqlalchemy_exception": module = "litestar.plugins.sqlalchemy.exceptions" from advanced_alchemy.exceptions import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImport] wrap_sqlalchemy_exception, # type: ignore[import-not-found] # pyright: ignore[reportMissingImport] ) else: # pragma: no cover raise RuntimeError(f"Unhandled module attribute: {attr_name!r}") value = globals()[attr_name] = locals()[attr_name] warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.repository.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.repository' is deprecated, please " f"import it from '{module}' instead", ) return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.exceptions import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImport] wrap_sqlalchemy_exception, ) from advanced_alchemy.repository import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImport] ModelT, SQLAlchemyAsyncRepository, SQLAlchemySyncRepository, ) litestar-2.16.0/litestar/contrib/sqlalchemy/repository/_async.py000066400000000000000000000022771500564371300251410ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ("SQLAlchemyAsyncRepository",) def __getattr__(attr_name: str) -> object: if attr_name in __all__: from advanced_alchemy.repository import ( SQLAlchemyAsyncRepository, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports,reportUnusedImport] ) warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.repository.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.repository._async' is deprecated, please " f"import it from 'litestar.plugins.sqlalchemy.repository' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.repository import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] SQLAlchemyAsyncRepository, ) litestar-2.16.0/litestar/contrib/sqlalchemy/repository/_sync.py000066400000000000000000000024571500564371300250000ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # Do not edit this file directly. It has been autogenerated from # litestar/contrib/sqlalchemy/repository/_async.py from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ("SQLAlchemySyncRepository",) def __getattr__(attr_name: str) -> object: if attr_name in __all__: from advanced_alchemy.repository import ( SQLAlchemySyncRepository, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports,reportUnusedImport] ) warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.repository.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.repository._sync' is deprecated, please " f"import it from 'litestar.plugins.sqlalchemy.repository' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.repository import ( SQLAlchemySyncRepository, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] ) litestar-2.16.0/litestar/contrib/sqlalchemy/repository/_util.py000066400000000000000000000030301500564371300247650ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "get_instrumented_attr", "wrap_sqlalchemy_exception", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: from advanced_alchemy.exceptions import ( wrap_sqlalchemy_exception, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] ) from advanced_alchemy.repository import ( get_instrumented_attr, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] ) warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.repository._util.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.repository._util' is deprecated, please " f"import it from 'litestar.plugins.sqlalchemy.repository' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.exceptions import ( wrap_sqlalchemy_exception, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] ) from advanced_alchemy.repository import ( get_instrumented_attr, # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] ) litestar-2.16.0/litestar/contrib/sqlalchemy/repository/types.py000066400000000000000000000027571500564371300250340ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # pyright: reportUnusedImport=false from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "ModelT", "RowT", "SQLAlchemyAsyncRepositoryT", "SQLAlchemySyncRepositoryT", "SelectT", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: from advanced_alchemy.repository.typing import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] ModelT, RowT, SelectT, SQLAlchemyAsyncRepositoryT, SQLAlchemySyncRepositoryT, ) warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.repository.types.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy.repository.types' is deprecated, please " f"import it from 'litestar.plugins.sqlalchemy.repository.typing' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.repository.typing import ( # type: ignore[import-not-found] # pyright: ignore[reportMissingImports] ModelT, RowT, SelectT, SQLAlchemyAsyncRepositoryT, SQLAlchemySyncRepositoryT, ) litestar-2.16.0/litestar/contrib/sqlalchemy/types.py000066400000000000000000000023231500564371300226020ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # pyright: reportUnusedImport=false from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "GUID", "ORA_JSONB", "BigIntIdentity", "DateTimeUTC", "JsonB", ) def __getattr__(attr_name: str) -> object: if attr_name in __all__: from advanced_alchemy.types import ( GUID, ORA_JSONB, BigIntIdentity, DateTimeUTC, JsonB, ) warn_deprecation( deprecated_name=f"litestar.contrib.sqlalchemy.{attr_name}", version="2.12", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.contrib.sqlalchemy' is deprecated, please " f"import it from 'advanced_alchemy.extensions.litestar.types' instead", ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") # pragma: no cover if TYPE_CHECKING: from advanced_alchemy.types import ( GUID, ORA_JSONB, BigIntIdentity, DateTimeUTC, JsonB, ) litestar-2.16.0/litestar/controller.py000066400000000000000000000306741500564371300200310ustar00rootroot00000000000000from __future__ import annotations import types from collections import defaultdict from copy import deepcopy from operator import attrgetter from typing import TYPE_CHECKING, Any, Mapping, Sequence, cast from litestar._layers.utils import narrow_response_cookies, narrow_response_headers from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers.base import BaseRouteHandler from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.handlers.websocket_handlers import WebsocketRouteHandler from litestar.types.empty import Empty from litestar.utils import normalize_path from litestar.utils.signature import add_types_to_signature_namespace __all__ = ("Controller",) if TYPE_CHECKING: from litestar.connection import Request, WebSocket from litestar.datastructures import CacheControlHeader, ETag from litestar.dto import AbstractDTO from litestar.openapi.spec import SecurityRequirement from litestar.response import Response from litestar.router import Router from litestar.types import ( AfterRequestHookHandler, AfterResponseHookHandler, BeforeRequestHookHandler, Dependencies, ExceptionHandlersMap, Guard, Middleware, ParametersMap, ResponseCookies, TypeEncodersMap, ) from litestar.types.composite_types import ResponseHeaders, TypeDecodersSequence from litestar.types.empty import EmptyType class Controller: """The Litestar Controller class. Subclass this class to create 'view' like components and utilize OOP. """ __slots__ = ( "after_request", "after_response", "before_request", "cache_control", "dependencies", "dto", "etag", "exception_handlers", "guards", "include_in_schema", "middleware", "opt", "owner", "parameters", "path", "request_class", "request_max_body_size", "response_class", "response_cookies", "response_headers", "return_dto", "security", "signature_namespace", "signature_types", "tags", "type_decoders", "type_encoders", "websocket_class", ) after_request: AfterRequestHookHandler | None """A sync or async function executed before a :class:`Request <.connection.Request>` is passed to any route handler. If this function returns a value, the request will not reach the route handler, and instead this value will be used. """ after_response: AfterResponseHookHandler | None """A sync or async function called after the response has been awaited. It receives the :class:`Request <.connection.Request>` instance and should not return any values. """ before_request: BeforeRequestHookHandler | None """A sync or async function called immediately before calling the route handler. It receives the :class:`Request <.connection.Request>` instance and any non-``None`` return value is used for the response, bypassing the route handler. """ cache_control: CacheControlHeader | None """A :class:`CacheControlHeader <.datastructures.CacheControlHeader>` header to add to route handlers of this controller. Can be overridden by route handlers. """ dependencies: Dependencies | None """A string keyed dictionary of dependency :class:`Provider <.di.Provide>` instances.""" dto: type[AbstractDTO] | None | EmptyType """:class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data.""" etag: ETag | None """An ``etag`` header of type :class:`ETag <.datastructures.ETag>` to add to route handlers of this controller. Can be overridden by route handlers. """ exception_handlers: ExceptionHandlersMap | None """A map of handler functions to status codes and/or exception types.""" guards: Sequence[Guard] | None """A sequence of :class:`Guard <.types.Guard>` callables.""" include_in_schema: bool | EmptyType """A boolean flag dictating whether the route handler should be documented in the OpenAPI schema""" middleware: Sequence[Middleware] | None """A sequence of :class:`Middleware <.types.Middleware>`.""" opt: Mapping[str, Any] | None """A string key mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. """ owner: Router """The :class:`Router <.router.Router>` or :class:`Litestar ` app that owns the controller. This value is set internally by Litestar and it should not be set when subclassing the controller. """ parameters: ParametersMap | None """A mapping of :class:`Parameter <.params.Parameter>` definitions available to all application paths.""" path: str """A path fragment for the controller. All route handlers under the controller will have the fragment appended to them. If not set it defaults to ``/``. """ request_class: type[Request] | None """A custom subclass of :class:`Request <.connection.Request>` to be used as the default request for all route handlers under the controller. """ request_max_body_size: int | None | EmptyType """ Maximum allowed size of the request body in bytes. If this size is exceeded, a '413 - Request Entity Too Large' error response is returned.""" response_class: type[Response] | None """A custom subclass of :class:`Response <.response.Response>` to be used as the default response for all route handlers under the controller. """ response_cookies: ResponseCookies | None """A list of :class:`Cookie <.datastructures.Cookie>` instances.""" response_headers: ResponseHeaders | None """A string keyed dictionary mapping :class:`ResponseHeader <.datastructures.ResponseHeader>` instances.""" return_dto: type[AbstractDTO] | None | EmptyType """:class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. """ tags: Sequence[str] | None """A sequence of string tags that will be appended to the schema of all route handlers under the controller.""" security: Sequence[SecurityRequirement] | None """A sequence of dictionaries that to the schema of all route handlers under the controller.""" signature_namespace: dict[str, Any] """A mapping of names to types for use in forward reference resolution during signature modelling.""" signature_types: Sequence[Any] """A sequence of types for use in forward reference resolution during signature modelling. These types will be added to the signature namespace using their ``__name__`` attribute. """ type_decoders: TypeDecodersSequence | None """A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization.""" type_encoders: TypeEncodersMap | None """A mapping of types to callables that transform them into types supported for serialization.""" websocket_class: type[WebSocket] | None """A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as the default websocket for all route handlers under the controller. """ def __init__(self, owner: Router) -> None: """Initialize a controller. Should only be called by routers as part of controller registration. Args: owner: An instance of :class:`Router <.router.Router>` """ # Since functions set on classes are bound, we need replace the bound instance with the class version for key in ("after_request", "after_response", "before_request"): cls_value = getattr(type(self), key, None) if callable(cls_value): setattr(self, key, cls_value) if not hasattr(self, "dto"): self.dto = Empty if not hasattr(self, "return_dto"): self.return_dto = Empty if not hasattr(self, "include_in_schema"): self.include_in_schema = Empty if not hasattr(self, "request_max_body_size"): self.request_max_body_size = Empty self.signature_namespace = add_types_to_signature_namespace( getattr(self, "signature_types", []), getattr(self, "signature_namespace", {}) ) for key in self.__slots__: if not hasattr(self, key): setattr(self, key, None) self.response_cookies = narrow_response_cookies(self.response_cookies) self.response_headers = narrow_response_headers(self.response_headers) self.path = normalize_path(self.path or "/") self.owner = owner def as_router(self) -> Router: from litestar.router import Router router = Router( path=self.path, route_handlers=self.get_route_handlers(), after_request=self.after_request, after_response=self.after_response, before_request=self.before_request, cache_control=self.cache_control, dependencies=self.dependencies, dto=self.dto, etag=self.etag, exception_handlers=self.exception_handlers, guards=self.guards, include_in_schema=self.include_in_schema, middleware=self.middleware, opt=self.opt, parameters=self.parameters, request_class=self.request_class, response_class=self.response_class, response_cookies=self.response_cookies, response_headers=self.response_headers, return_dto=self.return_dto, security=self.security, signature_types=self.signature_types, signature_namespace=self.signature_namespace, tags=self.tags, type_encoders=self.type_encoders, type_decoders=self.type_decoders, websocket_class=self.websocket_class, request_max_body_size=self.request_max_body_size, ) router.owner = self.owner return router def get_route_handlers(self) -> list[BaseRouteHandler]: """Get a controller's route handlers and set the controller as the handlers' owner. Returns: A list containing a copy of the route handlers defined on the controller """ route_handlers: list[BaseRouteHandler] = [] controller_names = set(dir(Controller)) self_handlers = [ getattr(self, name) for name in dir(self) if name not in controller_names and isinstance(getattr(self, name), BaseRouteHandler) ] self_handlers.sort(key=attrgetter("handler_id")) for self_handler in self_handlers: route_handler = deepcopy(self_handler) # at the point we get a reference to the handler function, it's unbound, so # we replace it with a regular bound method here route_handler._fn = types.MethodType(route_handler._fn, self) route_handler.owner = self route_handlers.append(route_handler) self.validate_route_handlers(route_handlers=route_handlers) return route_handlers def validate_route_handlers(self, route_handlers: list[BaseRouteHandler]) -> None: """Validate that the combination of path and decorator method or type are unique on the controller. Args: route_handlers: The controller's route handlers. Raises: ImproperlyConfiguredException Returns: None """ paths: defaultdict[str, set[str]] = defaultdict(set) for route_handler in route_handlers: if isinstance(route_handler, HTTPRouteHandler): methods: set[str] = cast("set[str]", route_handler.http_methods) elif isinstance(route_handler, WebsocketRouteHandler): methods = {"websocket"} else: methods = {"asgi"} for path in route_handler.paths: if (entry := paths[path]) and (intersection := entry.intersection(methods)): raise ImproperlyConfiguredException( f"the combination of path and method must be unique in a controller - " f"the following methods {''.join(m.lower() for m in intersection)} for {type(self).__name__} " f"controller path {path} are not unique" ) paths[path].update(methods) litestar-2.16.0/litestar/data_extractors.py000066400000000000000000000424211500564371300210260ustar00rootroot00000000000000from __future__ import annotations import inspect from typing import TYPE_CHECKING, Any, Callable, Coroutine, Iterable, Literal, TypedDict, cast from litestar._parsers import parse_cookie_string from litestar.connection.request import Request from litestar.datastructures.upload_file import UploadFile from litestar.enums import HttpMethod, RequestEncodingType __all__ = ( "ConnectionDataExtractor", "ExtractedRequestData", "ExtractedResponseData", "RequestExtractorField", "ResponseDataExtractor", "ResponseExtractorField", ) if TYPE_CHECKING: from litestar.connection import ASGIConnection from litestar.types import Method from litestar.types.asgi_types import HTTPResponseBodyEvent, HTTPResponseStartEvent def _obfuscate(values: dict[str, Any], fields_to_obfuscate: set[str]) -> dict[str, Any]: """Obfuscate values in a dictionary, replacing values with `******` Args: values: A dictionary of strings fields_to_obfuscate: keys to obfuscate Returns: A dictionary with obfuscated strings """ return {key: "*****" if key.lower() in fields_to_obfuscate else value for key, value in values.items()} RequestExtractorField = Literal[ "path", "method", "content_type", "headers", "cookies", "query", "path_params", "body", "scheme", "client" ] ResponseExtractorField = Literal["status_code", "headers", "body", "cookies"] class ExtractedRequestData(TypedDict, total=False): """Dictionary representing extracted request data.""" body: Coroutine[Any, Any, Any] client: tuple[str, int] content_type: tuple[str, dict[str, str]] cookies: dict[str, str] headers: dict[str, str] method: Method path: str path_params: dict[str, Any] query: bytes | dict[str, Any] scheme: str class ConnectionDataExtractor: """Utility class to extract data from an :class:`ASGIConnection `, :class:`Request ` or :class:`WebSocket ` instance. """ __slots__ = ( "connection_extractors", "obfuscate_cookies", "obfuscate_headers", "parse_body", "parse_query", "request_extractors", "skip_parse_malformed_body", ) def __init__( self, extract_body: bool = True, extract_client: bool = True, extract_content_type: bool = True, extract_cookies: bool = True, extract_headers: bool = True, extract_method: bool = True, extract_path: bool = True, extract_path_params: bool = True, extract_query: bool = True, extract_scheme: bool = True, obfuscate_cookies: set[str] | None = None, obfuscate_headers: set[str] | None = None, parse_body: bool = False, parse_query: bool = False, skip_parse_malformed_body: bool = False, ) -> None: """Initialize ``ConnectionDataExtractor`` Args: extract_body: Whether to extract body, (for requests only). extract_client: Whether to extract the client (host, port) mapping. extract_content_type: Whether to extract the content type and any options. extract_cookies: Whether to extract cookies. extract_headers: Whether to extract headers. extract_method: Whether to extract the HTTP method, (for requests only). extract_path: Whether to extract the path. extract_path_params: Whether to extract path parameters. extract_query: Whether to extract query parameters. extract_scheme: Whether to extract the http scheme. obfuscate_headers: headers keys to obfuscate. Obfuscated values are replaced with '*****'. obfuscate_cookies: cookie keys to obfuscate. Obfuscated values are replaced with '*****'. parse_body: Whether to parse the body value or return the raw byte string, (for requests only). parse_query: Whether to parse query parameters or return the raw byte string. skip_parse_malformed_body: Whether to skip parsing the body if it is malformed """ self.parse_body = parse_body self.parse_query = parse_query self.skip_parse_malformed_body = skip_parse_malformed_body self.obfuscate_headers = {h.lower() for h in (obfuscate_headers or set())} self.obfuscate_cookies = {c.lower() for c in (obfuscate_cookies or set())} self.connection_extractors: dict[str, Callable[[ASGIConnection[Any, Any, Any, Any]], Any]] = {} self.request_extractors: dict[RequestExtractorField, Callable[[Request[Any, Any, Any]], Any]] = {} if extract_scheme: self.connection_extractors["scheme"] = self.extract_scheme if extract_client: self.connection_extractors["client"] = self.extract_client if extract_path: self.connection_extractors["path"] = self.extract_path if extract_headers: self.connection_extractors["headers"] = self.extract_headers if extract_cookies: self.connection_extractors["cookies"] = self.extract_cookies if extract_query: self.connection_extractors["query"] = self.extract_query if extract_path_params: self.connection_extractors["path_params"] = self.extract_path_params if extract_method: self.request_extractors["method"] = self.extract_method if extract_content_type: self.request_extractors["content_type"] = self.extract_content_type if extract_body: self.request_extractors["body"] = self.extract_body def __call__(self, connection: ASGIConnection[Any, Any, Any, Any]) -> ExtractedRequestData: """Extract data from the connection, returning a dictionary of values. Notes: - The value for ``body`` - if present - is an unresolved Coroutine and as such should be awaited by the receiver. Args: connection: An ASGI connection or its subclasses. Returns: A string keyed dictionary of extracted values. """ extractors = ( {**self.connection_extractors, **self.request_extractors} # type: ignore[misc] if isinstance(connection, Request) else self.connection_extractors ) return cast("ExtractedRequestData", {key: extractor(connection) for key, extractor in extractors.items()}) async def extract( self, connection: ASGIConnection[Any, Any, Any, Any], fields: Iterable[str] ) -> ExtractedRequestData: extractors = ( {**self.connection_extractors, **self.request_extractors} # type: ignore[misc] if isinstance(connection, Request) else self.connection_extractors ) data = {} for key, extractor in extractors.items(): if key not in fields: continue if inspect.iscoroutinefunction(extractor): value = await extractor(connection) else: value = extractor(connection) data[key] = value return cast("ExtractedRequestData", data) @staticmethod def extract_scheme(connection: ASGIConnection[Any, Any, Any, Any]) -> str: """Extract the scheme from an ``ASGIConnection`` Args: connection: An :class:`ASGIConnection ` instance. Returns: The connection's scope["scheme"] value """ return connection.scope["scheme"] @staticmethod def extract_client(connection: ASGIConnection[Any, Any, Any, Any]) -> tuple[str, int]: """Extract the client from an ``ASGIConnection`` Args: connection: An :class:`ASGIConnection ` instance. Returns: The connection's scope["client"] value or a default value. """ return connection.scope.get("client") or ("", 0) @staticmethod def extract_path(connection: ASGIConnection[Any, Any, Any, Any]) -> str: """Extract the path from an ``ASGIConnection`` Args: connection: An :class:`ASGIConnection ` instance. Returns: The connection's scope["path"] value """ return connection.scope["path"] def extract_headers(self, connection: ASGIConnection[Any, Any, Any, Any]) -> dict[str, str]: """Extract headers from an ``ASGIConnection`` Args: connection: An :class:`ASGIConnection ` instance. Returns: A dictionary with the connection's headers. """ headers = {k.decode("latin-1"): v.decode("latin-1") for k, v in connection.scope["headers"]} return _obfuscate(headers, self.obfuscate_headers) if self.obfuscate_headers else headers def extract_cookies(self, connection: ASGIConnection[Any, Any, Any, Any]) -> dict[str, str]: """Extract cookies from an ``ASGIConnection`` Args: connection: An :class:`ASGIConnection ` instance. Returns: A dictionary with the connection's cookies. """ return _obfuscate(connection.cookies, self.obfuscate_cookies) if self.obfuscate_cookies else connection.cookies def extract_query(self, connection: ASGIConnection[Any, Any, Any, Any]) -> Any: """Extract query from an ``ASGIConnection`` Args: connection: An :class:`ASGIConnection ` instance. Returns: Either a dictionary with the connection's parsed query string or the raw query byte-string. """ return connection.query_params.dict() if self.parse_query else connection.scope.get("query_string", b"") @staticmethod def extract_path_params(connection: ASGIConnection[Any, Any, Any, Any]) -> dict[str, Any]: """Extract the path parameters from an ``ASGIConnection`` Args: connection: An :class:`ASGIConnection ` instance. Returns: A dictionary with the connection's path parameters. """ return connection.path_params @staticmethod def extract_method(request: Request[Any, Any, Any]) -> Method: """Extract the method from an ``ASGIConnection`` Args: request: A :class:`Request ` instance. Returns: The request's scope["method"] value. """ return request.scope["method"] @staticmethod def extract_content_type(request: Request[Any, Any, Any]) -> tuple[str, dict[str, str]]: """Extract the content-type from an ``ASGIConnection`` Args: request: A :class:`Request ` instance. Returns: A tuple containing the request's parsed 'Content-Type' header. """ return request.content_type async def extract_body(self, request: Request[Any, Any, Any]) -> Any: """Extract the body from an ``ASGIConnection`` Args: request: A :class:`Request ` instance. Returns: Either the parsed request body or the raw byte-string. """ if request.method == HttpMethod.GET: return None if not self.parse_body: return await request.body() try: request_encoding_type = request.content_type[0] if request_encoding_type == RequestEncodingType.JSON: return await request.json() form_data = await request.form() if request_encoding_type == RequestEncodingType.URL_ENCODED: return dict(form_data) return { key: repr(value) if isinstance(value, UploadFile) else value for key, value in form_data.multi_items() } except Exception as exc: if self.skip_parse_malformed_body: return await request.body() raise exc class ExtractedResponseData(TypedDict, total=False): """Dictionary representing extracted response data.""" body: bytes status_code: int headers: dict[str, str] cookies: dict[str, str] class ResponseDataExtractor: """Utility class to extract data from a ``Message``""" __slots__ = ("extractors", "obfuscate_cookies", "obfuscate_headers", "parse_headers") def __init__( self, extract_body: bool = True, extract_cookies: bool = True, extract_headers: bool = True, extract_status_code: bool = True, obfuscate_cookies: set[str] | None = None, obfuscate_headers: set[str] | None = None, ) -> None: """Initialize ``ResponseDataExtractor`` with options. Args: extract_body: Whether to extract the body. extract_cookies: Whether to extract the cookies. extract_headers: Whether to extract the headers. extract_status_code: Whether to extract the status code. obfuscate_cookies: cookie keys to obfuscate. Obfuscated values are replaced with '*****'. obfuscate_headers: headers keys to obfuscate. Obfuscated values are replaced with '*****'. """ self.obfuscate_headers = {h.lower() for h in (obfuscate_headers or set())} self.obfuscate_cookies = {c.lower() for c in (obfuscate_cookies or set())} self.extractors: dict[ ResponseExtractorField, Callable[[tuple[HTTPResponseStartEvent, HTTPResponseBodyEvent]], Any] ] = {} if extract_body: self.extractors["body"] = self.extract_response_body if extract_status_code: self.extractors["status_code"] = self.extract_status_code if extract_headers: self.extractors["headers"] = self.extract_headers if extract_cookies: self.extractors["cookies"] = self.extract_cookies def __call__(self, messages: tuple[HTTPResponseStartEvent, HTTPResponseBodyEvent]) -> ExtractedResponseData: """Extract data from the response, returning a dictionary of values. Args: messages: A tuple containing :class:`HTTPResponseStartEvent ` and :class:`HTTPResponseBodyEvent `. Returns: A string keyed dictionary of extracted values. """ return cast("ExtractedResponseData", {key: extractor(messages) for key, extractor in self.extractors.items()}) @staticmethod def extract_response_body(messages: tuple[HTTPResponseStartEvent, HTTPResponseBodyEvent]) -> bytes: """Extract the response body from a ``Message`` Args: messages: A tuple containing :class:`HTTPResponseStartEvent ` and :class:`HTTPResponseBodyEvent `. Returns: The Response's body as a byte-string. """ return messages[1]["body"] @staticmethod def extract_status_code(messages: tuple[HTTPResponseStartEvent, HTTPResponseBodyEvent]) -> int: """Extract a status code from a ``Message`` Args: messages: A tuple containing :class:`HTTPResponseStartEvent ` and :class:`HTTPResponseBodyEvent `. Returns: The Response's status-code. """ return messages[0]["status"] def extract_headers(self, messages: tuple[HTTPResponseStartEvent, HTTPResponseBodyEvent]) -> dict[str, str]: """Extract headers from a ``Message`` Args: messages: A tuple containing :class:`HTTPResponseStartEvent ` and :class:`HTTPResponseBodyEvent `. Returns: The Response's headers dict. """ headers = { key.decode("latin-1"): value.decode("latin-1") for key, value in filter(lambda x: x[0].lower() != b"set-cookie", messages[0]["headers"]) } return ( _obfuscate( headers, self.obfuscate_headers, ) if self.obfuscate_headers else headers ) def extract_cookies(self, messages: tuple[HTTPResponseStartEvent, HTTPResponseBodyEvent]) -> dict[str, str]: """Extract cookies from a ``Message`` Args: messages: A tuple containing :class:`HTTPResponseStartEvent ` and :class:`HTTPResponseBodyEvent `. Returns: The Response's cookies dict. """ if cookie_string := ";".join( [x[1].decode("latin-1") for x in filter(lambda x: x[0].lower() == b"set-cookie", messages[0]["headers"])] ): parsed_cookies = parse_cookie_string(cookie_string) return _obfuscate(parsed_cookies, self.obfuscate_cookies) if self.obfuscate_cookies else parsed_cookies return {} litestar-2.16.0/litestar/datastructures/000077500000000000000000000000001500564371300203375ustar00rootroot00000000000000litestar-2.16.0/litestar/datastructures/__init__.py000066400000000000000000000017461500564371300224600ustar00rootroot00000000000000from litestar.datastructures.cookie import Cookie from litestar.datastructures.headers import ( Accept, CacheControlHeader, ETag, Header, Headers, MutableScopeHeaders, ) from litestar.datastructures.multi_dicts import ( FormMultiDict, ImmutableMultiDict, MultiDict, MultiMixin, ) from litestar.datastructures.response_header import ResponseHeader from litestar.datastructures.secret_values import SecretBytes, SecretString from litestar.datastructures.state import ImmutableState, State from litestar.datastructures.upload_file import UploadFile from litestar.datastructures.url import URL, Address __all__ = ( "URL", "Accept", "Address", "CacheControlHeader", "Cookie", "ETag", "FormMultiDict", "Header", "Headers", "ImmutableMultiDict", "ImmutableState", "MultiDict", "MultiMixin", "MutableScopeHeaders", "ResponseHeader", "SecretBytes", "SecretString", "State", "UploadFile", ) litestar-2.16.0/litestar/datastructures/cookie.py000066400000000000000000000072721500564371300221720ustar00rootroot00000000000000from __future__ import annotations from dataclasses import asdict, dataclass, field from http.cookies import SimpleCookie from typing import Any, Literal __all__ = ("Cookie",) @dataclass class Cookie: """Container class for defining a cookie using the ``Set-Cookie`` header. See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie for more details regarding this header. """ key: str """Key for the cookie.""" path: str = "/" """Path fragment that must exist in the request url for the cookie to be valid. Defaults to ``/``. """ value: str | None = field(default=None) """Value for the cookie, if none given defaults to empty string.""" max_age: int | None = field(default=None) """Maximal age of the cookie before its invalidated.""" expires: int | None = field(default=None) """Seconds from now until the cookie expires.""" domain: str | None = field(default=None) """Domain for which the cookie is valid.""" secure: bool | None = field(default=None) """Https is required for the cookie.""" httponly: bool | None = field(default=None) """Forbids javascript to access the cookie via ``document.cookie``.""" samesite: Literal["lax", "strict", "none"] = field(default="lax") """Controls whether or not a cookie is sent with cross-site requests. Defaults to 'lax'. """ description: str | None = field(default=None) """Description of the response cookie header for OpenAPI documentation.""" documentation_only: bool = field(default=False) """Defines the Cookie instance as for OpenAPI documentation purpose only.""" @property def simple_cookie(self) -> SimpleCookie: """Get a simple cookie object from the values. Returns: A :class:`SimpleCookie ` """ simple_cookie: SimpleCookie = SimpleCookie() simple_cookie[self.key] = self.value or "" namespace = simple_cookie[self.key] for key, value in self.dict.items(): if key in {"key", "value"}: continue if value is not None: updated_key = key if updated_key == "max_age": updated_key = "max-age" namespace[updated_key] = value return simple_cookie def to_header(self, **kwargs: Any) -> str: """Return a string representation suitable to be sent as HTTP headers. Args: **kwargs: Any kwargs to pass to the simple cookie output method. """ return self.simple_cookie.output(**kwargs).strip() def to_encoded_header(self) -> tuple[bytes, bytes]: """Create encoded header for ASGI ``send``. Returns: A two tuple of bytes. """ return b"set-cookie", self.to_header(header="").strip().encode("latin-1") @property def dict(self) -> dict[str, Any]: """Get the cookie as a dict. Returns: A dict of values """ return { k: v for k, v in asdict(self).items() if k not in {"documentation_only", "description", "__pydantic_initialised__"} } def __hash__(self) -> int: return hash((self.key, self.path, self.domain)) def __eq__(self, other: Any) -> bool: """Determine whether two cookie instances are equal according to the cookie spec, i.e. hey have a similar path, domain and key. Args: other: An arbitrary value Returns: A boolean """ if isinstance(other, Cookie): return other.key == self.key and other.path == self.path and other.domain == self.domain return False litestar-2.16.0/litestar/datastructures/headers.py000066400000000000000000000407151500564371300223330ustar00rootroot00000000000000import re from abc import ABC, abstractmethod from contextlib import suppress from copy import copy from dataclasses import dataclass, fields from typing import ( TYPE_CHECKING, Any, ClassVar, Dict, Iterable, Iterator, List, Mapping, MutableMapping, Optional, Pattern, Tuple, Union, cast, ) import msgspec from multidict import CIMultiDict, CIMultiDictProxy, MultiMapping from litestar._multipart import parse_content_header from litestar.datastructures.multi_dicts import MultiMixin from litestar.exceptions import ImproperlyConfiguredException, ValidationException from litestar.types.empty import Empty from litestar.utils.dataclass import simple_asdict from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from litestar.types.asgi_types import ( HeaderScope, Message, RawHeaders, RawHeadersList, Scope, ) __all__ = ("Accept", "CacheControlHeader", "ETag", "Header", "Headers", "MutableScopeHeaders") ETAG_RE = re.compile(r'([Ww]/)?"(.+)"') PRINTABLE_ASCII_RE: Pattern[str] = re.compile(r"^[ -~]+$") def _encode_headers(headers: Iterable[Tuple[str, str]]) -> "RawHeadersList": return [(key.lower().encode("latin-1"), value.encode("latin-1")) for key, value in headers] class Headers(CIMultiDictProxy[str], MultiMixin[str]): """An immutable, case-insensitive multi dict for HTTP headers.""" def __init__(self, headers: Optional[Union[Mapping[str, str], "RawHeaders", MultiMapping]] = None) -> None: """Initialize ``Headers``. Args: headers: Initial value. """ if not isinstance(headers, MultiMapping): headers_: Union[Mapping[str, str], List[Tuple[str, str]]] = {} if headers: if isinstance(headers, Mapping): headers_ = headers # pyright: ignore else: headers_ = [(key.decode("latin-1"), value.decode("latin-1")) for key, value in headers] super().__init__(CIMultiDict(headers_)) else: super().__init__(headers) self._header_list: Optional[RawHeadersList] = None @classmethod def from_scope(cls, scope: "Scope") -> "Headers": """Create headers from a send-message. Args: scope: The ASGI connection scope. Returns: Headers Raises: ValueError: If the message does not have a ``headers`` key """ connection_state = ScopeState.from_scope(scope) if (headers := connection_state.headers) is Empty: headers = connection_state.headers = cls(scope["headers"]) return headers def to_header_list(self) -> "RawHeadersList": """Raw header value. Returns: A list of tuples contain the header and header-value as bytes """ # Since ``Headers`` are immutable, this can be cached if not self._header_list: self._header_list = _encode_headers((key, value) for key in set(self) for value in self.getall(key)) return self._header_list class MutableScopeHeaders(MutableMapping): """A case-insensitive, multidict-like structure that can be used to mutate headers within a :class:`Scope <.types.Scope>` """ def __init__(self, scope: Optional["HeaderScope"] = None) -> None: """Initialize ``MutableScopeHeaders`` from a ``HeaderScope``. Args: scope: The ASGI connection scope. """ self.headers: RawHeadersList if scope is not None: if not isinstance(scope["headers"], list): scope["headers"] = list(scope["headers"]) self.headers = cast("RawHeadersList", scope["headers"]) else: self.headers = [] @classmethod def from_message(cls, message: "Message") -> "MutableScopeHeaders": """Construct a header from a message object. Args: message: :class:`Message <.types.Message>`. Returns: MutableScopeHeaders. Raises: ValueError: If the message does not have a ``headers`` key. """ if "headers" not in message: raise ValueError(f"Invalid message type: {message['type']!r}") return cls(cast("HeaderScope", message)) def add(self, key: str, value: str) -> None: """Add a header to the scope. Notes: - This method keeps duplicates. Args: key: Header key. value: Header value. Returns: None. """ self.headers.append((key.lower().encode("latin-1"), value.encode("latin-1"))) def getall(self, key: str, default: Optional[List[str]] = None) -> List[str]: """Get all values of a header. Args: key: Header key. default: Default value to return if ``name`` is not found. Returns: A list of strings. Raises: KeyError: if no header for ``name`` was found and ``default`` is not given. """ name = key.lower() values = [ header_value.decode("latin-1") for header_name, header_value in self.headers if header_name.decode("latin-1").lower() == name ] if not values: if default: return default raise KeyError return values def extend_header_value(self, key: str, value: str) -> None: """Extend a multivalued header. Notes: - A multivalues header is a header that can take a comma separated list. - If the header previously did not exist, it will be added. Args: key: Header key. value: Header value to add, Returns: None """ existing = self.get(key) if existing is not None: value = ",".join([*existing.split(","), value]) self[key] = value def __getitem__(self, key: str) -> str: """Get the first header matching ``name``""" name = key.lower() for header in self.headers: if header[0].decode("latin-1").lower() == name: return header[1].decode("latin-1") raise KeyError def _find_indices(self, key: str) -> List[int]: name = key.lower() return [i for i, (name_, _) in enumerate(self.headers) if name_.decode("latin-1").lower() == name] def __setitem__(self, key: str, value: str) -> None: """Set a header in the scope, overwriting duplicates.""" name_encoded = key.lower().encode("latin-1") value_encoded = value.encode("latin-1") if indices := self._find_indices(key): for i in indices[1:]: del self.headers[i] self.headers[indices[0]] = (name_encoded, value_encoded) else: self.headers.append((name_encoded, value_encoded)) def __delitem__(self, key: str) -> None: """Delete all headers matching ``name``""" indices = self._find_indices(key) for i in indices[::-1]: del self.headers[i] def __len__(self) -> int: """Return the length of the internally stored headers, including duplicates.""" return len(self.headers) def __iter__(self) -> Iterator[str]: """Create an iterator of header names including duplicates.""" return iter(h[0].decode("latin-1") for h in self.headers) @dataclass class Header(ABC): """An abstract type for HTTP headers.""" HEADER_NAME: ClassVar[str] = "" documentation_only: bool = False """Defines the header instance as for OpenAPI documentation purpose only.""" @abstractmethod def _get_header_value(self) -> str: """Get the header value as string.""" raise NotImplementedError @classmethod @abstractmethod def from_header(cls, header_value: str) -> "Header": """Construct a header from its string representation.""" def to_header(self, include_header_name: bool = False) -> str: """Get the header as string. Args: include_header_name: should include the header name in the return value. If set to false the return value will only include the header value. if set to true the return value will be: ``
:
``. Defaults to false. """ if not self.HEADER_NAME: raise ImproperlyConfiguredException("Missing header name") return (f"{self.HEADER_NAME}: " if include_header_name else "") + self._get_header_value() @dataclass class CacheControlHeader(Header): """A ``cache-control`` header.""" HEADER_NAME: ClassVar[str] = "cache-control" max_age: Optional[int] = None """Accessor for the ``max-age`` directive.""" s_maxage: Optional[int] = None """Accessor for the ``s-maxage`` directive.""" no_cache: Optional[bool] = None """Accessor for the ``no-cache`` directive.""" no_store: Optional[bool] = None """Accessor for the ``no-store`` directive.""" private: Optional[bool] = None """Accessor for the ``private`` directive.""" public: Optional[bool] = None """Accessor for the ``public`` directive.""" no_transform: Optional[bool] = None """Accessor for the ``no-transform`` directive.""" must_revalidate: Optional[bool] = None """Accessor for the ``must-revalidate`` directive.""" proxy_revalidate: Optional[bool] = None """Accessor for the ``proxy-revalidate`` directive.""" must_understand: Optional[bool] = None """Accessor for the ``must-understand`` directive.""" immutable: Optional[bool] = None """Accessor for the ``immutable`` directive.""" stale_while_revalidate: Optional[int] = None """Accessor for the ``stale-while-revalidate`` directive.""" def _get_header_value(self) -> str: """Get the header value as string.""" cc_items = [ key.replace("_", "-") if isinstance(value, bool) else f"{key.replace('_', '-')}={value}" for key, value in simple_asdict(self, exclude_none=True, exclude={"documentation_only"}).items() ] return ", ".join(cc_items) @classmethod def from_header(cls, header_value: str) -> "CacheControlHeader": """Create a ``CacheControlHeader`` instance from the header value. Args: header_value: the header value as string Returns: An instance of ``CacheControlHeader`` """ kwargs: Dict[str, Any] = {} field_names = {f.name for f in fields(cls)} for cc_item in (stripped for v in header_value.split(",") if (stripped := v.strip())): key, *value = cc_item.split("=", maxsplit=1) key = key.replace("-", "_") if key not in field_names: raise ImproperlyConfiguredException("Invalid cache-control header") if not value: kwargs[key] = True else: (kwargs[key],) = value try: return msgspec.convert(kwargs, CacheControlHeader, strict=False) except msgspec.ValidationError as exc: raise ImproperlyConfiguredException from exc @classmethod def prevent_storing(cls) -> "CacheControlHeader": """Create a ``cache-control`` header with the ``no-store`` directive which indicates that any caches of any kind (private or shared) should not store this response. """ return cls(no_store=True) @dataclass class ETag(Header): """An ``etag`` header.""" HEADER_NAME: ClassVar[str] = "etag" weak: bool = False value: Optional[str] = None # only ASCII characters def _get_header_value(self) -> str: value = f'"{self.value}"' return f"W/{value}" if self.weak else value @classmethod def from_header(cls, header_value: str) -> "ETag": """Construct an ``etag`` header from its string representation. Note that this will unquote etag-values """ match = ETAG_RE.match(header_value) if not match: raise ImproperlyConfiguredException weak, value = match.group(1, 2) try: return cls(weak=bool(weak), value=value) except ValueError as exc: raise ImproperlyConfiguredException from exc def __post_init__(self) -> None: if self.documentation_only is False and self.value is None: raise ValidationException("value must be set if documentation_only is false") if self.value and not PRINTABLE_ASCII_RE.fullmatch(self.value): raise ValidationException("value must only contain ASCII printable characters") class MediaTypeHeader: """A helper class for ``Accept`` header parsing.""" __slots__ = ("_params_str", "maintype", "params", "subtype") def __init__(self, type_str: str) -> None: # preserve the original parameters, because the order might be # changed in the dict self._params_str = "".join(type_str.partition(";")[1:]) full_type, self.params = parse_content_header(type_str) self.maintype, _, self.subtype = full_type.partition("/") def __str__(self) -> str: return f"{self.maintype}/{self.subtype}{self._params_str}" @property def priority(self) -> Tuple[int, int]: # Use fixed point values with two decimals to avoid problems # when comparing float values quality = 100 if "q" in self.params: with suppress(ValueError): quality = int(100 * float(self.params["q"])) if self.maintype == "*": specificity = 0 elif self.subtype == "*": specificity = 1 elif not self.params or ("q" in self.params and len(self.params) == 1): # no params or 'q' is the only one which we ignore specificity = 2 else: specificity = 3 return quality, specificity def match(self, other: "MediaTypeHeader") -> bool: return next( (False for key, value in self.params.items() if key != "q" and value != other.params.get(key)), False if self.subtype != "*" and other.subtype != "*" and self.subtype != other.subtype else self.maintype == "*" or other.maintype == "*" or self.maintype == other.maintype, ) class Accept: """An ``Accept`` header.""" __slots__ = ("_accepted_types",) def __init__(self, accept_value: str) -> None: self._accepted_types = [MediaTypeHeader(t) for t in accept_value.split(",")] self._accepted_types.sort(key=lambda t: t.priority, reverse=True) def __len__(self) -> int: return len(self._accepted_types) def __getitem__(self, key: int) -> str: return str(self._accepted_types[key]) def __iter__(self) -> Iterator[str]: return map(str, self._accepted_types) def best_match(self, provided_types: List[str], default: Optional[str] = None) -> Optional[str]: """Find the best matching media type for the request. Args: provided_types: A list of media types that can be provided as a response. These types can contain a wildcard ``*`` character in the main- or subtype part. default: The media type that is returned if none of the provided types match. Returns: The best matching media type. If the matching provided type contains wildcard characters, they are replaced with the corresponding part of the accepted type. Otherwise the provided type is returned as-is. """ types = [MediaTypeHeader(t) for t in provided_types] for accepted in self._accepted_types: for provided in types: if provided.match(accepted): # Return the accepted type with wildcards replaced # by concrete parts from the provided type result = copy(provided) if result.subtype == "*": result.subtype = accepted.subtype if result.maintype == "*": result.maintype = accepted.maintype return str(result) return default def accepts(self, media_type: str) -> bool: """Check if the request accepts the specified media type. If multiple media types can be provided, it is better to use :func:`best_match`. Args: media_type: The media type to check for. Returns: True if the request accepts ``media_type``. """ return self.best_match([media_type]) == media_type litestar-2.16.0/litestar/datastructures/multi_dicts.py000066400000000000000000000076621500564371300232440ustar00rootroot00000000000000from __future__ import annotations from abc import ABC from typing import TYPE_CHECKING, Any, Generator, Generic, Iterable, Mapping, TypeVar from multidict import MultiDict as BaseMultiDict from multidict import MultiDictProxy, MultiMapping from litestar.datastructures.upload_file import UploadFile if TYPE_CHECKING: from typing_extensions import Self __all__ = ("FormMultiDict", "ImmutableMultiDict", "MultiDict", "MultiMixin") T = TypeVar("T") class MultiMixin(Generic[T], MultiMapping[T], ABC): """Mixin providing common methods for multi dicts, used by :class:`ImmutableMultiDict` and :class:`MultiDict`""" def dict(self) -> dict[str, list[Any]]: """Return the multi-dict as a dict of lists. Returns: A dict of lists """ return {k: self.getall(k) for k in set(self.keys())} def multi_items(self) -> Generator[tuple[str, T], None, None]: """Get all keys and values, including duplicates. Returns: A list of tuples containing key-value pairs """ for key in set(self): for value in self.getall(key): yield key, value class MultiDict(BaseMultiDict[T], MultiMixin[T], Generic[T]): """MultiDict, using :class:`MultiDict `.""" def __init__(self, args: MultiMapping | Mapping[str, T] | Iterable[tuple[str, T]] | None = None) -> None: """Initialize ``MultiDict`` from a`MultiMapping``, :class:`Mapping ` or an iterable of tuples. Args: args: Mapping-like structure to create the ``MultiDict`` from """ super().__init__(args or {}) def immutable(self) -> ImmutableMultiDict[T]: """Create an. :class:`ImmutableMultiDict` view. Returns: An immutable multi dict """ return ImmutableMultiDict[T](self) # pyright: ignore def copy(self) -> Self: """Return a shallow copy""" return type(self)(list(self.multi_items())) class ImmutableMultiDict(MultiDictProxy[T], MultiMixin[T], Generic[T]): """Immutable MultiDict, using class:`MultiDictProxy `.""" def __init__(self, args: MultiMapping | Mapping[str, Any] | Iterable[tuple[str, Any]] | None = None) -> None: """Initialize ``ImmutableMultiDict`` from a `MultiMapping``, :class:`Mapping ` or an iterable of tuples. Args: args: Mapping-like structure to create the ``ImmutableMultiDict`` from """ super().__init__(BaseMultiDict(args or {})) def mutable_copy(self) -> MultiDict[T]: """Create a mutable copy as a :class:`MultiDict` Returns: A mutable multi dict """ return MultiDict(list(self.multi_items())) def copy(self) -> Self: # type: ignore[override] """Return a shallow copy""" return type(self)(self.items()) class FormMultiDict(ImmutableMultiDict[Any]): """MultiDict for form data.""" @classmethod def from_form_data(cls, form_data: dict[str, list[str] | str | UploadFile]) -> FormMultiDict: """Create a FormMultiDict from form data. Args: form_data: Form data to create the FormMultiDict from. Returns: A FormMultiDict instance """ # Convert form_data to a list[tuple[str, str | UploadFile]] before passing it # to FormMultiDict so multi-keys can be accessed properly items = [] for k, v in form_data.items(): if not isinstance(v, list): items.append((k, v)) else: for sv in v: items.append((k, sv)) return cls(items) async def close(self) -> None: """Close all files in the multi-dict. Returns: None """ for _, value in self.multi_items(): if isinstance(value, UploadFile): await value.close() litestar-2.16.0/litestar/datastructures/response_header.py000066400000000000000000000133741500564371300240670ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Any from litestar.exceptions import ImproperlyConfiguredException from litestar.utils import warn_deprecation if TYPE_CHECKING: from litestar.openapi.spec import Example __all__ = ("ResponseHeader",) @dataclass class ResponseHeader: """Container type for a response header.""" name: str """Header name""" documentation_only: bool = False """Defines the ResponseHeader instance as for OpenAPI documentation purpose only.""" value: str | None = None """Value to set for the response header.""" description: str | None = None """A brief description of the parameter. This could contain examples of use. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. """ required: bool = False """Determines whether this parameter is mandatory. If the [parameter location](https://spec.openapis.org/oas/v3.1.0#parameterIn) is `"path"`, this property is **REQUIRED** and its value MUST be `true`. Otherwise, the property MAY be included and its default value is `false`. """ deprecated: bool = False """Specifies that a parameter is deprecated and SHOULD be transitioned out of usage. Default value is `false`. """ allow_empty_value: bool = None # type: ignore[assignment] """Sets the ability to pass empty-valued parameters. This is valid only for `query` parameters and allows sending a parameter with an empty value. Default value is `false`. If. [style](https://spec.openapis.org/oas/v3.1.0#parameterStyle) is used, and if behavior is `n/a` (cannot be serialized), the value of `allowEmptyValue` SHALL be ignored. Use of this property is NOT RECOMMENDED, as it is likely to be removed in a later revision. The rules for serialization of the parameter are specified in one of two ways. For simpler scenarios, a [schema](https://spec.openapis.org/oas/v3.1.0#parameterSchema) and [style](https://spec.openapis.org/oas/v3.1.0#parameterStyle) can describe the structure and syntax of the parameter. """ style: str | None = None """Describes how the parameter value will be serialized depending on the type of the parameter value. Default values (based on value of `in`): - for `query` - `form`; - for `path` - `simple`; - for `header` - `simple`; - for `cookie` - `form`. """ explode: bool | None = None """When this is true, parameter values of type `array` or `object` generate separate parameters for each value of the array or key-value pair of the map. For other types of parameters this property has no effect. When [style](https://spec.openapis.org/oas/v3.1.0#parameterStyle) is `form`, the default value is `true`. For all other styles, the default value is `false`. """ allow_reserved: bool = None # type: ignore[assignment] """Determines whether the parameter value SHOULD allow reserved characters, as defined by. [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.2) `:/?#[]@!$&'()*+,;=` to be included without percent- encoding. This property only applies to parameters with an `in` value of `query`. The default value is `false`. """ example: Any | None = None """Example of the parameter's potential value. The example SHOULD match the specified schema and encoding properties if present. The `example` field is mutually exclusive of the `examples` field. Furthermore, if referencing a `schema` that contains an example, the `example` value SHALL _override_ the example provided by the schema. To represent examples of media types that cannot naturally be represented in JSON or YAML, a string value can contain the example with escaping where necessary. """ examples: dict[str, Example] | None = None """Examples of the parameter's potential value. Each example SHOULD contain a value in the correct format as specified in the parameter encoding. The `examples` field is mutually exclusive of the `example` field. Furthermore, if referencing a `schema` that contains an example, the `examples` value SHALL _override_ the example provided by the schema. For more complex scenarios, the [content](https://spec.openapis.org/oas/v3.1.0#parameterContent) property can define the media type and schema of the parameter. A parameter MUST contain either a `schema` property, or a `content` property, but not both. When `example` or `examples` are provided in conjunction with the `schema` object, the example MUST follow the prescribed serialization strategy for the parameter. """ def __post_init__(self) -> None: """Ensure that either value is set or the instance is for documentation_only.""" if not self.documentation_only and self.value is None: raise ImproperlyConfiguredException("value must be set if documentation_only is false") if self.allow_reserved is None: self.allow_reserved = False # type: ignore[unreachable] else: warn_deprecation( "2.13.1", "allow_reserved", kind="parameter", removal_in="4", info="This property is invalid for headers and will be ignored", ) if self.allow_empty_value is None: self.allow_empty_value = False # type: ignore[unreachable] else: warn_deprecation( "2.13.1", "allow_empty_value", kind="parameter", removal_in="4", info="This property is invalid for headers and will be ignored", ) def __hash__(self) -> int: return hash(self.name) litestar-2.16.0/litestar/datastructures/secret_values.py000066400000000000000000000052161500564371300235610ustar00rootroot00000000000000from __future__ import annotations from abc import ABC, abstractmethod from typing import Generic, TypeVar, Union __all__ = ( "SecretBytes", "SecretString", "SecretT", "SecretValue", ) # NOTE: Union instead of ` | ` is used for compatibility with Sphinx autodoc. # - Sphinx autodoc doesn't handle string annotations for "bound" parameter in TypeVar. # - We cannot use `|` without string annotations due to supporting < python 3.10. SecretT = TypeVar("SecretT", bound=Union[str, bytes]) """Type that represents a secret value of type ``str`` or ``bytes``.""" class SecretValue(ABC, Generic[SecretT]): """Represents a secret value that can be of type `str` or `bytes`.""" def __init__(self, secret_value: SecretT) -> None: """Initializes a :class:`SecretValue` object with a secret value of type ``str`` or ``bytes``. Args: secret_value (str | bytes): The secret value to be encapsulated. """ self._secret_value = secret_value def get_secret(self) -> SecretT: """Returns the actual secret value. Returns: str | bytes: The secret value. """ return self._secret_value @abstractmethod def get_obscured(self) -> SecretT: """Return the hidden representation of the secret value. Raises: NotImplementedError: Always raised to enforce implementation in subclasses. """ raise NotImplementedError("Subclasses must implement get_obscured") def __str__(self) -> str: """Returns a string representation of the hidden secret value. Returns: str: String representation of the hidden secret value. """ return str(self.get_obscured()) def __repr__(self) -> str: """Returns a string representation of the object for debugging purposes. Returns: str: String representation of the object. """ class_name = self.__class__.__name__ return f"{class_name}({self.get_obscured()!r})" class SecretString(SecretValue[str]): """Represents a secret string value.""" def get_obscured(self) -> str: """Overrides the base class method to return the hidden string value. Returns: str: The hidden string representation of the secret value. """ return "******" class SecretBytes(SecretValue[bytes]): """Represents a secret bytes value.""" def get_obscured(self) -> bytes: """Overrides the base class method to return the hidden bytes value. Returns: bytes: The hidden bytes representation of the secret value. """ return b"******" litestar-2.16.0/litestar/datastructures/state.py000066400000000000000000000231141500564371300220320ustar00rootroot00000000000000from __future__ import annotations from copy import deepcopy from threading import RLock from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, Iterator, Mapping, MutableMapping from litestar.utils.scope.state import CONNECTION_STATE_KEY if TYPE_CHECKING: from typing_extensions import Self __all__ = ("ImmutableState", "State") class ImmutableState(Mapping[str, Any]): """An object meant to store arbitrary state. It can be accessed using dot notation while exposing dict like functionalities. """ __slots__ = ( "_deep_copy", "_state", ) _state: dict[str, Any] def __init__( self, state: ImmutableState | Mapping[str, Any] | Iterable[tuple[str, Any]], deep_copy: bool = True ) -> None: """Initialize an ``ImmutableState`` instance. Args: state: An object to initialize the state from. Can be a dict, an instance of :class:`ImmutableState`, or a tuple of key value paris. deep_copy: Whether to 'deepcopy' the passed in state. Examples: .. code-block:: python from litestar.datastructures import ImmutableState state_dict = {"first": 1, "second": 2, "third": 3, "fourth": 4} state = ImmutableState(state_dict) # state implements the Mapping type: assert len(state) == 3 assert "first" in state assert not "fourth" in state assert state["first"] == 1 assert [(k, v) for k, v in state.items()] == [("first", 1), ("second", 2), ("third", 3)] # state implements __bool__ assert state # state is true when it has values. assert not State() # state is empty when it has no values. # it has a 'dict' method to retrieve a shallow copy of the underlying dict inner_dict = state.dict() assert inner_dict == state_dict # you can also retrieve a mutable State by calling 'mutable_copy' mutable_state = state.mutable_copy() del state["first"] assert "first" not in state """ if isinstance(state, ImmutableState): state = state._state if not isinstance(state, dict) and isinstance(state, Iterable): state = dict(state) super().__setattr__("_deep_copy", deep_copy) super().__setattr__("_state", deepcopy(state) if deep_copy else state) def __bool__(self) -> bool: """Return a boolean indicating whether the wrapped dict instance has values.""" return bool(self._state) def __getitem__(self, key: str) -> Any: """Get the value for the corresponding key from the wrapped state object using subscription notation. Args: key: Key to access. Raises: KeyError Returns: A value from the wrapped state instance. """ return self._state[key] def __iter__(self) -> Iterator[str]: """Return an iterator iterating the wrapped state dict. Returns: An iterator of strings """ return iter(self._state) def __len__(self) -> int: """Return length of the wrapped state dict. Returns: An integer """ return len(self._state) def __getattr__(self, key: str) -> Any: """Get the value for the corresponding key from the wrapped state object using attribute notation. Args: key: Key to retrieve Raises: AttributeError: if the given attribute is not set. Returns: The retrieved value """ try: return self._state[key] except KeyError as e: raise AttributeError from e def __copy__(self) -> Self: """Return a shallow copy of the given state object. Customizes how the builtin "copy" function will work. """ return self.__class__(self._state, deep_copy=self._deep_copy) # pyright: ignore def mutable_copy(self) -> State: """Return a mutable copy of the state object. Returns: A ``State`` """ return State(self._state, deep_copy=self._deep_copy) def dict(self) -> dict[str, Any]: """Return a shallow copy of the wrapped dict. Returns: A dict """ return {k: v for k, v in self._state.items() if k != CONNECTION_STATE_KEY} @classmethod def __get_validators__( cls, ) -> Generator[Callable[[ImmutableState | dict[str, Any] | Iterable[tuple[str, Any]]], ImmutableState], None, None]: # type: ignore[valid-type] """Pydantic compatible method to allow custom parsing of state instances in a SignatureModel.""" yield cls.validate @classmethod def validate(cls, value: ImmutableState | dict[str, Any] | Iterable[tuple[str, Any]]) -> Self: # type: ignore[valid-type] """Parse a value and instantiate state inside a SignatureModel. This allows us to use custom subclasses of state, as well as allows users to decide whether state is mutable or immutable. Args: value: The value from which to initialize the state instance. Returns: An ImmutableState instance """ deep_copy = value._deep_copy if isinstance(value, ImmutableState) else False return cls(value, deep_copy=deep_copy) class State(ImmutableState, MutableMapping[str, Any]): """An object meant to store arbitrary state. It can be accessed using dot notation while exposing dict like functionalities. """ __slots__ = ("_lock",) _lock: RLock def __init__( self, state: ImmutableState | Mapping[str, Any] | Iterable[tuple[str, Any]] | None = None, deep_copy: bool = False, ) -> None: """Initialize a ``State`` instance with an optional value. Args: state: An object to initialize the state from. Can be a dict, an instance of 'ImmutableState', or a tuple of key value paris. deep_copy: Whether to 'deepcopy' the passed in state. .. code-block:: python :caption: Examples from litestar.datastructures import State state_dict = {"first": 1, "second": 2, "third": 3, "fourth": 4} state = State(state_dict) # state can be accessed using '.' notation assert state.fourth == 4 del state.fourth # state implements the Mapping type: assert len(state) == 3 assert "first" in state assert not "fourth" in state assert state["first"] == 1 assert [(k, v) for k, v in state.items()] == [("first", 1), ("second", 2), ("third", 3)] state["fourth"] = 4 assert "fourth" in state del state["fourth"] # state implements __bool__ assert state # state is true when it has values. assert not State() # state is empty when it has no values. # it has shallow copy copied_state = state.copy() del copied_state.first assert state.first # it has a 'dict' method to retrieve a shallow copy of the underlying dict inner_dict = state.dict() assert inner_dict == state_dict # you can get an immutable copy of the state by calling 'immutable_immutable_copy' immutable_copy = state.immutable_copy() del immutable_copy.first # raises AttributeError """ super().__init__(state if state is not None else {}, deep_copy=deep_copy) super().__setattr__("_lock", RLock()) def __delitem__(self, key: str) -> None: """Delete the value from the key from the wrapped state object using subscription notation. Args: key: Key to delete Raises: KeyError: if the given attribute is not set. Returns: None """ with self._lock: del self._state[key] def __setitem__(self, key: str, value: Any) -> None: """Set an item in the state using subscription notation. Args: key: Key to set. value: Value to set. Returns: None """ with self._lock: self._state[key] = value def __setattr__(self, key: str, value: Any) -> None: """Set an item in the state using attribute notation. Args: key: Key to set. value: Value to set. Returns: None """ with self._lock: self._state[key] = value def __delattr__(self, key: str) -> None: """Delete the value from the key from the wrapped state object using attribute notation. Args: key: Key to delete Raises: AttributeError: if the given attribute is not set. Returns: None """ try: with self._lock: del self._state[key] except KeyError as e: raise AttributeError from e def copy(self) -> Self: """Return a shallow copy of the state object. Returns: A ``State`` """ return self.__class__(self.dict(), deep_copy=self._deep_copy) # pyright: ignore def immutable_copy(self) -> ImmutableState: """Return a shallow copy of the state object, setting it to be frozen. Returns: A ``State`` """ return ImmutableState(self, deep_copy=self._deep_copy) litestar-2.16.0/litestar/datastructures/upload_file.py000066400000000000000000000055141500564371300232010ustar00rootroot00000000000000# ruff: noqa: SIM115 from __future__ import annotations from tempfile import SpooledTemporaryFile from litestar.concurrency import sync_to_thread from litestar.constants import ONE_MEGABYTE __all__ = ("UploadFile",) class UploadFile: """Representation of a file upload""" __slots__ = ("content_type", "file", "filename", "headers") def __init__( self, content_type: str, filename: str, file_data: bytes | None = None, headers: dict[str, str] | None = None, max_spool_size: int = ONE_MEGABYTE, ) -> None: """Upload file in-memory container. Args: content_type: Content type for the file. filename: The filename. file_data: File data. headers: Any attached headers. max_spool_size: The size above which the temporary file will be rolled to disk. """ self.filename = filename self.content_type = content_type self.file = SpooledTemporaryFile(max_size=max_spool_size) self.headers = headers or {} if file_data: self.file.write(file_data) self.file.seek(0) @property def rolled_to_disk(self) -> bool: """Determine whether the spooled file exceeded the rolled-to-disk threshold and is no longer in memory. Returns: A boolean flag """ return getattr(self.file, "_rolled", False) async def write(self, data: bytes | bytearray) -> int: """Proxy for data writing. Args: data: Byte string to write. Returns: Number of bytes written (int). """ if self.rolled_to_disk: return await sync_to_thread(self.file.write, data) return self.file.write(data) async def read(self, size: int = -1) -> bytes: """Proxy for data reading. Args: size: position from which to read. Returns: Byte string. """ if self.rolled_to_disk: return await sync_to_thread(self.file.read, size) return self.file.read(size) async def seek(self, offset: int) -> int: """Async proxy for file seek. Args: offset: start position.. Returns: The new absolute file position (in bytes) after seeking. """ if self.rolled_to_disk: return await sync_to_thread(self.file.seek, offset) return self.file.seek(offset) async def close(self) -> None: """Async proxy for file close. Returns: None. """ if self.file.closed: return None if self.rolled_to_disk: return await sync_to_thread(self.file.close) return self.file.close() def __repr__(self) -> str: return f"{self.filename} - {self.content_type}" litestar-2.16.0/litestar/datastructures/url.py000066400000000000000000000162671500564371300215270ustar00rootroot00000000000000from __future__ import annotations from functools import lru_cache from typing import TYPE_CHECKING, Any, NamedTuple from urllib.parse import SplitResult, urlencode, urlsplit, urlunsplit from litestar._parsers import parse_query_string from litestar.datastructures import MultiDict from litestar.types import Empty if TYPE_CHECKING: from typing_extensions import Self from litestar.types import EmptyType, Scope __all__ = ("URL", "Address") _DEFAULT_SCHEME_PORTS = {"http": 80, "https": 443, "ftp": 21, "ws": 80, "wss": 443} class Address(NamedTuple): """Just a network address.""" host: str """Address host.""" port: int """Address port.""" def make_absolute_url(path: str | URL, base: str | URL) -> str: """Create an absolute URL. Args: path: URL path to make absolute base: URL to use as a base Returns: A string representing the new, absolute URL """ url = base if isinstance(base, URL) else URL(base) netloc = url.netloc path = url.path.rstrip("/") + str(path) return str(URL.from_components(scheme=url.scheme, netloc=netloc, path=path)) class URL: """Representation and modification utilities of a URL.""" __slots__ = ( "_parsed_url", "_query_params", "fragment", "hostname", "netloc", "password", "path", "port", "query", "scheme", "username", ) _query_params: EmptyType | MultiDict _parsed_url: str | None scheme: str """URL scheme.""" netloc: str """Network location.""" path: str """Hierarchical path.""" fragment: str """Fragment component.""" query: str """Query string.""" username: str | None """Username if specified.""" password: str | None """Password if specified.""" port: int | None """Port if specified.""" hostname: str | None """Hostname if specified.""" def __new__(cls, url: str | SplitResult) -> URL: """Create a new instance. Args: url: url string or split result to represent. """ return cls._new(url=url) @classmethod @lru_cache def _new(cls, url: str | SplitResult) -> URL: instance = super().__new__(cls) instance._parsed_url = None if isinstance(url, str): result = urlsplit(url) instance._parsed_url = url else: result = url instance.scheme = result.scheme instance.netloc = result.netloc instance.path = result.path instance.fragment = result.fragment instance.query = result.query instance.username = result.username instance.password = result.password instance.port = result.port instance.hostname = result.hostname instance._query_params = Empty return instance @property def _url(self) -> str: if not self._parsed_url: self._parsed_url = str( urlunsplit( SplitResult( scheme=self.scheme, netloc=self.netloc, path=self.path, fragment=self.fragment, query=self.query, ) ) ) return self._parsed_url @classmethod @lru_cache def from_components( cls, scheme: str = "", netloc: str = "", path: str = "", fragment: str = "", query: str = "", ) -> Self: """Create a new URL from components. Args: scheme: URL scheme netloc: Network location path: Hierarchical path query: Query component fragment: Fragment identifier Returns: A new URL with the given components """ return cls( SplitResult( scheme=scheme, netloc=netloc, path=path, fragment=fragment, query=query, ) ) @classmethod def from_scope(cls, scope: Scope) -> Self: """Construct a URL from a :class:`Scope <.types.Scope>` Args: scope: A scope Returns: A URL """ scheme = scope.get("scheme", "http") server = scope.get("server") path = scope.get("root_path", "") + scope["path"] query_string = scope.get("query_string", b"") # we use iteration here because it's faster, and headers might not yet be cached host = next( ( header_value.decode("latin-1") for header_name, header_value in scope.get("headers", []) if header_name == b"host" ), "", ) if server and not host: host, port = server default_port = _DEFAULT_SCHEME_PORTS[scheme] if port != default_port: host = f"{host}:{port}" return cls.from_components( scheme=scheme if server else "", query=query_string.decode(), netloc=host, path=path, ) def with_replacements( self, scheme: str = "", netloc: str = "", path: str = "", query: str | MultiDict | None | EmptyType = Empty, fragment: str = "", ) -> Self: """Create a new URL, replacing the given components. Args: scheme: URL scheme netloc: Network location path: Hierarchical path query: Raw query string fragment: Fragment identifier Returns: A new URL with the given components replaced """ if isinstance(query, MultiDict): query = urlencode(query=query) query = (query if query is not Empty else self.query) or "" return type(self).from_components( scheme=scheme or self.scheme, netloc=netloc or self.netloc, path=path or self.path, query=query, fragment=fragment or self.fragment, ) @property def query_params(self) -> MultiDict: """Query parameters of a URL as a :class:`MultiDict <.datastructures.multi_dicts.MultiDict>` Returns: A :class:`MultiDict <.datastructures.multi_dicts.MultiDict>` with query parameters Notes: - The returned ``MultiDict`` is mutable, :class:`URL` itself is *immutable*, therefore mutating the query parameters will not directly mutate the ``URL``. If you want to modify query parameters, make modifications in the multidict and pass them back to :meth:`with_replacements` """ if self._query_params is Empty: self._query_params = MultiDict(parse_query_string(query_string=self.query.encode())) return self._query_params def __str__(self) -> str: return self._url def __eq__(self, other: Any) -> bool: if isinstance(other, (str, URL)): return str(self) == str(other) return NotImplemented # pragma: no cover def __repr__(self) -> str: return f"{type(self).__name__}({self._url!r})" litestar-2.16.0/litestar/di.py000066400000000000000000000102751500564371300162350ustar00rootroot00000000000000from __future__ import annotations from inspect import isasyncgenfunction, isclass, isgeneratorfunction from typing import TYPE_CHECKING, Any from litestar.exceptions import ImproperlyConfiguredException from litestar.types import Empty from litestar.utils import ensure_async_callable from litestar.utils.predicates import is_async_callable from litestar.utils.warnings import ( warn_implicit_sync_to_thread, warn_sync_to_thread_with_async_callable, warn_sync_to_thread_with_generator, ) if TYPE_CHECKING: from litestar._signature import SignatureModel from litestar.types import AnyCallable from litestar.utils.signature import ParsedSignature __all__ = ("Provide",) class Provide: """Wrapper class for dependency injection""" __slots__ = ( "dependency", "has_async_generator_dependency", "has_sync_callable", "has_sync_generator_dependency", "parsed_fn_signature", "signature_model", "sync_to_thread", "use_cache", "value", ) parsed_fn_signature: ParsedSignature signature_model: type[SignatureModel] dependency: AnyCallable def __init__( self, dependency: AnyCallable | type[Any], use_cache: bool = False, sync_to_thread: bool | None = None, ) -> None: """Initialize ``Provide`` Args: dependency: Callable to call or class to instantiate. The result is then injected as a dependency. use_cache: Cache the dependency return value. Defaults to False. sync_to_thread: Run sync code in an async thread. Defaults to False. """ if not callable(dependency): raise ImproperlyConfiguredException("Provider dependency must be a callable value") is_class_dependency = isclass(dependency) self.has_sync_generator_dependency = isgeneratorfunction( dependency if not is_class_dependency else dependency.__call__ # type: ignore[operator] ) self.has_async_generator_dependency = isasyncgenfunction( dependency if not is_class_dependency else dependency.__call__ # type: ignore[operator] ) has_generator_dependency = self.has_sync_generator_dependency or self.has_async_generator_dependency if has_generator_dependency and use_cache: raise ImproperlyConfiguredException( "Cannot cache generator dependency, consider using Lifespan Context instead." ) has_sync_callable = is_class_dependency or not is_async_callable(dependency) # pyright: ignore if sync_to_thread is not None: if has_generator_dependency: warn_sync_to_thread_with_generator(dependency, stacklevel=3) # type: ignore[arg-type] elif not has_sync_callable: warn_sync_to_thread_with_async_callable(dependency, stacklevel=3) # pyright: ignore elif has_sync_callable and not has_generator_dependency: warn_implicit_sync_to_thread(dependency, stacklevel=3) # pyright: ignore if sync_to_thread and has_sync_callable: self.dependency = ensure_async_callable(dependency) # pyright: ignore self.has_sync_callable = False else: self.dependency = dependency # pyright: ignore self.has_sync_callable = has_sync_callable self.sync_to_thread = bool(sync_to_thread) self.use_cache = use_cache self.value: Any = Empty async def __call__(self, **kwargs: Any) -> Any: """Call the provider's dependency.""" if self.use_cache and self.value is not Empty: return self.value if self.has_sync_callable: value = self.dependency(**kwargs) else: value = await self.dependency(**kwargs) if self.use_cache: self.value = value return value def __eq__(self, other: Any) -> bool: # check if memory address is identical, otherwise compare attributes return other is self or ( isinstance(other, self.__class__) and other.dependency == self.dependency and other.use_cache == self.use_cache and other.value == self.value ) litestar-2.16.0/litestar/dto/000077500000000000000000000000001500564371300160505ustar00rootroot00000000000000litestar-2.16.0/litestar/dto/__init__.py000066400000000000000000000007311500564371300201620ustar00rootroot00000000000000from .base_dto import AbstractDTO from .config import DTOConfig from .data_structures import DTOData, DTOFieldDefinition from .dataclass_dto import DataclassDTO from .field import DTOField, Mark, dto_field from .msgspec_dto import MsgspecDTO from .types import RenameStrategy __all__ = ( "AbstractDTO", "DTOConfig", "DTOData", "DTOField", "DTOFieldDefinition", "DataclassDTO", "Mark", "MsgspecDTO", "RenameStrategy", "dto_field", ) litestar-2.16.0/litestar/dto/_backend.py000066400000000000000000001070151500564371300201540ustar00rootroot00000000000000"""DTO backends do the heavy lifting of decoding and validating raw bytes into domain models, and back again, to bytes. """ from __future__ import annotations from dataclasses import replace from typing import ( TYPE_CHECKING, AbstractSet, Any, Callable, ClassVar, Collection, Final, Mapping, Protocol, Union, cast, ) import msgspec from msgspec import UNSET, Struct, UnsetType, convert, defstruct, field from typing_extensions import Annotated from litestar.dto._types import ( CollectionType, CompositeType, MappingType, NestedFieldInfo, SimpleType, TransferDTOFieldDefinition, TransferType, TupleType, UnionType, ) from litestar.dto.data_structures import DTOData, DTOFieldDefinition from litestar.dto.field import Mark from litestar.enums import RequestEncodingType from litestar.params import KwargDefinition from litestar.serialization import decode_json, decode_msgpack from litestar.types import Empty from litestar.typing import FieldDefinition from litestar.utils import unique_name_for_scope if TYPE_CHECKING: from litestar.connection import ASGIConnection from litestar.dto import AbstractDTO, RenameStrategy from litestar.types.serialization import LitestarEncodableType __all__ = ("DTOBackend",) class CompositeTypeHandler(Protocol): def __call__( self, field_definition: FieldDefinition, exclude: AbstractSet[str], include: AbstractSet[str], rename_fields: dict[str, str], unique_name: str, nested_depth: int, ) -> CompositeType: ... class DTOBackend: __slots__ = ( "annotation", "attribute_accessor", "dto_data_type", "dto_factory", "field_definition", "handler_id", "is_data_field", "model_type", "parsed_field_definitions", "reverse_name_map", "transfer_model_type", "wrapper_attribute_name", ) _seen_model_names: ClassVar[set[str]] = set() def __init__( self, dto_factory: type[AbstractDTO], field_definition: FieldDefinition, handler_id: str, is_data_field: bool, model_type: type[Any], wrapper_attribute_name: str | None, ) -> None: """Create dto backend instance. Args: dto_factory: The DTO factory class calling this backend. field_definition: Parsed type. handler_id: The name of the handler that this backend is for. is_data_field: Whether the field is a subclass of DTOData. model_type: Model type. wrapper_attribute_name: If the data that DTO should operate upon is wrapped in a generic datastructure, this is the name of the attribute that the data is stored in. """ self.dto_factory: Final[type[AbstractDTO]] = dto_factory self.field_definition: Final[FieldDefinition] = field_definition self.is_data_field: Final[bool] = is_data_field self.handler_id: Final[str] = handler_id self.model_type: Final[type[Any]] = model_type self.wrapper_attribute_name: Final[str | None] = wrapper_attribute_name self.attribute_accessor = dto_factory.attribute_accessor self.parsed_field_definitions = self.parse_model( model_type=model_type, exclude=self.dto_factory.config.exclude, include=self.dto_factory.config.include, rename_fields=self.dto_factory.config.rename_fields, ) self.transfer_model_type = self.create_transfer_model_type( model_name=model_type.__name__, field_definitions=self.parsed_field_definitions ) self.dto_data_type: type[DTOData] | None = None if field_definition.is_subclass_of(DTOData): self.dto_data_type = field_definition.annotation field_definition = self.field_definition.inner_types[0] self.annotation = build_annotation_for_backend(model_type, field_definition, self.transfer_model_type) def parse_model( self, model_type: Any, exclude: AbstractSet[str], include: AbstractSet[str], rename_fields: dict[str, str], nested_depth: int = 0, ) -> tuple[TransferDTOFieldDefinition, ...]: """Reduce :attr:`model_type` to a tuple :class:`TransferDTOFieldDefinition` instances. Returns: Fields for data transfer. """ defined_fields = [] generic_field_definitions = list(FieldDefinition.from_annotation(model_type).generic_types or ()) for field_definition in self.dto_factory.generate_field_definitions(model_type): if field_definition.is_type_var: base_arg_field = generic_field_definitions.pop() field_definition = replace( field_definition, annotation=base_arg_field.annotation, raw=base_arg_field.raw ) if _should_mark_private(field_definition, self.dto_factory.config.underscore_fields_private): field_definition.dto_field.mark = Mark.PRIVATE try: transfer_type = self._create_transfer_type( field_definition=field_definition, exclude=exclude, include=include, rename_fields=rename_fields, field_name=field_definition.name, unique_name=field_definition.model_name, nested_depth=nested_depth, ) except RecursionError: continue transfer_field_definition = TransferDTOFieldDefinition.from_dto_field_definition( field_definition=field_definition, serialization_name=rename_fields.get(field_definition.name), transfer_type=transfer_type, is_partial=self.dto_factory.config.partial, is_excluded=_should_exclude_field( field_definition=field_definition, exclude=exclude, include=include, is_data_field=self.is_data_field, ), ) defined_fields.append(transfer_field_definition) return tuple(defined_fields) def _create_transfer_model_name(self, model_name: str) -> str: long_name_prefix = self.handler_id.split("::")[0] short_name_prefix = _camelize(long_name_prefix.split(".")[-1], True) name_suffix = "RequestBody" if self.is_data_field else "ResponseBody" if (short_name := f"{short_name_prefix}{model_name}{name_suffix}") not in self._seen_model_names: name = short_name elif (long_name := f"{long_name_prefix}{model_name}{name_suffix}") not in self._seen_model_names: name = long_name else: name = unique_name_for_scope(long_name, self._seen_model_names) self._seen_model_names.add(name) return name def create_transfer_model_type( self, model_name: str, field_definitions: tuple[TransferDTOFieldDefinition, ...], ) -> type[Struct]: """Create a model for data transfer. Args: model_name: name for the type that should be unique across all transfer types. field_definitions: field definitions for the container type. Returns: A ``BackendT`` class. """ struct_name = self._create_transfer_model_name(model_name) struct = _create_struct_for_field_definitions( model_name=struct_name, field_definitions=field_definitions, rename_strategy=self.dto_factory.config.rename_strategy, forbid_unknown_fields=self.dto_factory.config.forbid_unknown_fields, ) setattr(struct, "__schema_name__", struct_name) return struct def parse_raw(self, raw: bytes, asgi_connection: ASGIConnection) -> Struct | Collection[Struct]: """Parse raw bytes into transfer model type. Args: raw: bytes asgi_connection: The current ASGI Connection Returns: The raw bytes parsed into transfer model type. """ request_encoding = RequestEncodingType.JSON if (content_type := getattr(asgi_connection, "content_type", None)) and (media_type := content_type[0]): request_encoding = media_type type_decoders = asgi_connection.route_handler.resolve_type_decoders() if request_encoding == RequestEncodingType.MESSAGEPACK: result = decode_msgpack(value=raw, target_type=self.annotation, type_decoders=type_decoders, strict=False) else: result = decode_json(value=raw, target_type=self.annotation, type_decoders=type_decoders, strict=False) return cast("Struct | Collection[Struct]", result) def parse_builtins(self, builtins: Any, asgi_connection: ASGIConnection) -> Any: """Parse builtin types into transfer model type. Args: builtins: Builtin type. asgi_connection: The current ASGI Connection Returns: The builtin type parsed into transfer model type. """ return convert( obj=builtins, type=self.annotation, dec_hook=asgi_connection.route_handler.default_deserializer, strict=False, str_keys=True, ) def populate_data_from_builtins(self, builtins: Any, asgi_connection: ASGIConnection) -> Any: """Populate model instance from builtin types. Args: builtins: Builtin type. asgi_connection: The current ASGI Connection Returns: Instance or collection of ``model_type`` instances. """ if self.dto_data_type: return self.dto_data_type( backend=self, data_as_builtins=_transfer_data( destination_type=dict, source_data=self.parse_builtins(builtins, asgi_connection), field_definitions=self.parsed_field_definitions, field_definition=self.field_definition, is_data_field=self.is_data_field, attribute_accessor=self.attribute_accessor, ), ) return self.transfer_data_from_builtins(self.parse_builtins(builtins, asgi_connection)) def transfer_data_from_builtins(self, builtins: Any) -> Any: """Populate model instance from builtin types. Args: builtins: Builtin type. Returns: Instance or collection of ``model_type`` instances. """ return _transfer_data( destination_type=self.model_type, source_data=builtins, field_definitions=self.parsed_field_definitions, field_definition=self.field_definition, is_data_field=self.is_data_field, attribute_accessor=self.attribute_accessor, ) def populate_data_from_raw(self, raw: bytes, asgi_connection: ASGIConnection) -> Any: """Parse raw bytes into instance of `model_type`. Args: raw: bytes asgi_connection: The current ASGI Connection Returns: Instance or collection of ``model_type`` instances. """ if self.dto_data_type: return self.dto_data_type( backend=self, data_as_builtins=_transfer_data( destination_type=dict, source_data=self.parse_raw(raw, asgi_connection), field_definitions=self.parsed_field_definitions, field_definition=self.field_definition, is_data_field=self.is_data_field, attribute_accessor=self.attribute_accessor, ), ) return _transfer_data( destination_type=self.model_type, source_data=self.parse_raw(raw, asgi_connection), field_definitions=self.parsed_field_definitions, field_definition=self.field_definition, is_data_field=self.is_data_field, attribute_accessor=self.attribute_accessor, ) def encode_data(self, data: Any) -> LitestarEncodableType: """Encode data into a ``LitestarEncodableType``. Args: data: Data to encode. Returns: Encoded data. """ if self.wrapper_attribute_name: wrapped_transfer = _transfer_data( destination_type=self.transfer_model_type, source_data=self.attribute_accessor(data, self.wrapper_attribute_name), field_definitions=self.parsed_field_definitions, field_definition=self.field_definition, is_data_field=self.is_data_field, attribute_accessor=self.attribute_accessor, ) setattr( data, self.wrapper_attribute_name, wrapped_transfer, ) return cast("LitestarEncodableType", data) return cast( "LitestarEncodableType", _transfer_data( destination_type=self.transfer_model_type, source_data=data, field_definitions=self.parsed_field_definitions, field_definition=self.field_definition, is_data_field=self.is_data_field, attribute_accessor=self.attribute_accessor, ), ) def _get_handler_for_field_definition(self, field_definition: FieldDefinition) -> CompositeTypeHandler | None: if field_definition.is_union: return self._create_union_type if field_definition.is_tuple: if len(field_definition.inner_types) == 2 and field_definition.inner_types[1].annotation is Ellipsis: return self._create_collection_type return self._create_tuple_type if field_definition.is_mapping: return self._create_mapping_type if field_definition.is_non_string_collection: return self._create_collection_type return None def _create_transfer_type( self, field_definition: FieldDefinition, exclude: AbstractSet[str], include: AbstractSet[str], rename_fields: dict[str, str], field_name: str, unique_name: str, nested_depth: int, ) -> CompositeType | SimpleType: exclude = _filter_nested_field(exclude, field_name) include = _filter_nested_field(include, field_name) rename_fields = _filter_nested_field_mapping(rename_fields, field_name) if composite_type_handler := self._get_handler_for_field_definition(field_definition): return composite_type_handler( field_definition=field_definition, exclude=exclude, include=include, rename_fields=rename_fields, unique_name=unique_name, nested_depth=nested_depth, ) transfer_model: NestedFieldInfo | None = None if self.dto_factory.detect_nested_field(field_definition): if nested_depth == self.dto_factory.config.max_nested_depth: raise RecursionError unique_name = f"{unique_name}{field_definition.raw.__name__}" nested_field_definitions = self.parse_model( model_type=field_definition.annotation, exclude=exclude, include=include, rename_fields=rename_fields, nested_depth=nested_depth + 1, ) transfer_model = NestedFieldInfo( model=self.create_transfer_model_type(unique_name, nested_field_definitions), field_definitions=nested_field_definitions, ) return SimpleType(field_definition, nested_field_info=transfer_model) def _create_collection_type( self, field_definition: FieldDefinition, exclude: AbstractSet[str], include: AbstractSet[str], rename_fields: dict[str, str], unique_name: str, nested_depth: int, ) -> CollectionType: inner_types = field_definition.inner_types inner_type = self._create_transfer_type( field_definition=inner_types[0] if inner_types else FieldDefinition.from_annotation(Any), exclude=exclude, include=include, field_name="0", unique_name=f"{unique_name}_0", nested_depth=nested_depth, rename_fields=rename_fields, ) return CollectionType( field_definition=field_definition, inner_type=inner_type, has_nested=inner_type.has_nested ) def _create_mapping_type( self, field_definition: FieldDefinition, exclude: AbstractSet[str], include: AbstractSet[str], rename_fields: dict[str, str], unique_name: str, nested_depth: int, ) -> MappingType: inner_types = field_definition.inner_types key_type = self._create_transfer_type( field_definition=inner_types[0] if inner_types else FieldDefinition.from_annotation(Any), exclude=exclude, include=include, field_name="0", unique_name=f"{unique_name}_0", nested_depth=nested_depth, rename_fields=rename_fields, ) value_type = self._create_transfer_type( field_definition=inner_types[1] if inner_types else FieldDefinition.from_annotation(Any), exclude=exclude, include=include, field_name="1", unique_name=f"{unique_name}_1", nested_depth=nested_depth, rename_fields=rename_fields, ) return MappingType( field_definition=field_definition, key_type=key_type, value_type=value_type, has_nested=key_type.has_nested or value_type.has_nested, ) def _create_tuple_type( self, field_definition: FieldDefinition, exclude: AbstractSet[str], include: AbstractSet[str], rename_fields: dict[str, str], unique_name: str, nested_depth: int, ) -> TupleType: inner_types = tuple( self._create_transfer_type( field_definition=inner_type, exclude=exclude, include=include, field_name=str(i), unique_name=f"{unique_name}_{i}", nested_depth=nested_depth, rename_fields=rename_fields, ) for i, inner_type in enumerate(field_definition.inner_types) ) return TupleType( field_definition=field_definition, inner_types=inner_types, has_nested=any(t.has_nested for t in inner_types), ) def _create_union_type( self, field_definition: FieldDefinition, exclude: AbstractSet[str], include: AbstractSet[str], rename_fields: dict[str, str], unique_name: str, nested_depth: int, ) -> UnionType: inner_types = tuple( self._create_transfer_type( field_definition=inner_type, exclude=exclude, include=include, field_name=str(i), unique_name=f"{unique_name}_{i}", nested_depth=nested_depth, rename_fields=rename_fields, ) for i, inner_type in enumerate(field_definition.inner_types) ) return UnionType( field_definition=field_definition, inner_types=inner_types, has_nested=any(t.has_nested for t in inner_types), ) def _camelize(value: str, capitalize_first_letter: bool) -> str: return "".join( word if index == 0 and not capitalize_first_letter else word.capitalize() for index, word in enumerate(value.split("_")) ) def _filter_nested_field(field_name_set: AbstractSet[str], field_name: str) -> AbstractSet[str]: """Filter a nested field name.""" return {split[1] for s in field_name_set if (split := s.split(".", 1))[0] == field_name and len(split) > 1} def _filter_nested_field_mapping(field_name_mapping: Mapping[str, str], field_name: str) -> dict[str, str]: """Filter a nested field name.""" return { split[1]: v for s, v in field_name_mapping.items() if (split := s.split(".", 1))[0] == field_name and len(split) > 1 } def _transfer_data( destination_type: type[Any], source_data: Any | Collection[Any], field_definitions: tuple[TransferDTOFieldDefinition, ...], field_definition: FieldDefinition, is_data_field: bool, attribute_accessor: Callable[[object, str], Any], ) -> Any: """Create instance or iterable of instances of ``destination_type``. Args: destination_type: the model type received by the DTO on type narrowing. source_data: data that has been parsed and validated via the backend. field_definitions: model field definitions. field_definition: the parsed type that represents the handler annotation for which the DTO is being applied. is_data_field: whether the DTO is being applied to a ``data`` field. attribute_accessor: 'getattr'-like function to access attributes on the data source Returns: Data parsed into ``destination_type``. """ if field_definition.is_non_string_collection: if not field_definition.is_mapping: return field_definition.instantiable_origin( _transfer_data( destination_type=destination_type, source_data=item, field_definitions=field_definitions, field_definition=field_definition.inner_types[0], is_data_field=is_data_field, attribute_accessor=attribute_accessor, ) for item in source_data ) return field_definition.instantiable_origin( ( key, _transfer_data( destination_type=destination_type, source_data=value, field_definitions=field_definitions, field_definition=field_definition.inner_types[1], is_data_field=is_data_field, attribute_accessor=attribute_accessor, ), ) for key, value in source_data.items() # type: ignore[union-attr] ) return _transfer_instance_data( destination_type=destination_type, source_instance=source_data, field_definitions=field_definitions, is_data_field=is_data_field, attribute_accessor=attribute_accessor, ) def _transfer_instance_data( destination_type: type[Any], source_instance: Any, field_definitions: tuple[TransferDTOFieldDefinition, ...], is_data_field: bool, attribute_accessor: Callable[[object, str], Any], ) -> Any: """Create instance of ``destination_type`` with data from ``source_instance``. Args: destination_type: the model type received by the DTO on type narrowing. source_instance: primitive data that has been parsed and validated via the backend. field_definitions: model field definitions. is_data_field: whether the given field is a 'data' kwarg field. attribute_accessor: 'getattr'-like function to access attributes on the data source Returns: Data parsed into ``model_type``. """ unstructured_data = {} for field_definition in field_definitions: if not is_data_field: if field_definition.is_excluded: continue elif not ( field_definition.name in source_instance if isinstance(source_instance, Mapping) else hasattr(source_instance, field_definition.name) ): continue transfer_type = field_definition.transfer_type source_value = ( source_instance[field_definition.name] if isinstance(source_instance, Mapping) else attribute_accessor(source_instance, field_definition.name) ) if field_definition.is_partial and is_data_field and source_value is UNSET: continue unstructured_data[field_definition.name] = _transfer_type_data( source_value=source_value, transfer_type=transfer_type, nested_as_dict=destination_type is dict, is_data_field=is_data_field, attribute_accessor=attribute_accessor, ) return destination_type(**unstructured_data) def _transfer_type_data( source_value: Any, transfer_type: TransferType, nested_as_dict: bool, is_data_field: bool, attribute_accessor: Callable[[object, str], Any], ) -> Any: if isinstance(transfer_type, SimpleType) and transfer_type.nested_field_info: if nested_as_dict: destination_type: Any = dict elif is_data_field: destination_type = transfer_type.field_definition.annotation else: destination_type = transfer_type.nested_field_info.model return _transfer_instance_data( destination_type=destination_type, source_instance=source_value, field_definitions=transfer_type.nested_field_info.field_definitions, is_data_field=is_data_field, attribute_accessor=attribute_accessor, ) if isinstance(transfer_type, UnionType) and transfer_type.has_nested: return _transfer_nested_union_type_data( transfer_type=transfer_type, source_value=source_value, is_data_field=is_data_field, attribute_accessor=attribute_accessor, ) if isinstance(transfer_type, CollectionType): if transfer_type.has_nested: return transfer_type.field_definition.instantiable_origin( _transfer_type_data( source_value=item, transfer_type=transfer_type.inner_type, nested_as_dict=False, is_data_field=is_data_field, attribute_accessor=attribute_accessor, ) for item in source_value ) return transfer_type.field_definition.instantiable_origin(source_value) if isinstance(transfer_type, MappingType): if transfer_type.has_nested: return transfer_type.field_definition.instantiable_origin( ( key, _transfer_type_data( source_value=value, transfer_type=transfer_type.value_type, nested_as_dict=False, is_data_field=is_data_field, attribute_accessor=attribute_accessor, ), ) for key, value in source_value.items() ) return transfer_type.field_definition.instantiable_origin(source_value) return source_value def _transfer_nested_union_type_data( transfer_type: UnionType, source_value: Any, is_data_field: bool, attribute_accessor: Callable[[object, str], Any], ) -> Any: for inner_type in transfer_type.inner_types: if isinstance(inner_type, CompositeType): raise RuntimeError("Composite inner types not (yet) supported for nested unions.") if inner_type.nested_field_info and isinstance( source_value, inner_type.nested_field_info.model if is_data_field else inner_type.field_definition.annotation, ): return _transfer_instance_data( destination_type=inner_type.field_definition.annotation if is_data_field else inner_type.nested_field_info.model, source_instance=source_value, field_definitions=inner_type.nested_field_info.field_definitions, is_data_field=is_data_field, attribute_accessor=attribute_accessor, ) return source_value def _create_msgspec_field(field_definition: TransferDTOFieldDefinition) -> Any: kwargs: dict[str, Any] = {} if field_definition.is_partial: kwargs["default"] = UNSET elif field_definition.default is not Empty: kwargs["default"] = field_definition.default elif field_definition.default_factory is not None: kwargs["default_factory"] = field_definition.default_factory if field_definition.serialization_name is not None: kwargs["name"] = field_definition.serialization_name return field(**kwargs) def _create_struct_field_meta_for_field_definition(field_definition: TransferDTOFieldDefinition) -> msgspec.Meta | None: if (kwarg_definition := field_definition.kwarg_definition) is None or not isinstance( kwarg_definition, KwargDefinition ): return None return msgspec.Meta( description=kwarg_definition.description, examples=[e.value for e in kwarg_definition.examples or []], ge=kwarg_definition.ge, gt=kwarg_definition.gt, le=kwarg_definition.le, lt=kwarg_definition.lt, max_length=kwarg_definition.max_length if not field_definition.is_partial else None, min_length=kwarg_definition.min_length if not field_definition.is_partial else None, multiple_of=kwarg_definition.multiple_of, pattern=kwarg_definition.pattern, title=kwarg_definition.title, ) def _create_struct_for_field_definitions( *, model_name: str, field_definitions: tuple[TransferDTOFieldDefinition, ...], rename_strategy: RenameStrategy | dict[str, str] | None, forbid_unknown_fields: bool, ) -> type[Struct]: struct_fields: list[tuple[str, type] | tuple[str, type, type]] = [] for field_definition in field_definitions: if field_definition.is_excluded: continue field_type = _create_transfer_model_type_annotation(field_definition.transfer_type) if field_definition.is_partial: field_type = Union[field_type, UnsetType] if field_definition.passthrough_constraints: if (field_meta := _create_struct_field_meta_for_field_definition(field_definition)) is not None: field_type = Annotated[field_type, field_meta] elif field_definition.kwarg_definition: field_type = Annotated[field_type, field_definition.kwarg_definition] struct_fields.append( ( field_definition.name, field_type, _create_msgspec_field(field_definition), ) ) return defstruct( model_name, struct_fields, frozen=True, kw_only=True, rename=rename_strategy, forbid_unknown_fields=forbid_unknown_fields, ) def build_annotation_for_backend( model_type: type[Any], field_definition: FieldDefinition, transfer_model: type[Struct] ) -> Any: """A helper to re-build a generic outer type with new inner type. Args: model_type: The original model type. field_definition: The parsed type that represents the handler annotation for which the DTO is being applied. transfer_model: The transfer model generated to represent the model type. Returns: Annotation with new inner type if applicable. """ if not field_definition.inner_types: if field_definition.is_subclass_of(model_type): return transfer_model return field_definition.annotation inner_types = tuple( build_annotation_for_backend(model_type, inner_type, transfer_model) for inner_type in field_definition.inner_types ) return field_definition.safe_generic_origin[inner_types] def _should_mark_private(field_definition: DTOFieldDefinition, underscore_fields_private: bool) -> bool: """Returns ``True`` where a field should be marked as private. Fields should be marked as private when: - the ``underscore_fields_private`` flag is set. - the field is not already marked. - the field name is prefixed with an underscore Args: field_definition: defined DTO field underscore_fields_private: whether fields prefixed with an underscore should be marked as private. """ return bool( underscore_fields_private and field_definition.dto_field.mark is None and field_definition.name.startswith("_") ) def _should_exclude_field( field_definition: DTOFieldDefinition, exclude: AbstractSet[str], include: AbstractSet[str], is_data_field: bool ) -> bool: """Returns ``True`` where a field should be excluded from data transfer. Args: field_definition: defined DTO field exclude: names of fields to exclude include: names of fields to exclude is_data_field: whether the field is a data field Returns: ``True`` if the field should not be included in any data transfer. """ field_name = field_definition.name if field_name in exclude: return True if include and field_name not in include and not (any(f.startswith(f"{field_name}.") for f in include)): return True if field_definition.dto_field.mark is Mark.PRIVATE: return True if is_data_field and field_definition.dto_field.mark is Mark.READ_ONLY: return True return not is_data_field and field_definition.dto_field.mark is Mark.WRITE_ONLY def _create_transfer_model_type_annotation(transfer_type: TransferType) -> Any: """Create a type annotation for a transfer model. Uses the parsed type that originates from the data model and the transfer model generated to represent a nested type to reconstruct the type annotation for the transfer model. """ if isinstance(transfer_type, SimpleType): if transfer_type.nested_field_info: return transfer_type.nested_field_info.model return transfer_type.field_definition.annotation if isinstance(transfer_type, CollectionType): return _create_transfer_model_collection_type(transfer_type) if isinstance(transfer_type, MappingType): return _create_transfer_model_mapping_type(transfer_type) if isinstance(transfer_type, TupleType): return _create_transfer_model_tuple_type(transfer_type) if isinstance(transfer_type, UnionType): return _create_transfer_model_union_type(transfer_type) raise RuntimeError(f"Unexpected transfer type: {type(transfer_type)}") def _create_transfer_model_collection_type(transfer_type: CollectionType) -> Any: generic_collection_type = transfer_type.field_definition.safe_generic_origin inner_type = _create_transfer_model_type_annotation(transfer_type.inner_type) if transfer_type.field_definition.origin is tuple: return generic_collection_type[inner_type, ...] return generic_collection_type[inner_type] def _create_transfer_model_tuple_type(transfer_type: TupleType) -> Any: inner_types = tuple(_create_transfer_model_type_annotation(t) for t in transfer_type.inner_types) return transfer_type.field_definition.safe_generic_origin[inner_types] def _create_transfer_model_union_type(transfer_type: UnionType) -> Any: inner_types = tuple(_create_transfer_model_type_annotation(t) for t in transfer_type.inner_types) return transfer_type.field_definition.safe_generic_origin[inner_types] def _create_transfer_model_mapping_type(transfer_type: MappingType) -> Any: key_type = _create_transfer_model_type_annotation(transfer_type.key_type) value_type = _create_transfer_model_type_annotation(transfer_type.value_type) return transfer_type.field_definition.safe_generic_origin[key_type, value_type] litestar-2.16.0/litestar/dto/_codegen_backend.py000066400000000000000000000607331500564371300216450ustar00rootroot00000000000000"""DTO backends do the heavy lifting of decoding and validating raw bytes into domain models, and back again, to bytes. """ from __future__ import annotations import linecache import re import secrets import textwrap from contextlib import contextmanager, nullcontext from typing import ( TYPE_CHECKING, Any, Callable, ContextManager, Generator, Mapping, Protocol, cast, ) from msgspec import UNSET from litestar.dto._backend import DTOBackend from litestar.dto._types import ( CollectionType, CompositeType, MappingType, SimpleType, TransferDTOFieldDefinition, TransferType, UnionType, ) from litestar.utils.helpers import unique_name_for_scope if TYPE_CHECKING: from litestar.connection import ASGIConnection from litestar.dto import AbstractDTO from litestar.types.serialization import LitestarEncodableType from litestar.typing import FieldDefinition __all__ = ("DTOCodegenBackend",) class DTOCodegenBackend(DTOBackend): __slots__ = ( "_encode_data", "_transfer_to_dict", "_transfer_to_model_type", ) def __init__( self, dto_factory: type[AbstractDTO], field_definition: FieldDefinition, handler_id: str, is_data_field: bool, model_type: type[Any], wrapper_attribute_name: str | None, ) -> None: """Create dto backend instance. Args: dto_factory: The DTO factory class calling this backend. field_definition: Parsed type. handler_id: The name of the handler that this backend is for. is_data_field: Whether the field is a subclass of DTOData. model_type: Model type. wrapper_attribute_name: If the data that DTO should operate upon is wrapped in a generic datastructure, this is the name of the attribute that the data is stored in. """ super().__init__( dto_factory=dto_factory, field_definition=field_definition, handler_id=handler_id, is_data_field=is_data_field, model_type=model_type, wrapper_attribute_name=wrapper_attribute_name, ) self._transfer_to_dict = self._create_transfer_data_fn( destination_type=dict, field_definition=self.field_definition, ) self._transfer_to_model_type = self._create_transfer_data_fn( destination_type=self.model_type, field_definition=self.field_definition, ) self._encode_data = self._create_transfer_data_fn( destination_type=self.transfer_model_type, field_definition=self.field_definition, ) def populate_data_from_builtins(self, builtins: Any, asgi_connection: ASGIConnection) -> Any: """Populate model instance from builtin types. Args: builtins: Builtin type. asgi_connection: The current ASGI Connection Returns: Instance or collection of ``model_type`` instances. """ if self.dto_data_type: return self.dto_data_type( backend=self, data_as_builtins=self._transfer_to_dict(self.parse_builtins(builtins, asgi_connection)), ) return self.transfer_data_from_builtins(self.parse_builtins(builtins, asgi_connection)) def transfer_data_from_builtins(self, builtins: Any) -> Any: """Populate model instance from builtin types. Args: builtins: Builtin type. Returns: Instance or collection of ``model_type`` instances. """ return self._transfer_to_model_type(builtins) def populate_data_from_raw(self, raw: bytes, asgi_connection: ASGIConnection) -> Any: """Parse raw bytes into instance of `model_type`. Args: raw: bytes asgi_connection: The current ASGI Connection Returns: Instance or collection of ``model_type`` instances. """ if self.dto_data_type: return self.dto_data_type( backend=self, data_as_builtins=self._transfer_to_dict(self.parse_raw(raw, asgi_connection)), ) return self._transfer_to_model_type(self.parse_raw(raw, asgi_connection)) def encode_data(self, data: Any) -> LitestarEncodableType: """Encode data into a ``LitestarEncodableType``. Args: data: Data to encode. Returns: Encoded data. """ if self.wrapper_attribute_name: wrapped_transfer = self._encode_data(getattr(data, self.wrapper_attribute_name)) setattr(data, self.wrapper_attribute_name, wrapped_transfer) return cast("LitestarEncodableType", data) return cast("LitestarEncodableType", self._encode_data(data)) def _create_transfer_data_fn( self, destination_type: type[Any], field_definition: FieldDefinition, ) -> Any: """Create instance or iterable of instances of ``destination_type``. Args: destination_type: the model type received by the DTO on type narrowing. field_definition: the parsed type that represents the handler annotation for which the DTO is being applied. Returns: Data parsed into ``destination_type``. """ return TransferFunctionFactory.create_transfer_data( destination_type=destination_type, field_definitions=self.parsed_field_definitions, is_data_field=self.is_data_field, field_definition=field_definition, attribute_accessor=self.attribute_accessor, ) class FieldAccessManager(Protocol): def __call__(self, source_name: str, field_name: str, expect_optional: bool) -> ContextManager[str]: ... class TransferFunctionFactory: def __init__( self, is_data_field: bool, nested_as_dict: bool, attribute_accessor: Callable[[object, str], Any], ) -> None: self.attribute_accessor = attribute_accessor self.is_data_field = is_data_field self._fn_locals: dict[str, Any] = { "Mapping": Mapping, "UNSET": UNSET, } if attribute_accessor is not getattr: self.attribute_accessor_name: str | None = self._add_to_fn_globals("__getattr_impl", attribute_accessor) else: self.attribute_accessor_name = None self._indentation = 1 self._body = "" self.names: set[str] = set() self.nested_as_dict = nested_as_dict self._re_index_access = re.compile(r"\[['\"](\w+?)['\"]]") def _add_to_fn_globals(self, name: str, value: Any) -> str: unique_name = unique_name_for_scope(name, self._fn_locals) self._fn_locals[unique_name] = value return unique_name def _create_local_name(self, name: str) -> str: unique_name = unique_name_for_scope(name, self.names) self.names.add(unique_name) return unique_name def _make_function( self, source_value_name: str, return_value_name: str, fn_name: str = "func", ) -> Callable[[Any], Any]: """Wrap the current body contents in a function definition and turn it into a callable object""" source = f"def {fn_name}({source_value_name}):\n{self._body} return {return_value_name}" ctx: dict[str, Any] = {**self._fn_locals} # add the function to linecache, to get better stacktraces when an error occurs # otherwise, the traceback within the generated code will just point # to '' file_name = f"dto_transfer_function_{secrets.token_hex(6)}" linecache.cache[file_name] = ( len(source), None, # mtime: not applicable [line + "\n" for line in source.splitlines()], file_name, ) code = compile(source, file_name, "exec") exec(code, ctx) # noqa: S102 return ctx["func"] # type: ignore[no-any-return] def _add_stmt(self, stmt: str) -> None: self._body += textwrap.indent(stmt + "\n", " " * self._indentation) @contextmanager def _start_block(self, expr: str | None = None) -> Generator[None, None, None]: """Start an indented block. If `expr` is given, use it as the "opening line" of the block. """ if expr is not None: self._add_stmt(expr) self._indentation += 1 yield self._indentation -= 1 @contextmanager def _try_except_pass(self, exception: str) -> Generator[None, None, None]: """Enter a `try / except / pass` block. Content written while inside this context will go into the `try` block. """ with self._start_block("try:"): yield with self._start_block(expr=f"except {exception}:"): self._add_stmt("pass") @contextmanager def _access_mapping_item( self, source_name: str, field_name: str, expect_optional: bool ) -> Generator[str, None, None]: """Enter a context within which an item of a mapping can be accessed safely, i.e. only if it is contained within that mapping. Yields an expression that accesses the mapping item. Content written while within this context can use this expression to access the desired value. """ value_expr = f"{source_name}['{field_name}']" # if we expect an optional item, it's faster to check if it exists beforehand if expect_optional: with self._start_block(f"if '{field_name}' in {source_name}:"): yield value_expr # the happy path of a try/except will be faster than that, so we use that if # we expect a value else: with self._try_except_pass("KeyError"): yield value_expr @contextmanager def _access_attribute(self, source_name: str, field_name: str, expect_optional: bool) -> Generator[str, None, None]: """Enter a context within which an attribute of an object can be accessed safely, i.e. only if the object actually has the attribute. Yields an expression that retrieves the object attribute. Content written while within this context can use this expression to access the desired value. """ if self.attribute_accessor_name: value_expr = f"{self.attribute_accessor_name}({source_name}, '{field_name}')" else: value_expr = f"{source_name}.{field_name}" # if we expect an optional attribute it's faster to check with hasattr if expect_optional: with self._start_block(f"if hasattr({source_name}, '{field_name}'):"): yield value_expr # the happy path of a try/except will be faster than that, so we use that if # we expect a value else: with self._try_except_pass("AttributeError"): yield value_expr @classmethod def create_transfer_instance_data( cls, field_definitions: tuple[TransferDTOFieldDefinition, ...], destination_type: type[Any], is_data_field: bool, attribute_accessor: Callable[[object, str], Any], ) -> Callable[[Any], Any]: factory = cls( is_data_field=is_data_field, nested_as_dict=destination_type is dict, attribute_accessor=attribute_accessor, ) tmp_return_type_name = factory._create_local_name("tmp_return_type") source_instance_name = factory._create_local_name("source_instance") destination_type_name = factory._add_to_fn_globals("destination_type", destination_type) factory._create_transfer_instance_data( tmp_return_type_name=tmp_return_type_name, source_instance_name=source_instance_name, destination_type_name=destination_type_name, field_definitions=field_definitions, destination_type_is_dict=destination_type is dict, ) return factory._make_function(source_value_name=source_instance_name, return_value_name=tmp_return_type_name) @classmethod def create_transfer_type_data( cls, transfer_type: TransferType, is_data_field: bool, attribute_accessor: Callable[[object, str], Any], ) -> Callable[[Any], Any]: factory = cls( is_data_field=is_data_field, nested_as_dict=False, attribute_accessor=attribute_accessor, ) tmp_return_type_name = factory._create_local_name("tmp_return_type") source_value_name = factory._create_local_name("source_value") factory._create_transfer_type_data_body( transfer_type=transfer_type, nested_as_dict=False, assignment_target=tmp_return_type_name, source_value_name=source_value_name, ) return factory._make_function(source_value_name=source_value_name, return_value_name=tmp_return_type_name) @classmethod def create_transfer_data( cls, *, destination_type: type[Any], field_definitions: tuple[TransferDTOFieldDefinition, ...], is_data_field: bool, field_definition: FieldDefinition | None = None, attribute_accessor: Callable[[object, str], Any], ) -> Callable[[Any], Any]: if field_definition and field_definition.is_non_string_collection: factory = cls( is_data_field=is_data_field, nested_as_dict=False, attribute_accessor=attribute_accessor, ) source_value_name = factory._create_local_name("source_value") return_value_name = factory._create_local_name("tmp_return_value") factory._create_transfer_data_body_nested( field_definitions=field_definitions, field_definition=field_definition, destination_type=destination_type, source_data_name=source_value_name, assignment_target=return_value_name, ) return factory._make_function(source_value_name=source_value_name, return_value_name=return_value_name) return cls.create_transfer_instance_data( destination_type=destination_type, field_definitions=field_definitions, is_data_field=is_data_field, attribute_accessor=attribute_accessor, ) def _create_transfer_data_body_nested( self, field_definition: FieldDefinition, field_definitions: tuple[TransferDTOFieldDefinition, ...], destination_type: type[Any], source_data_name: str, assignment_target: str, ) -> None: origin_name = self._add_to_fn_globals("origin", field_definition.instantiable_origin) transfer_func = TransferFunctionFactory.create_transfer_data( is_data_field=self.is_data_field, destination_type=destination_type, field_definition=field_definition.inner_types[0], field_definitions=field_definitions, attribute_accessor=self.attribute_accessor, ) transfer_func_name = self._add_to_fn_globals("transfer_data", transfer_func) if field_definition.is_mapping: self._add_stmt( f"{assignment_target} = {origin_name}((key, {transfer_func_name}(item)) for key, item in {source_data_name}.items())" ) else: self._add_stmt( f"{assignment_target} = {origin_name}({transfer_func_name}(item) for item in {source_data_name})" ) def _create_transfer_instance_data( self, tmp_return_type_name: str, source_instance_name: str, destination_type_name: str, field_definitions: tuple[TransferDTOFieldDefinition, ...], destination_type_is_dict: bool, ) -> None: local_dict_name = self._create_local_name("unstructured_data") self._add_stmt(f"{local_dict_name} = {{}}") if field_definitions := tuple(f for f in field_definitions if self.is_data_field or not f.is_excluded): if len(field_definitions) > 1 and ("." in source_instance_name or "[" in source_instance_name): # If there's more than one field we have to access, we check if it is # nested. If it is nested, we assign it to a local variable to avoid # repeated lookups. This is only a small performance improvement for # regular attributes, but can be quite significant for properties or # other types of descriptors, where I/O may be involved, such as the # case for lazy loaded relationships in SQLAlchemy if "." in source_instance_name: level_1, level_2 = source_instance_name.split(".", 1) else: level_1, level_2, *_ = self._re_index_access.split(source_instance_name, maxsplit=1) new_source_instance_name = self._create_local_name(f"{level_1}_{level_2}") self._add_stmt(f"{new_source_instance_name} = {source_instance_name}") source_instance_name = new_source_instance_name for source_type in ("mapping", "object"): if source_type == "mapping": block_expr = f"if isinstance({source_instance_name}, Mapping):" access_item = self._access_mapping_item else: block_expr = "else:" access_item = self._access_attribute with self._start_block(expr=block_expr): self._create_transfer_instance_data_inner( local_dict_name=local_dict_name, field_definitions=field_definitions, access_field_safe=access_item, source_instance_name=source_instance_name, ) # if the destination type is a dict we can reuse our temporary dictionary of # unstructured data as the "return value" if not destination_type_is_dict: self._add_stmt(f"{tmp_return_type_name} = {destination_type_name}(**{local_dict_name})") else: self._add_stmt(f"{tmp_return_type_name} = {local_dict_name}") def _create_transfer_instance_data_inner( self, *, local_dict_name: str, field_definitions: tuple[TransferDTOFieldDefinition, ...], access_field_safe: FieldAccessManager, source_instance_name: str, ) -> None: for field_definition in field_definitions: with access_field_safe( source_name=source_instance_name, field_name=field_definition.name, expect_optional=field_definition.is_partial or field_definition.is_optional, ) as source_value_expr: if self.is_data_field and field_definition.is_partial: # we assign the source value to a name here, so we can skip # getting it twice from the source instance source_value_name = self._create_local_name("source_value") self._add_stmt(f"{source_value_name} = {source_value_expr}") ctx = self._start_block(f"if {source_value_name} is not UNSET:") else: # in these cases, we only ever access the source value once, so # we can skip assigning it source_value_name = source_value_expr ctx = nullcontext() # type: ignore[assignment] with ctx: self._create_transfer_type_data_body( transfer_type=field_definition.transfer_type, nested_as_dict=self.nested_as_dict, source_value_name=source_value_name, assignment_target=f"{local_dict_name}['{field_definition.name}']", ) def _create_transfer_type_data_body( self, transfer_type: TransferType, nested_as_dict: bool, source_value_name: str, assignment_target: str, ) -> None: if isinstance(transfer_type, SimpleType) and transfer_type.nested_field_info: if nested_as_dict: destination_type: Any = dict elif self.is_data_field: destination_type = transfer_type.field_definition.annotation else: destination_type = transfer_type.nested_field_info.model self._create_transfer_instance_data( field_definitions=transfer_type.nested_field_info.field_definitions, tmp_return_type_name=assignment_target, source_instance_name=source_value_name, destination_type_name=self._add_to_fn_globals("destination_type", destination_type), destination_type_is_dict=destination_type is dict, ) return if isinstance(transfer_type, UnionType) and transfer_type.has_nested: self._create_transfer_nested_union_type_data( transfer_type=transfer_type, source_value_name=source_value_name, assignment_target=assignment_target, ) return if isinstance(transfer_type, CollectionType): origin_name = self._add_to_fn_globals("origin", transfer_type.field_definition.instantiable_origin) if transfer_type.has_nested: transfer_type_data_fn = TransferFunctionFactory.create_transfer_type_data( is_data_field=self.is_data_field, transfer_type=transfer_type.inner_type, attribute_accessor=self.attribute_accessor, ) transfer_type_data_name = self._add_to_fn_globals("transfer_type_data", transfer_type_data_fn) self._add_stmt( f"{assignment_target} = {origin_name}({transfer_type_data_name}(item) for item in {source_value_name})" ) return self._add_stmt(f"{assignment_target} = {origin_name}({source_value_name})") return if isinstance(transfer_type, MappingType): origin_name = self._add_to_fn_globals("origin", transfer_type.field_definition.instantiable_origin) if transfer_type.has_nested: transfer_type_data_fn = TransferFunctionFactory.create_transfer_type_data( is_data_field=self.is_data_field, transfer_type=transfer_type.value_type, attribute_accessor=self.attribute_accessor, ) transfer_type_data_name = self._add_to_fn_globals("transfer_type_data", transfer_type_data_fn) self._add_stmt( f"{assignment_target} = {origin_name}((key, {transfer_type_data_name}(item)) for key, item in {source_value_name}.items())" ) return self._add_stmt(f"{assignment_target} = {origin_name}({source_value_name})") return self._add_stmt(f"{assignment_target} = {source_value_name}") def _create_transfer_nested_union_type_data( self, transfer_type: UnionType, source_value_name: str, assignment_target: str, ) -> None: for inner_type in transfer_type.inner_types: if isinstance(inner_type, CompositeType): continue if inner_type.nested_field_info: if self.is_data_field: constraint_type = inner_type.nested_field_info.model destination_type = inner_type.field_definition.annotation else: constraint_type = inner_type.field_definition.annotation destination_type = inner_type.nested_field_info.model constraint_type_name = self._add_to_fn_globals("constraint_type", constraint_type) destination_type_name = self._add_to_fn_globals("destination_type", destination_type) with self._start_block(f"if isinstance({source_value_name}, {constraint_type_name}):"): self._create_transfer_instance_data( destination_type_name=destination_type_name, destination_type_is_dict=destination_type is dict, field_definitions=inner_type.nested_field_info.field_definitions, source_instance_name=source_value_name, tmp_return_type_name=assignment_target, ) return self._add_stmt(f"{assignment_target} = {source_value_name}") litestar-2.16.0/litestar/dto/_types.py000066400000000000000000000101151500564371300177230ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from litestar.dto.data_structures import DTOFieldDefinition if TYPE_CHECKING: from typing import Any from typing_extensions import Self from litestar.typing import FieldDefinition @dataclass(frozen=True) class NestedFieldInfo: """Type for representing fields and model type of nested model type.""" __slots__ = ("field_definitions", "model") model: type[Any] field_definitions: tuple[TransferDTOFieldDefinition, ...] @dataclass(frozen=True) class TransferType: """Type for representing model types for data transfer.""" __slots__ = ("field_definition",) field_definition: FieldDefinition @dataclass(frozen=True) class SimpleType(TransferType): """Represents indivisible, non-composite types.""" __slots__ = ("nested_field_info",) nested_field_info: NestedFieldInfo | None """If the type is a 'nested' type, this is the model generated for transfer to/from it.""" @property def has_nested(self) -> bool: return self.nested_field_info is not None @dataclass(frozen=True) class CompositeType(TransferType): """A type that is made up of other types.""" __slots__ = ("has_nested",) has_nested: bool """Whether the type represents nested model types within itself.""" @dataclass(frozen=True) class UnionType(CompositeType): """Type for representing union types for data transfer.""" __slots__ = ("inner_types",) inner_types: tuple[CompositeType | SimpleType, ...] @dataclass(frozen=True) class CollectionType(CompositeType): """Type for representing collection types for data transfer.""" __slots__ = ("inner_type",) inner_type: CompositeType | SimpleType @dataclass(frozen=True) class TupleType(CompositeType): """Type for representing tuples for data transfer.""" __slots__ = ("inner_types",) inner_types: tuple[CompositeType | SimpleType, ...] @dataclass(frozen=True) class MappingType(CompositeType): """Type for representing mappings for data transfer.""" __slots__ = ("key_type", "value_type") key_type: CompositeType | SimpleType value_type: CompositeType | SimpleType @dataclass(frozen=True) class TransferDTOFieldDefinition(DTOFieldDefinition): __slots__ = ( "is_excluded", "is_partial", "serialization_name", "transfer_type", "unique_name", ) transfer_type: TransferType """Type of the field for transfer.""" serialization_name: str | None """Name of the field as it should appear in serialized form.""" is_partial: bool """Whether the field is optional for transfer.""" is_excluded: bool """Whether the field should be excluded from transfer.""" @classmethod def from_dto_field_definition( cls, field_definition: DTOFieldDefinition, transfer_type: TransferType, serialization_name: str | None, is_partial: bool, is_excluded: bool, ) -> Self: return cls( annotation=field_definition.annotation, args=field_definition.args, default=field_definition.default, default_factory=field_definition.default_factory, dto_field=field_definition.dto_field, extra=field_definition.extra, inner_types=field_definition.inner_types, instantiable_origin=field_definition.instantiable_origin, is_excluded=is_excluded, is_partial=is_partial, kwarg_definition=field_definition.kwarg_definition, metadata=field_definition.metadata, name=field_definition.name, origin=field_definition.origin, raw=field_definition.raw, safe_generic_origin=field_definition.safe_generic_origin, serialization_name=serialization_name, transfer_type=transfer_type, type_wrappers=field_definition.type_wrappers, model_name=field_definition.model_name, passthrough_constraints=field_definition.passthrough_constraints, ) litestar-2.16.0/litestar/dto/base_dto.py000066400000000000000000000365361500564371300202170ustar00rootroot00000000000000from __future__ import annotations import dataclasses import typing from abc import abstractmethod from inspect import getmodule from typing import TYPE_CHECKING, Callable, Collection, Generic, TypeVar from typing_extensions import NotRequired, TypedDict, get_type_hints from litestar.dto._backend import DTOBackend from litestar.dto._codegen_backend import DTOCodegenBackend from litestar.dto.config import DTOConfig from litestar.dto.data_structures import DTOData from litestar.dto.types import RenameStrategy from litestar.enums import RequestEncodingType from litestar.exceptions.dto_exceptions import InvalidAnnotationException from litestar.types.builtin_types import NoneType from litestar.types.composite_types import TypeEncodersMap from litestar.typing import FieldDefinition from litestar.utils.signature import ParsedSignature if TYPE_CHECKING: from typing import Any, ClassVar, Generator from typing_extensions import Self from litestar._openapi.schema_generation import SchemaCreator from litestar.connection import ASGIConnection from litestar.dto.data_structures import DTOFieldDefinition from litestar.openapi.spec import Reference, Schema from litestar.types.serialization import LitestarEncodableType __all__ = ("AbstractDTO",) T = TypeVar("T") class _BackendDict(TypedDict): data_backend: NotRequired[DTOBackend] return_backend: NotRequired[DTOBackend] class AbstractDTO(Generic[T]): """Base class for DTO types.""" __slots__ = ("asgi_connection",) config: ClassVar[DTOConfig] """Config objects to define properties of the DTO.""" model_type: type[T] """If ``annotation`` is an iterable, this is the inner type, otherwise will be the same as ``annotation``.""" attribute_accessor: Callable[[object, str], Any] = getattr """:func:`getattr` like callable to access attributes on the data source""" _dto_backends: ClassVar[dict[str, _BackendDict]] = {} def __init__(self, asgi_connection: ASGIConnection) -> None: """Create an AbstractDTOFactory type. Args: asgi_connection: A :class:`ASGIConnection ` instance. """ self.asgi_connection = asgi_connection def __class_getitem__(cls, annotation: Any) -> type[Self]: field_definition = FieldDefinition.from_annotation(annotation) if (field_definition.is_optional and len(field_definition.args) > 2) or ( field_definition.is_union and not field_definition.is_optional ): raise InvalidAnnotationException("Unions are currently not supported as type argument to DTOs.") if field_definition.is_forward_ref: raise InvalidAnnotationException("Forward references are not supported as type argument to DTO") # if a configuration is not provided, and the type narrowing is a type var, we don't want to create a subclass config = cls.get_dto_config_from_annotated_type(field_definition) if not config: if field_definition.is_type_var: return cls config = cls.config if hasattr(cls, "config") else DTOConfig() cls_dict: dict[str, Any] = {"config": config, "_type_backend_map": {}, "_handler_backend_map": {}} if not field_definition.is_type_var: cls_dict.update(model_type=field_definition.annotation) return type(f"{cls.__name__}[{annotation}]", (cls,), cls_dict) # pyright: ignore def __init_subclass__(cls, **kwargs: Any) -> None: if (config := getattr(cls, "config", None)) and (model_type := getattr(cls, "model_type", None)): # it's a concrete class cls.config = cls.get_config_for_model_type(config, model_type) @classmethod def get_config_for_model_type(cls, config: DTOConfig, model_type: type[Any]) -> DTOConfig: """Create a new configuration for this specific ``model_type``, during the creation of the factory. The returned config object will be set as the ``config`` attribute on the newly defined factory class. .. versionadded: 2.11 """ return config def decode_builtins(self, value: dict[str, Any]) -> Any: """Decode a dictionary of Python values into an the DTO's datatype.""" backend = self._dto_backends[self.asgi_connection.route_handler.handler_id]["data_backend"] # pyright: ignore return backend.populate_data_from_builtins(value, self.asgi_connection) def decode_bytes(self, value: bytes) -> Any: """Decode a byte string into an the DTO's datatype.""" backend = self._dto_backends[self.asgi_connection.route_handler.handler_id]["data_backend"] # pyright: ignore return backend.populate_data_from_raw(value, self.asgi_connection) def data_to_encodable_type(self, data: T | Collection[T]) -> LitestarEncodableType: backend = self._dto_backends[self.asgi_connection.route_handler.handler_id]["return_backend"] # pyright: ignore return backend.encode_data(data) @classmethod @abstractmethod def generate_field_definitions(cls, model_type: type[Any]) -> Generator[DTOFieldDefinition, None, None]: """Generate ``FieldDefinition`` instances from ``model_type``. Yields: ``FieldDefinition`` instances. """ @classmethod @abstractmethod def detect_nested_field(cls, field_definition: FieldDefinition) -> bool: """Return ``True`` if ``field_definition`` represents a nested model field. Args: field_definition: inspect type to determine if field represents a nested model. Returns: ``True`` if ``field_definition`` represents a nested model field. """ @classmethod def is_supported_model_type_field(cls, field_definition: FieldDefinition) -> bool: """Check support for the given type. Args: field_definition: A :class:`FieldDefinition ` instance. Returns: Whether the type of the field definition is supported by the DTO. """ return field_definition.is_subclass_of(cls.model_type) or ( field_definition.origin and any( cls.resolve_model_type(inner_field).is_subclass_of(cls.model_type) for inner_field in field_definition.inner_types ) ) @classmethod def create_for_field_definition( cls, field_definition: FieldDefinition, handler_id: str, backend_cls: type[DTOBackend] | None = None, ) -> None: """Creates a DTO subclass for a field definition. Args: field_definition: A :class:`FieldDefinition ` instance. handler_id: ID of the route handler for which to create a DTO instance. backend_cls: Alternative DTO backend class to use Returns: None """ if handler_id not in cls._dto_backends: cls._dto_backends[handler_id] = {} backend_context = cls._dto_backends[handler_id] key = "data_backend" if field_definition.name == "data" else "return_backend" if key not in backend_context: model_type_field_definition = cls.resolve_model_type(field_definition=field_definition) wrapper_attribute_name: str | None = None if not model_type_field_definition.is_subclass_of(cls.model_type): if resolved_generic_result := cls.resolve_generic_wrapper_type( field_definition=model_type_field_definition ): model_type_field_definition, field_definition, wrapper_attribute_name = resolved_generic_result else: raise InvalidAnnotationException( f"DTO narrowed with '{cls.model_type}', handler type is '{field_definition.annotation}'" ) if backend_cls is None: backend_cls = DTOCodegenBackend if cls.config.experimental_codegen_backend is not False else DTOBackend backend_context[key] = backend_cls( # type: ignore[literal-required] dto_factory=cls, field_definition=field_definition, model_type=model_type_field_definition.annotation, wrapper_attribute_name=wrapper_attribute_name, is_data_field=field_definition.name == "data", handler_id=handler_id, ) @classmethod def create_openapi_schema( cls, field_definition: FieldDefinition, handler_id: str, schema_creator: SchemaCreator ) -> Reference | Schema: """Create an OpenAPI request body. Args: field_definition: A parsed type annotation that represents the annotation used on the handler. handler_id: ID of the route handler for which to create a DTO instance. schema_creator: A factory for creating schemas. Has a ``for_field_definition()`` method that accepts a :class:`~litestar.typing.FieldDefinition` instance. Returns: OpenAPI request body. """ key = "data_backend" if field_definition.name == "data" else "return_backend" backend = cls._dto_backends[handler_id][key] # type: ignore[literal-required] if backend.wrapper_attribute_name: # The DTO has been built for a handler that has a DTO supported type wrapped in a generic type. # # The backend doesn't receive the full annotation, only the type of the attribute on the outer type that # holds the DTO supported type. # # This special casing rebuilds the outer generic type annotation with the original model replaced by the DTO # generated transfer model type in the type arguments. transfer_model = backend.transfer_model_type generic_args = tuple(transfer_model if a is cls.model_type else a for a in field_definition.args) annotation = field_definition.safe_generic_origin[generic_args] else: annotation = backend.annotation return schema_creator.for_field_definition( FieldDefinition.from_annotation(annotation, kwarg_definition=field_definition.kwarg_definition) ) @classmethod def resolve_generic_wrapper_type( cls, field_definition: FieldDefinition ) -> tuple[FieldDefinition, FieldDefinition, str] | None: """Handle where DTO supported data is wrapped in a generic container type. Args: field_definition: A parsed type annotation that represents the annotation used to narrow the DTO type. Returns: The data model type. """ if field_definition.origin and ( inner_fields := [ inner_field for inner_field in field_definition.inner_types if cls.resolve_model_type(inner_field).is_subclass_of(cls.model_type) ] ): inner_field = inner_fields[0] model_field_definition = cls.resolve_model_type(inner_field) for attr, attr_type in cls.get_model_type_hints(field_definition.origin).items(): if isinstance(attr_type.annotation, TypeVar) or any( isinstance(t.annotation, TypeVar) for t in attr_type.inner_types ): if attr_type.is_non_string_collection: # the inner type of the collection type is the type var, so we need to specialize the # collection type with the DTO supported type. specialized_annotation = attr_type.safe_generic_origin[model_field_definition.annotation] return model_field_definition, FieldDefinition.from_annotation(specialized_annotation), attr return model_field_definition, inner_field, attr return None @staticmethod def get_model_namespace(model_type: type[Any], namespace: dict[str, Any] | None = None) -> dict[str, Any]: namespace = namespace or {} namespace.update(vars(typing)) namespace.update( { "TypeEncodersMap": TypeEncodersMap, "DTOConfig": DTOConfig, "RenameStrategy": RenameStrategy, "RequestEncodingType": RequestEncodingType, } ) if model_module := getmodule(model_type): namespace.update(vars(model_module)) return namespace @classmethod def get_property_fields(cls, model_type: type[Any]) -> dict[str, FieldDefinition]: return { name: dataclasses.replace( ParsedSignature.from_fn(attr.fget, cls.get_model_namespace(model_type)).return_type, name=name, ) for name, attr in vars(model_type).items() if isinstance(attr, property) and attr.fget is not None } @staticmethod def get_model_type_hints( model_type: type[Any], namespace: dict[str, Any] | None = None ) -> dict[str, FieldDefinition]: """Retrieve type annotations for ``model_type``. Args: model_type: Any type-annotated class. namespace: Optional namespace to use for resolving type hints. Returns: Parsed type hints for ``model_type`` resolved within the scope of its module. """ namespace = AbstractDTO.get_model_namespace(model_type, namespace) return { k: FieldDefinition.from_kwarg(annotation=v, name=k) for k, v in get_type_hints(model_type, localns=namespace, include_extras=True).items() # pyright: ignore } @staticmethod def get_dto_config_from_annotated_type(field_definition: FieldDefinition) -> DTOConfig | None: """Extract data type and config instances from ``Annotated`` annotation. Args: field_definition: A parsed type annotation that represents the annotation used to narrow the DTO type. Returns: The type and config object extracted from the annotation. """ return next((item for item in field_definition.metadata if isinstance(item, DTOConfig)), None) @classmethod def resolve_model_type(cls, field_definition: FieldDefinition) -> FieldDefinition: """Resolve the data model type from a parsed type. Args: field_definition: A parsed type annotation that represents the annotation used to narrow the DTO type. Returns: A :class:`FieldDefinition <.typing.FieldDefinition>` that represents the data model type. """ if field_definition.is_optional: return cls.resolve_model_type( next(t for t in field_definition.inner_types if not t.is_subclass_of(NoneType)) ) if field_definition.is_subclass_of(DTOData): return cls.resolve_model_type(field_definition.inner_types[0]) if field_definition.is_collection: if field_definition.is_mapping: return cls.resolve_model_type(field_definition.inner_types[1]) if field_definition.is_tuple: if any(t is Ellipsis for t in field_definition.args): return cls.resolve_model_type(field_definition.inner_types[0]) elif field_definition.is_non_string_collection: return cls.resolve_model_type(field_definition.inner_types[0]) return field_definition litestar-2.16.0/litestar/dto/config.py000066400000000000000000000052631500564371300176750ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING from litestar.exceptions import ImproperlyConfiguredException if TYPE_CHECKING: from typing import AbstractSet from litestar.dto.types import RenameStrategy __all__ = ("DTOConfig",) @dataclass(frozen=True) class DTOConfig: """Control the generated DTO.""" exclude: AbstractSet[str] = field(default_factory=set) """Explicitly exclude fields from the generated DTO. If exclude is specified, all fields not specified in exclude will be included by default. Notes: - The field names are dot-separated paths to nested fields, e.g. ``"address.street"`` will exclude the ``"street"`` field from a nested ``"address"`` model. - 'exclude' mutually exclusive with 'include' - specifying both values will raise an ``ImproperlyConfiguredException``. """ include: AbstractSet[str] = field(default_factory=set) """Explicitly include fields in the generated DTO. If include is specified, all fields not specified in include will be excluded by default. Notes: - The field names are dot-separated paths to nested fields, e.g. ``"address.street"`` will include the ``"street"`` field from a nested ``"address"`` model. - 'include' mutually exclusive with 'exclude' - specifying both values will raise an ``ImproperlyConfiguredException``. """ rename_fields: dict[str, str] = field(default_factory=dict) """Mapping of field names, to new name.""" rename_strategy: RenameStrategy | None = None """Rename all fields using a pre-defined strategy or a custom strategy. The pre-defined strategies are: `upper`, `lower`, `camel`, `pascal`. A custom strategy is any callable that accepts a string as an argument and return a string. Fields defined in ``rename_fields`` are ignored.""" max_nested_depth: int = 1 """The maximum depth of nested items allowed for data transfer.""" partial: bool = False """Allow transfer of partial data.""" underscore_fields_private: bool = True """Fields starting with an underscore are considered private and excluded from data transfer.""" experimental_codegen_backend: bool | None = None """Use the experimental codegen backend""" forbid_unknown_fields: bool = False """Raise an exception for fields present in the raw data that are not defined on the model""" def __post_init__(self) -> None: if self.include and self.exclude: raise ImproperlyConfiguredException( "'include' and 'exclude' are mutually exclusive options, please use one of them" ) litestar-2.16.0/litestar/dto/data_structures.py000066400000000000000000000102731500564371300216410ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Generic, TypeVar from litestar.typing import FieldDefinition if TYPE_CHECKING: from typing import Any, Callable from litestar.dto import DTOField from litestar.dto._backend import DTOBackend T = TypeVar("T") class DTOData(Generic[T]): """DTO validated data and utility methods.""" __slots__ = ("_backend", "_data_as_builtins") def __init__(self, backend: DTOBackend, data_as_builtins: Any) -> None: self._backend = backend self._data_as_builtins = data_as_builtins def create_instance(self, **kwargs: Any) -> T: """Create an instance of the DTO validated data. Args: **kwargs: Additional data to create the instance with. Takes precedence over DTO validated data. """ data = dict(self._data_as_builtins) for k, v in kwargs.items(): _set_nested_dict_value(data, k.split("__"), v) return self._backend.transfer_data_from_builtins(data) # type: ignore[no-any-return] def update_instance(self, instance: T, **kwargs: Any) -> T: """Update an instance with the DTO validated data. Args: instance: The instance to update. **kwargs: Additional data to update the instance with. Takes precedence over DTO validated data. """ data = {**self._data_as_builtins, **kwargs} for k, v in data.items(): setattr(instance, k, v) return instance def as_builtins(self) -> Any: """Return the DTO validated data as builtins.""" return self._data_as_builtins def _set_nested_dict_value(d: dict[str, Any], keys: list[str], value: Any) -> None: if len(keys) == 1: d[keys[0]] = value else: key = keys[0] d.setdefault(key, {}) _set_nested_dict_value(d[key], keys[1:], value) @dataclass(frozen=True) class DTOFieldDefinition(FieldDefinition): """A model field representation for purposes of generating a DTO backend model type.""" __slots__ = ( "default_factory", "dto_field", "model_name", "passthrough_constraints", ) model_name: str """The name of the model for which the field is generated.""" default_factory: Callable[[], Any] | None """Default factory of the field.""" dto_field: DTOField """DTO field configuration.""" passthrough_constraints: bool """Pass constraints of the source annotation to be validated by the DTO backend""" @classmethod def from_field_definition( cls, field_definition: FieldDefinition, model_name: str, default_factory: Callable[[], Any] | None, dto_field: DTOField, passthrough_constraints: bool = True, ) -> DTOFieldDefinition: """Create a :class:`FieldDefinition` from a :class:`FieldDefinition`. Args: field_definition: A :class:`FieldDefinition` to create a :class:`FieldDefinition` from. model_name: The name of the model. default_factory: Default factory function, if any. dto_field: DTOField instance. passthrough_constraints: Pass constraints of the source annotation to be validated by the DTO backend Returns: A :class:`FieldDefinition` instance. """ return DTOFieldDefinition( annotation=field_definition.annotation, args=field_definition.args, default=field_definition.default, default_factory=default_factory, dto_field=dto_field, extra=field_definition.extra, inner_types=field_definition.inner_types, instantiable_origin=field_definition.instantiable_origin, kwarg_definition=field_definition.kwarg_definition, metadata=field_definition.metadata, model_name=model_name, name=field_definition.name, origin=field_definition.origin, raw=field_definition.raw, safe_generic_origin=field_definition.safe_generic_origin, type_wrappers=field_definition.type_wrappers, passthrough_constraints=passthrough_constraints, ) litestar-2.16.0/litestar/dto/dataclass_dto.py000066400000000000000000000052001500564371300212240ustar00rootroot00000000000000from __future__ import annotations from dataclasses import MISSING, fields, replace from typing import TYPE_CHECKING, Generic, TypeVar from litestar.dto.base_dto import AbstractDTO from litestar.dto.data_structures import DTOFieldDefinition from litestar.dto.field import DTOField, extract_dto_field from litestar.params import DependencyKwarg, KwargDefinition from litestar.types.empty import Empty if TYPE_CHECKING: from typing import Collection, Generator from litestar.types.protocols import DataclassProtocol from litestar.typing import FieldDefinition __all__ = ("DataclassDTO", "T") T = TypeVar("T", bound="DataclassProtocol | Collection[DataclassProtocol]") AnyDataclass = TypeVar("AnyDataclass", bound="DataclassProtocol") class DataclassDTO(AbstractDTO[T], Generic[T]): """Support for domain modelling with dataclasses.""" @classmethod def generate_field_definitions( cls, model_type: type[DataclassProtocol] ) -> Generator[DTOFieldDefinition, None, None]: dc_fields = {f.name: f for f in fields(model_type)} properties = cls.get_property_fields(model_type) for key, field_definition in cls.get_model_type_hints(model_type).items(): if not (dc_field := dc_fields.get(key)): continue default = dc_field.default if dc_field.default is not MISSING else Empty default_factory = dc_field.default_factory if dc_field.default_factory is not MISSING else None field_definition = replace( DTOFieldDefinition.from_field_definition( field_definition=field_definition, default_factory=default_factory, dto_field=extract_dto_field(field_definition, dc_field.metadata), model_name=model_type.__name__, ), name=key, default=default, ) yield ( replace(field_definition, default=Empty, kwarg_definition=default) if isinstance(default, (KwargDefinition, DependencyKwarg)) else field_definition ) for key, property_field in properties.items(): if key.startswith("_"): continue yield DTOFieldDefinition.from_field_definition( property_field, model_name=model_type.__name__, default_factory=None, dto_field=DTOField(mark="read-only"), ) @classmethod def detect_nested_field(cls, field_definition: FieldDefinition) -> bool: return hasattr(field_definition.annotation, "__dataclass_fields__") litestar-2.16.0/litestar/dto/field.py000066400000000000000000000051651500564371300175140ustar00rootroot00000000000000"""DTO domain types.""" from __future__ import annotations from dataclasses import dataclass from enum import Enum from typing import TYPE_CHECKING from litestar.exceptions import ImproperlyConfiguredException if TYPE_CHECKING: from typing import Any, Literal, Mapping from litestar.typing import FieldDefinition __all__ = ( "DTO_FIELD_META_KEY", "DTOField", "Mark", "dto_field", "extract_dto_field", ) DTO_FIELD_META_KEY = "__dto__" class Mark(str, Enum): """For marking field definitions on domain models.""" READ_ONLY = "read-only" """To mark a field that can be read, but not updated by clients.""" WRITE_ONLY = "write-only" """To mark a field that can be written to, but not read by clients.""" PRIVATE = "private" """To mark a field that can neither be read or updated by clients.""" @dataclass class DTOField: """For configuring DTO behavior on model fields.""" mark: Mark | Literal["read-only", "write-only", "private"] | None = None """Mark the field as read-only, or private.""" def dto_field(mark: Literal["read-only", "write-only", "private"] | Mark) -> dict[str, DTOField]: """Create a field metadata mapping. Args: mark: A DTO mark for the field, e.g., "read-only". Returns: A dict for setting as field metadata, such as the dataclass "metadata" field key, or the SQLAlchemy "info" field. Marking a field automates its inclusion/exclusion from DTO field definitions, depending on the DTO's purpose. """ return {DTO_FIELD_META_KEY: DTOField(mark=Mark(mark))} def extract_dto_field(field_definition: FieldDefinition, field_info_mapping: Mapping[str, Any]) -> DTOField: """Extract ``DTOField`` instance for a model field. Supports ``DTOField`` to bet set via ``Annotated`` or via a field info/metadata mapping. E.g., ``Annotated[str, DTOField(mark="read-only")]`` or ``info=dto_field(mark="read-only")``. If a value is found in ``field_info_mapping``, it is prioritized over the field definition's metadata. Args: field_definition: A field definition. field_info_mapping: A field metadata/info attribute mapping, e.g., SQLAlchemy's ``info`` attribute, or dataclasses ``metadata`` attribute. Returns: DTO field info, if any. """ if inst := field_info_mapping.get(DTO_FIELD_META_KEY): if not isinstance(inst, DTOField): raise ImproperlyConfiguredException(f"DTO field info must be an instance of DTOField, got '{inst}'") return inst return next((f for f in field_definition.metadata if isinstance(f, DTOField)), DTOField()) litestar-2.16.0/litestar/dto/msgspec_dto.py000066400000000000000000000056011500564371300207330ustar00rootroot00000000000000from __future__ import annotations import dataclasses from dataclasses import replace from typing import TYPE_CHECKING, Generic, TypeVar import msgspec.inspect from msgspec import NODEFAULT, Struct, structs from litestar.dto.base_dto import AbstractDTO from litestar.dto.data_structures import DTOFieldDefinition from litestar.dto.field import DTO_FIELD_META_KEY, DTOField, extract_dto_field from litestar.plugins.core._msgspec import kwarg_definition_from_field from litestar.types.empty import Empty if TYPE_CHECKING: from typing import Any, Collection, Generator from litestar.typing import FieldDefinition __all__ = ("MsgspecDTO",) T = TypeVar("T", bound="Struct | Collection[Struct]") def _default_or_empty(value: Any) -> Any: return Empty if value is NODEFAULT else value def _default_or_none(value: Any) -> Any: return None if value is NODEFAULT else value class MsgspecDTO(AbstractDTO[T], Generic[T]): """Support for domain modelling with Msgspec.""" @classmethod def generate_field_definitions(cls, model_type: type[Struct]) -> Generator[DTOFieldDefinition, None, None]: msgspec_fields = {f.name: f for f in structs.fields(model_type)} inspect_fields: dict[str, msgspec.inspect.Field] = { field.name: field for field in msgspec.inspect.type_info(model_type).fields # type: ignore[attr-defined] } property_fields = cls.get_property_fields(model_type) for key, field_definition in cls.get_model_type_hints(model_type).items(): kwarg_definition, extra = kwarg_definition_from_field(inspect_fields[key]) field_definition = dataclasses.replace(field_definition, kwarg_definition=kwarg_definition) field_definition.extra.update(extra) dto_field = extract_dto_field(field_definition, field_definition.extra) field_definition.extra.pop(DTO_FIELD_META_KEY, None) msgspec_field = msgspec_fields[key] yield replace( DTOFieldDefinition.from_field_definition( field_definition=field_definition, dto_field=dto_field, model_name=model_type.__name__, default_factory=_default_or_none(msgspec_field.default_factory), ), default=_default_or_empty(msgspec_field.default), name=key, ) for key, property_field in property_fields.items(): if key.startswith("_"): continue yield DTOFieldDefinition.from_field_definition( property_field, model_name=model_type.__name__, default_factory=None, dto_field=DTOField(mark="read-only"), ) @classmethod def detect_nested_field(cls, field_definition: FieldDefinition) -> bool: return field_definition.is_subclass_of(Struct) litestar-2.16.0/litestar/dto/types.py000066400000000000000000000006131500564371300175660ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Callable, Literal from typing_extensions import TypeAlias __all__ = ("RenameStrategy",) RenameStrategy: TypeAlias = 'Literal["lower", "upper", "camel", "pascal", "kebab"] | Callable[[str], str]' """A pre-defined strategy or a custom callback for converting DTO field names.""" litestar-2.16.0/litestar/enums.py000066400000000000000000000041541500564371300167670ustar00rootroot00000000000000from enum import Enum __all__ = ( "CompressionEncoding", "HttpMethod", "MediaType", "OpenAPIMediaType", "ParamType", "RequestEncodingType", "ScopeType", ) class HttpMethod(str, Enum): """An Enum for HTTP methods.""" DELETE = "DELETE" GET = "GET" HEAD = "HEAD" OPTIONS = "OPTIONS" PATCH = "PATCH" POST = "POST" PUT = "PUT" TRACE = "TRACE" class MediaType(str, Enum): """An Enum for ``Content-Type`` header values.""" JSON = "application/json" MESSAGEPACK = "application/x-msgpack" HTML = "text/html" TEXT = "text/plain" CSS = "text/css" XML = "application/xml" class OpenAPIMediaType(str, Enum): """An Enum for OpenAPI specific response ``Content-Type`` header values.""" OPENAPI_YAML = "application/vnd.oai.openapi" OPENAPI_JSON = "application/vnd.oai.openapi+json" class RequestEncodingType(str, Enum): """An Enum for request ``Content-Type`` header values designating encoding formats.""" JSON = "application/json" MESSAGEPACK = "application/x-msgpack" MULTI_PART = "multipart/form-data" URL_ENCODED = "application/x-www-form-urlencoded" class ScopeType(str, Enum): """An Enum for the 'http' key stored under Scope. Notes: - ``asgi`` is used by Litestar internally and is not part of the specification. """ HTTP = "http" WEBSOCKET = "websocket" ASGI = "asgi" class ParamType(str, Enum): """An Enum for the types of parameters a request can receive.""" PATH = "path" QUERY = "query" COOKIE = "cookie" HEADER = "header" class CompressionEncoding(str, Enum): """An Enum for supported compression encodings.""" GZIP = "gzip" BROTLI = "br" class ASGIExtension(str, Enum): """ASGI extension keys: https://asgi.readthedocs.io/en/latest/extensions.html""" WS_DENIAL = "websocket.http.response" SERVER_PUSH = "http.response.push" ZERO_COPY_SEND_EXTENSION = "http.response.zerocopysend" PATH_SEND = "http.response.pathsend" TLS = "tls" EARLY_HINTS = "http.response.early_hint" HTTP_TRAILERS = "http.response.trailers" litestar-2.16.0/litestar/events/000077500000000000000000000000001500564371300165665ustar00rootroot00000000000000litestar-2.16.0/litestar/events/__init__.py000066400000000000000000000003111500564371300206720ustar00rootroot00000000000000from .emitter import BaseEventEmitterBackend, SimpleEventEmitter from .listener import EventListener, listener __all__ = ("BaseEventEmitterBackend", "EventListener", "SimpleEventEmitter", "listener") litestar-2.16.0/litestar/events/emitter.py000066400000000000000000000106621500564371300206160ustar00rootroot00000000000000from __future__ import annotations import math import sys from abc import ABC, abstractmethod from collections import defaultdict from contextlib import AsyncExitStack from functools import partial from typing import TYPE_CHECKING, Any, Sequence if sys.version_info < (3, 9): from typing import AsyncContextManager else: from contextlib import AbstractAsyncContextManager as AsyncContextManager import anyio from litestar.exceptions import ImproperlyConfiguredException if TYPE_CHECKING: from types import TracebackType from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream from litestar.events.listener import EventListener __all__ = ("BaseEventEmitterBackend", "SimpleEventEmitter") class BaseEventEmitterBackend(AsyncContextManager["BaseEventEmitterBackend"], ABC): """Abstract class used to define event emitter backends.""" __slots__ = ("listeners",) listeners: defaultdict[str, set[EventListener]] def __init__(self, listeners: Sequence[EventListener]) -> None: """Create an event emitter instance. Args: listeners: A list of listeners. """ self.listeners = defaultdict(set) for listener in listeners: for event_id in listener.event_ids: self.listeners[event_id].add(listener) @abstractmethod def emit(self, event_id: str, *args: Any, **kwargs: Any) -> None: """Emit an event to all attached listeners. Args: event_id: The ID of the event to emit, e.g 'my_event'. *args: args to pass to the listener(s). **kwargs: kwargs to pass to the listener(s) Returns: None """ raise NotImplementedError("not implemented") class SimpleEventEmitter(BaseEventEmitterBackend): """Event emitter the works only in the current process""" __slots__ = ("_exit_stack", "_queue", "_receive_stream", "_send_stream") def __init__(self, listeners: Sequence[EventListener]) -> None: """Create an event emitter instance. Args: listeners: A list of listeners. """ super().__init__(listeners=listeners) self._receive_stream: MemoryObjectReceiveStream | None = None self._send_stream: MemoryObjectSendStream | None = None self._exit_stack: AsyncExitStack | None = None async def _worker(self, receive_stream: MemoryObjectReceiveStream) -> None: """Run items from ``receive_stream`` in a task group. Returns: None """ async with receive_stream, anyio.create_task_group() as task_group: async for item in receive_stream: fn, args, kwargs = item if kwargs: fn = partial(fn, **kwargs) task_group.start_soon(fn, *args) # pyright: ignore[reportGeneralTypeIssues] async def __aenter__(self) -> SimpleEventEmitter: self._exit_stack = AsyncExitStack() send_stream, receive_stream = anyio.create_memory_object_stream(math.inf) # type: ignore[var-annotated] self._send_stream = send_stream task_group = anyio.create_task_group() await self._exit_stack.enter_async_context(task_group) await self._exit_stack.enter_async_context(send_stream) task_group.start_soon(self._worker, receive_stream) return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: if self._exit_stack: await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb) self._exit_stack = None self._send_stream = None def emit(self, event_id: str, *args: Any, **kwargs: Any) -> None: """Emit an event to all attached listeners. Args: event_id: The ID of the event to emit, e.g 'my_event'. *args: args to pass to the listener(s). **kwargs: kwargs to pass to the listener(s) Returns: None """ if not (self._send_stream and self._exit_stack): raise RuntimeError("Emitter not initialized") if listeners := self.listeners.get(event_id): for listener in listeners: self._send_stream.send_nowait((listener.fn, args, kwargs)) return raise ImproperlyConfiguredException(f"no event listeners are registered for event ID: {event_id}") litestar-2.16.0/litestar/events/listener.py000066400000000000000000000046601500564371300207730ustar00rootroot00000000000000from __future__ import annotations import logging from functools import update_wrapper from typing import TYPE_CHECKING, Any from litestar.exceptions import ImproperlyConfiguredException from litestar.utils import ensure_async_callable from litestar.utils.predicates import is_async_callable if TYPE_CHECKING: from litestar.types import AnyCallable, AsyncAnyCallable __all__ = ("EventListener", "listener") logger = logging.getLogger(__name__) class EventListener: """Decorator for event listeners""" __slots__ = ("event_ids", "fn", "listener_id") fn: AsyncAnyCallable def __init__(self, *event_ids: str) -> None: """Create a decorator for event handlers. Args: *event_ids: The id of the event to listen to or a list of event ids to listen to. """ self.event_ids: frozenset[str] = frozenset(event_ids) def __call__(self, fn: AnyCallable) -> EventListener: """Decorate a callable by wrapping it inside an instance of EventListener. Args: fn: Callable to decorate. Returns: An instance of EventListener """ if not callable(fn): raise ImproperlyConfiguredException("EventListener instance should be called as a decorator on a callable") _fn = ensure_async_callable(fn) if not is_async_callable(fn): update_wrapper(_fn, fn) self.fn = self.wrap_in_error_handler(_fn) return self @staticmethod def wrap_in_error_handler(fn: AsyncAnyCallable) -> AsyncAnyCallable: """Wrap a listener function to handle errors. Listeners are executed concurrently in a TaskGroup, so we need to ensure that exceptions do not propagate to the task group which results in any other unfinished listeners to be cancelled, and the receive stream to be closed. See https://github.com/litestar-org/litestar/issues/2809 Args: fn: The listener function to wrap. """ async def wrapped(*args: Any, **kwargs: Any) -> None: """Wrap a listener function to handle errors.""" try: await fn(*args, **kwargs) except Exception as exc: logger.exception("Error while executing listener %s: %s", fn.__name__, exc) return wrapped def __hash__(self) -> int: return hash(self.event_ids) + hash(self.fn) listener = EventListener litestar-2.16.0/litestar/exceptions/000077500000000000000000000000001500564371300174435ustar00rootroot00000000000000litestar-2.16.0/litestar/exceptions/__init__.py000066400000000000000000000024261500564371300215600ustar00rootroot00000000000000from .base_exceptions import LitestarException, LitestarWarning, MissingDependencyException, SerializationException from .dto_exceptions import DTOFactoryException, InvalidAnnotationException from .http_exceptions import ( ClientException, HTTPException, ImproperlyConfiguredException, InternalServerException, MethodNotAllowedException, NoRouteMatchFoundException, NotAuthorizedException, NotFoundException, PermissionDeniedException, ServiceUnavailableException, TemplateNotFoundException, TooManyRequestsException, ValidationException, ) from .websocket_exceptions import WebSocketDisconnect, WebSocketException __all__ = ( "ClientException", "DTOFactoryException", "HTTPException", "ImproperlyConfiguredException", "InternalServerException", "InvalidAnnotationException", "LitestarException", "LitestarWarning", "MethodNotAllowedException", "MissingDependencyException", "NoRouteMatchFoundException", "NotAuthorizedException", "NotFoundException", "PermissionDeniedException", "SerializationException", "ServiceUnavailableException", "TemplateNotFoundException", "TooManyRequestsException", "ValidationException", "WebSocketDisconnect", "WebSocketException", ) litestar-2.16.0/litestar/exceptions/base_exceptions.py000066400000000000000000000036541500564371300232000ustar00rootroot00000000000000from __future__ import annotations from typing import Any __all__ = ("LitestarException", "LitestarWarning", "MissingDependencyException", "SerializationException") class LitestarException(Exception): """Base exception class from which all Litestar exceptions inherit.""" detail: str def __init__(self, *args: Any, detail: str = "") -> None: """Initialize ``LitestarException``. Args: *args: args are converted to :class:`str` before passing to :class:`Exception` detail: detail of the exception. """ str_args = [str(arg) for arg in args if arg] if not detail: if str_args: detail, *str_args = str_args elif hasattr(self, "detail"): detail = self.detail self.detail = detail super().__init__(*str_args) def __repr__(self) -> str: if self.detail: return f"{self.__class__.__name__} - {self.detail}" return self.__class__.__name__ def __str__(self) -> str: return " ".join((*self.args, self.detail)).strip() class MissingDependencyException(LitestarException, ImportError): """Missing optional dependency. This exception is raised only when a module depends on a dependency that has not been installed. """ def __init__(self, package: str, install_package: str | None = None, extra: str | None = None) -> None: super().__init__( f"Package {package!r} is not installed but required. You can install it by running " f"'pip install litestar[{extra or install_package or package}]' to install litestar with the required extra " f"or 'pip install {install_package or package}' to install the package separately" ) class SerializationException(LitestarException): """Encoding or decoding of an object failed.""" class LitestarWarning(UserWarning): """Base class for Litestar warnings""" litestar-2.16.0/litestar/exceptions/dto_exceptions.py000066400000000000000000000005131500564371300230430ustar00rootroot00000000000000from __future__ import annotations from litestar.exceptions import LitestarException __all__ = ("DTOFactoryException", "InvalidAnnotationException") class DTOFactoryException(LitestarException): """Base DTO exception type.""" class InvalidAnnotationException(DTOFactoryException): """Unexpected DTO type argument.""" litestar-2.16.0/litestar/exceptions/http_exceptions.py000066400000000000000000000113451500564371300232410ustar00rootroot00000000000000from __future__ import annotations from http import HTTPStatus from typing import Any from litestar.exceptions.base_exceptions import LitestarException from litestar.status_codes import ( HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_405_METHOD_NOT_ALLOWED, HTTP_413_REQUEST_ENTITY_TOO_LARGE, HTTP_429_TOO_MANY_REQUESTS, HTTP_500_INTERNAL_SERVER_ERROR, HTTP_503_SERVICE_UNAVAILABLE, ) __all__ = ( "ClientException", "HTTPException", "ImproperlyConfiguredException", "InternalServerException", "MethodNotAllowedException", "NoRouteMatchFoundException", "NotAuthorizedException", "NotFoundException", "PermissionDeniedException", "ServiceUnavailableException", "TemplateNotFoundException", "TooManyRequestsException", "ValidationException", ) class HTTPException(LitestarException): """Base exception for HTTP error responses. These exceptions carry information to construct an HTTP response. """ status_code: int = HTTP_500_INTERNAL_SERVER_ERROR """Exception status code.""" detail: str """Exception details or message.""" headers: dict[str, str] | None """Headers to attach to the response.""" extra: dict[str, Any] | list[Any] | None """An extra mapping to attach to the exception.""" def __init__( self, *args: Any, detail: str = "", status_code: int | None = None, headers: dict[str, str] | None = None, extra: dict[str, Any] | list[Any] | None = None, ) -> None: """Initialize ``HTTPException``. Set ``detail`` and ``args`` if not provided. Args: *args: if ``detail`` kwarg not provided, first arg should be error detail. detail: Exception details or message. Will default to args[0] if not provided. status_code: Exception HTTP status code. headers: Headers to set on the response. extra: An extra mapping to attach to the exception. """ super().__init__(*args, detail=detail) self.status_code = status_code or self.status_code self.extra = extra self.headers = headers if not self.detail: self.detail = HTTPStatus(self.status_code).phrase self.args = (f"{self.status_code}: {self.detail}", *self.args) def __repr__(self) -> str: return f"{self.status_code} - {self.__class__.__name__} - {self.detail}" def __str__(self) -> str: return " ".join(self.args).strip() class ImproperlyConfiguredException(HTTPException, ValueError): """Application has improper configuration.""" class ClientException(HTTPException): """Client error.""" status_code: int = HTTP_400_BAD_REQUEST class ValidationException(ClientException, ValueError): """Client data validation error.""" class NotAuthorizedException(ClientException): """Request lacks valid authentication credentials for the requested resource.""" status_code = HTTP_401_UNAUTHORIZED class PermissionDeniedException(ClientException): """Request understood, but not authorized.""" status_code = HTTP_403_FORBIDDEN class NotFoundException(ClientException, ValueError): """Cannot find the requested resource.""" status_code = HTTP_404_NOT_FOUND class MethodNotAllowedException(ClientException): """Server knows the request method, but the target resource doesn't support this method.""" status_code = HTTP_405_METHOD_NOT_ALLOWED class RequestEntityTooLarge(ClientException): status_code = HTTP_413_REQUEST_ENTITY_TOO_LARGE detail = "Request Entity Too Large" class TooManyRequestsException(ClientException): """Request limits have been exceeded.""" status_code = HTTP_429_TOO_MANY_REQUESTS class InternalServerException(HTTPException): """Server encountered an unexpected condition that prevented it from fulfilling the request.""" status_code: int = HTTP_500_INTERNAL_SERVER_ERROR class ServiceUnavailableException(InternalServerException): """Server is not ready to handle the request.""" status_code = HTTP_503_SERVICE_UNAVAILABLE class NoRouteMatchFoundException(InternalServerException): """A route with the given name could not be found.""" class TemplateNotFoundException(InternalServerException): """Referenced template could not be found.""" def __init__(self, *args: Any, template_name: str) -> None: """Initialize ``TemplateNotFoundException``. Args: *args (Any): Passed through to ``super().__init__()`` - should not include ``detail``. template_name (str): Name of template that could not be found. """ super().__init__(*args, detail=f"Template {template_name} not found.") litestar-2.16.0/litestar/exceptions/responses/000077500000000000000000000000001500564371300214645ustar00rootroot00000000000000litestar-2.16.0/litestar/exceptions/responses/__init__.py000066400000000000000000000071361500564371300236040ustar00rootroot00000000000000from __future__ import annotations from dataclasses import asdict, dataclass, field from typing import Any from litestar import MediaType, Request, Response from litestar.exceptions import HTTPException, LitestarException from litestar.exceptions.responses import _debug_response from litestar.serialization import encode_json, get_serializer from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR __all__ = ( "ExceptionResponseContent", "create_debug_response", "create_exception_response", ) @dataclass class ExceptionResponseContent: """Represent the contents of an exception-response.""" status_code: int """Exception status code.""" detail: str """Exception details or message.""" media_type: MediaType | str """Media type of the response.""" headers: dict[str, str] | None = field(default=None) """Headers to attach to the response.""" extra: dict[str, Any] | list[Any] | None = field(default=None) """An extra mapping to attach to the exception.""" def to_response(self, request: Request | None = None) -> Response: """Create a response from the model attributes. Returns: A response instance. """ content: Any = {k: v for k, v in asdict(self).items() if k not in ("headers", "media_type") and v is not None} type_encoders = _debug_response._get_type_encoders_for_request(request) if request is not None else None if self.media_type != MediaType.JSON: content = encode_json(content, get_serializer(type_encoders)) return Response( content=content, headers=self.headers, status_code=self.status_code, media_type=self.media_type, type_encoders=type_encoders, ) def create_exception_response(request: Request[Any, Any, Any], exc: Exception) -> Response: """Construct a response from an exception. Notes: - For instances of :class:`HTTPException ` or other exception classes that have a ``status_code`` attribute (e.g. Starlette exceptions), the status code is drawn from the exception, otherwise response status is ``HTTP_500_INTERNAL_SERVER_ERROR``. Args: request: The request that triggered the exception. exc: An exception. Returns: Response: HTTP response constructed from exception details. """ headers: dict[str, Any] | None extra: dict[str, Any] | list | None if isinstance(exc, HTTPException): status_code = exc.status_code headers = exc.headers extra = exc.extra else: status_code = HTTP_500_INTERNAL_SERVER_ERROR headers = None extra = None detail = ( exc.detail if isinstance(exc, LitestarException) and status_code != HTTP_500_INTERNAL_SERVER_ERROR else "Internal Server Error" ) try: media_type = request.route_handler.media_type except (KeyError, AttributeError): media_type = MediaType.JSON content = ExceptionResponseContent( status_code=status_code, detail=detail, headers=headers, extra=extra, media_type=media_type, ) return content.to_response(request=request) def create_debug_response(request: Request, exc: Exception) -> Response: """Create a debug response from an exception. Args: request: The request that triggered the exception. exc: An exception. Returns: Response: Debug response constructed from exception details. """ return _debug_response.create_debug_response(request, exc) litestar-2.16.0/litestar/exceptions/responses/_debug_response.py000066400000000000000000000174341500564371300252120ustar00rootroot00000000000000from __future__ import annotations from html import escape from inspect import getinnerframes from pathlib import Path from traceback import format_exception from typing import TYPE_CHECKING, Any from litestar.enums import MediaType from litestar.response import Response from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR from litestar.utils import get_name __all__ = ( "create_debug_response", "create_exception_html", "create_frame_html", "create_html_response_content", "create_line_html", "create_plain_text_response_content", "get_symbol_name", ) if TYPE_CHECKING: from inspect import FrameInfo from litestar.connection import Request from litestar.types import TypeEncodersMap tpl_dir = Path(__file__).parent / "templates" def get_symbol_name(frame: FrameInfo) -> str: """Return full name of the function that is being executed by the given frame. Args: frame: An instance of [FrameInfo](https://docs.python.org/3/library/inspect.html#inspect.FrameInfo). Notes: - class detection assumes standard names (self and cls) of params. - if current class name can not be determined only function (method) name will be returned. - we can not distinguish static methods from ordinary functions at the moment. Returns: A string containing full function name. """ locals_dict = frame.frame.f_locals # this piece assumes that the code uses standard names "self" and "cls" # in instance and class methods instance_or_cls = inst if (inst := locals_dict.get("self")) is not None else locals_dict.get("cls") classname = f"{get_name(instance_or_cls)}." if instance_or_cls is not None else "" return f"{classname}{frame.function}" def create_line_html( line: str, line_no: int, frame_index: int, idx: int, ) -> str: """Produce HTML representation of a line including real line number in the source code. Args: line: A string representing the current line. line_no: The line number associated with the executed line. frame_index: Index of the executed line in the code context. idx: Index of the current line in the code context. Returns: A string containing HTML representation of the given line. """ template = '{line_no}{line}' data = { # line_no - frame_index produces actual line number of the very first line in the frame code context. # so adding index (aka relative number) of a line in the code context we can calculate its actual number in the source file, "line_no": line_no - frame_index + idx, "line": escape(line).replace(" ", " "), "line_class": "executed-line" if idx == frame_index else "", } return template.format(**data) def create_frame_html(frame: FrameInfo, collapsed: bool) -> str: """Produce HTML representation of the given frame object including filename containing source code and name of the function being executed. Args: frame: An instance of [FrameInfo](https://docs.python.org/3/library/inspect.html#inspect.FrameInfo). collapsed: Flag controlling whether frame should be collapsed on the page load. Returns: A string containing HTML representation of the execution frame. """ frame_tpl = (tpl_dir / "frame.html").read_text() code_lines: list[str] = [ create_line_html(line, frame.lineno, frame.index or 0, idx) for idx, line in enumerate(frame.code_context or []) ] data = { "file": escape(frame.filename), "line": frame.lineno, "symbol_name": escape(get_symbol_name(frame)), "code": "".join(code_lines), "frame_class": "collapsed" if collapsed else "", } return frame_tpl.format(**data) def create_exception_html(exc: BaseException, line_limit: int) -> str: """Produce HTML representation of the exception frames. Args: exc: An Exception instance to generate. line_limit: Number of lines of code context to return, which are centered around the executed line. Returns: A string containing HTML representation of the execution frames related to the exception. """ frames = getinnerframes(exc.__traceback__, line_limit) if exc.__traceback__ else [] result = [create_frame_html(frame=frame, collapsed=idx > 0) for idx, frame in enumerate(reversed(frames))] return "".join(result) def create_html_response_content(exc: Exception, request: Request, line_limit: int = 15) -> str: """Given an exception, produces its traceback in HTML. Args: exc: An Exception instance to render debug response from. request: A :class:`Request ` instance. line_limit: Number of lines of code context to return, which are centered around the executed line. Returns: A string containing HTML page with exception traceback. """ exception_data: list[str] = [create_exception_html(exc, line_limit)] cause = exc.__cause__ while cause: cause_data = create_exception_html(cause, line_limit) cause_header = '

The above exception was caused by

' cause_error_description = f"

{escape(str(cause))}

" cause_error = f"

{escape(cause.__class__.__name__)}

" exception_data.append( f'
{cause_header}{cause_error}{cause_error_description}{cause_data}
' ) cause = cause.__cause__ scripts = (tpl_dir / "scripts.js").read_text() styles = (tpl_dir / "styles.css").read_text() body_tpl = (tpl_dir / "body.html").read_text() return body_tpl.format( scripts=scripts, styles=styles, error=f"{escape(exc.__class__.__name__)} on {request.method} {escape(request.url.path)}", error_description=escape(str(exc)), exception_data="".join(exception_data), ) def create_plain_text_response_content(exc: Exception) -> str: """Given an exception, produces its traceback in plain text. Args: exc: An Exception instance to render debug response from. Returns: A string containing exception traceback. """ return "".join(format_exception(type(exc), value=exc, tb=exc.__traceback__)) def create_debug_response(request: Request, exc: Exception) -> Response: """Create debug response either in plain text or HTML depending on client capabilities. Args: request: A :class:`Request ` instance. exc: An Exception instance to render debug response from. Returns: A response with a rendered exception traceback. """ if MediaType.HTML in request.headers.get("accept", ""): content: Any = create_html_response_content(exc=exc, request=request) media_type = MediaType.HTML elif MediaType.JSON in request.headers.get("accept", ""): content = {"details": create_plain_text_response_content(exc), "status_code": HTTP_500_INTERNAL_SERVER_ERROR} media_type = MediaType.JSON else: content = create_plain_text_response_content(exc) media_type = MediaType.TEXT return Response( content=content, media_type=media_type, status_code=HTTP_500_INTERNAL_SERVER_ERROR, type_encoders=_get_type_encoders_for_request(request), ) def _get_type_encoders_for_request(request: Request) -> TypeEncodersMap | None: try: return request.route_handler.resolve_type_encoders() # we might be in a 404, or before we could resolve the handler, so this # could potentially error out. In this case we fall back on the application # type encoders except (KeyError, AttributeError): return request.app.type_encoders litestar-2.16.0/litestar/exceptions/responses/templates/000077500000000000000000000000001500564371300234625ustar00rootroot00000000000000litestar-2.16.0/litestar/exceptions/responses/templates/body.html000066400000000000000000000006161500564371300253100ustar00rootroot00000000000000 Litestar exception page

{error}

{error_description}

{exception_data} litestar-2.16.0/litestar/exceptions/responses/templates/frame.html000066400000000000000000000005301500564371300254400ustar00rootroot00000000000000
{file} in {symbol_name} at line {line}
{code}
litestar-2.16.0/litestar/exceptions/responses/templates/scripts.js000066400000000000000000000016301500564371300255070ustar00rootroot00000000000000const expanders = document.querySelectorAll(".frame .expander"); for (const expander of expanders) { expander.addEventListener("click", (evt) => { const currentSnippet = evt.currentTarget.closest(".frame"); const snippetWrapper = currentSnippet.querySelector( ".code-snippet-wrapper", ); if (currentSnippet.classList.contains("collapsed")) { snippetWrapper.style.height = `${snippetWrapper.scrollHeight}px`; currentSnippet.classList.remove("collapsed"); } else { currentSnippet.classList.add("collapsed"); snippetWrapper.style.height = "0px"; } }); } // init height for non-collapsed code snippets so animation will be show // their first collapse const nonCollapsedSnippets = document.querySelectorAll( ".frame:not(.collapsed) .code-snippet-wrapper", ); for (const snippet of nonCollapsedSnippets) { snippet.style.height = `${snippet.scrollHeight}px`; } litestar-2.16.0/litestar/exceptions/responses/templates/styles.css000066400000000000000000000034461500564371300255260ustar00rootroot00000000000000:root { --code-background-color: #f5f5f5; --code-background-color-dark: #b8b8b8; --code-color: #1d2534; --code-color-light: #546996; --code-font-family: Consolas, monospace; --header-color: #303b55; --warn-color: hsl(356, 92%, 60%); --text-font-family: -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; } html { font-size: 20px; } body { font-family: var(--text-font-family); font-size: 0.8rem; } h1, h2, h3, h4 { color: var(--header-color); } h4 { font-size: 1rem; } h3 { font-size: 1.35rem; } h2 { font-size: 1.83rem; } h3 span, h4 span { color: var(--warn-color); } .frame { background-color: var(--code-background-color); border-radius: 0.2rem; margin-bottom: 20px; } .frame-name { border-bottom: 1px solid var(--code-color-light); padding: 10px 16px; } .frame.collapsed .frame-name { border-bottom: none; } .frame-name span { font-weight: 700; } span.expander { display: inline-block; margin-right: 10px; cursor: pointer; transition: transform 0.33s ease-in-out; } .frame.collapsed span.expander { transform: rotate(-90deg); } .frame-name span.breakable { word-break: break-all; } .code-snippet-wrapper { height: auto; overflow-y: hidden; transition: height 0.33s ease-in-out; } .frame.collapsed .code-snippet-wrapper { height: 0; } .code-snippet { margin: 10px 16px; border-spacing: 0 0; color: var(--code-color); font-family: var(--code-font-family); font-size: 0.68rem; } .code-snippet td { padding: 0; text-align: left; } td.line_no { color: var(--code-color-light); min-width: 4ch; padding-right: 20px; text-align: right; user-select: none; } td.code_line { width: 99%; } tr.executed-line { background-color: var(--code-background-color-dark); } .cause-wrapper { margin-top: 50px; } litestar-2.16.0/litestar/exceptions/websocket_exceptions.py000066400000000000000000000025071500564371300242500ustar00rootroot00000000000000from typing import Any from litestar.exceptions.base_exceptions import LitestarException from litestar.status_codes import WS_1000_NORMAL_CLOSURE __all__ = ("WebSocketDisconnect", "WebSocketException") class WebSocketException(LitestarException): """Exception class for websocket related events.""" code: int """Exception code. For custom exceptions, this should be a number in the 4000+ range. Other codes can be found in ``litestar.status_code`` with the ``WS_`` prefix. """ def __init__(self, *args: Any, detail: str, code: int = 4500) -> None: """Initialize ``WebSocketException``. Args: *args: Any exception args. detail: Exception details. code: Exception code. Should be a number in the >= 1000. """ super().__init__(*args, detail=detail) self.code = code class WebSocketDisconnect(WebSocketException): """Exception class for websocket disconnect events.""" def __init__(self, *args: Any, detail: str, code: int = WS_1000_NORMAL_CLOSURE) -> None: """Initialize ``WebSocketDisconnect``. Args: *args: Any exception args. detail: Exception details. code: Exception code. Should be a number in the >= 1000. """ super().__init__(*args, detail=detail, code=code) litestar-2.16.0/litestar/file_system.py000066400000000000000000000125011500564371300201560ustar00rootroot00000000000000from __future__ import annotations from stat import S_ISDIR from typing import TYPE_CHECKING, Any, AnyStr, cast from anyio import AsyncFile, Path, open_file from litestar.concurrency import sync_to_thread from litestar.exceptions import InternalServerException, NotAuthorizedException from litestar.types.file_types import FileSystemProtocol from litestar.utils.predicates import is_async_callable __all__ = ("BaseLocalFileSystem", "FileSystemAdapter") if TYPE_CHECKING: from os import stat_result from _typeshed import OpenBinaryMode from litestar.types import PathType from litestar.types.file_types import FileInfo class BaseLocalFileSystem(FileSystemProtocol): """Base class for a local file system.""" async def info(self, path: PathType, **kwargs: Any) -> FileInfo: """Retrieve information about a given file path. Args: path: A file path. **kwargs: Any additional kwargs. Returns: A dictionary of file info. """ result = await Path(path).stat() return await FileSystemAdapter.parse_stat_result(path=path, result=result) async def open(self, file: PathType, mode: str, buffering: int = -1) -> AsyncFile[AnyStr]: # pyright: ignore """Return a file-like object from the filesystem. Notes: - The return value must be a context-manager Args: file: Path to the target file. mode: Mode, similar to the built ``open``. buffering: Buffer size. """ return await open_file(file=file, mode=mode, buffering=buffering) # type: ignore[call-overload, no-any-return] class FileSystemAdapter: """Wrapper around a ``FileSystemProtocol``, normalising its interface.""" def __init__(self, file_system: FileSystemProtocol) -> None: """Initialize an adapter from a given ``file_system`` Args: file_system: A filesystem class adhering to the :class:`FileSystemProtocol ` """ self.file_system = file_system async def info(self, path: PathType) -> FileInfo: """Proxies the call to the underlying FS Spec's ``info`` method, ensuring it's done in an async fashion and with strong typing. Args: path: A file path to load the info for. Returns: A dictionary of file info. """ try: awaitable = ( self.file_system.info(str(path)) if is_async_callable(self.file_system.info) else sync_to_thread(self.file_system.info, str(path)) ) return cast("FileInfo", await awaitable) except FileNotFoundError as e: raise e except PermissionError as e: raise NotAuthorizedException(f"failed to read {path} due to missing permissions") from e except OSError as e: # pragma: no cover raise InternalServerException from e async def open( self, file: PathType, mode: OpenBinaryMode = "rb", buffering: int = -1, ) -> AsyncFile[bytes]: """Return a file-like object from the filesystem. Notes: - The return value must function correctly in a context ``with`` block. Args: file: Path to the target file. mode: Mode, similar to the built ``open``. buffering: Buffer size. """ try: if is_async_callable(self.file_system.open): # pyright: ignore return cast( "AsyncFile[bytes]", await self.file_system.open( file=file, mode=mode, buffering=buffering, ), ) return AsyncFile(await sync_to_thread(self.file_system.open, file, mode=mode, buffering=buffering)) # type: ignore[arg-type] except PermissionError as e: raise NotAuthorizedException(f"failed to open {file} due to missing permissions") from e except OSError as e: raise InternalServerException from e @staticmethod async def parse_stat_result(path: PathType, result: stat_result) -> FileInfo: """Convert a ``stat_result`` instance into a ``FileInfo``. Args: path: The file path for which the :func:`stat_result ` is provided. result: The :func:`stat_result ` instance. Returns: A dictionary of file info. """ file_info: FileInfo = { "created": result.st_ctime, "gid": result.st_gid, "ino": result.st_ino, "islink": await Path(path).is_symlink(), "mode": result.st_mode, "mtime": result.st_mtime, "name": str(path), "nlink": result.st_nlink, "size": result.st_size, "type": "directory" if S_ISDIR(result.st_mode) else "file", "uid": result.st_uid, } if file_info["islink"]: file_info["destination"] = str(await Path(path).readlink()).encode("utf-8") try: file_info["size"] = (await Path(path).stat()).st_size except OSError: # pragma: no cover file_info["size"] = result.st_size return file_info litestar-2.16.0/litestar/handlers/000077500000000000000000000000001500564371300170625ustar00rootroot00000000000000litestar-2.16.0/litestar/handlers/__init__.py000066400000000000000000000013621500564371300211750ustar00rootroot00000000000000from .asgi_handlers import ASGIRouteHandler, asgi from .base import BaseRouteHandler from .http_handlers import HTTPRouteHandler, delete, get, head, patch, post, put, route from .websocket_handlers import ( WebsocketListener, WebsocketListenerRouteHandler, WebsocketRouteHandler, send_websocket_stream, websocket, websocket_listener, websocket_stream, ) __all__ = ( "ASGIRouteHandler", "BaseRouteHandler", "HTTPRouteHandler", "WebsocketListener", "WebsocketListenerRouteHandler", "WebsocketRouteHandler", "asgi", "delete", "get", "head", "patch", "post", "put", "route", "send_websocket_stream", "websocket", "websocket_listener", "websocket_stream", ) litestar-2.16.0/litestar/handlers/asgi_handlers.py000066400000000000000000000114451500564371300222440ustar00rootroot00000000000000from __future__ import annotations import warnings from typing import TYPE_CHECKING, Any, Mapping, Sequence from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers.base import BaseRouteHandler from litestar.types.builtin_types import NoneType from litestar.utils.predicates import is_async_callable __all__ = ("ASGIRouteHandler", "asgi") if TYPE_CHECKING: from litestar import Litestar from litestar.types import ( ExceptionHandlersMap, Guard, MaybePartial, # noqa: F401 ) class ASGIRouteHandler(BaseRouteHandler): """ASGI Route Handler decorator. Use this decorator to decorate ASGI applications. """ __slots__ = ("copy_scope", "is_mount", "is_static") def __init__( self, path: str | Sequence[str] | None = None, *, exception_handlers: ExceptionHandlersMap | None = None, guards: Sequence[Guard] | None = None, name: str | None = None, opt: Mapping[str, Any] | None = None, is_mount: bool = False, is_static: bool = False, signature_namespace: Mapping[str, Any] | None = None, copy_scope: bool | None = None, **kwargs: Any, ) -> None: """Initialize ``ASGIRouteHandler``. Args: exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. name: A string identifying the route handler. opt: A string key mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. path: A path fragment for the route handler function or a list of path fragments. If not given defaults to ``/`` is_mount: A boolean dictating whether the handler's paths should be regarded as mount paths. Mount path accept any arbitrary paths that begin with the defined prefixed path. For example, a mount with the path ``/some-path/`` will accept requests for ``/some-path/`` and any sub path under this, e.g. ``/some-path/sub-path/`` etc. is_static: A boolean dictating whether the handler's paths should be regarded as static paths. Static paths are used to deliver static files. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. type_encoders: A mapping of types to callables that transform them into types supported for serialization. copy_scope: Copy the ASGI 'scope' before calling the mounted application. Should be set to 'True' unless side effects via scope mutations by the mounted ASGI application are intentional **kwargs: Any additional kwarg - will be set in the opt dictionary. """ self.is_mount = is_mount or is_static self.is_static = is_static self.copy_scope = copy_scope super().__init__( path, exception_handlers=exception_handlers, guards=guards, name=name, opt=opt, signature_namespace=signature_namespace, **kwargs, ) def on_registration(self, app: Litestar) -> None: super().on_registration(app) if self.copy_scope is None: warnings.warn( f"{self}: 'copy_scope' not set for ASGI handler. Leaving 'copy_scope' unset will warn about mounted " "ASGI applications modifying the scope. Set 'copy_scope=True' to ensure calling into mounted ASGI apps " "does not cause any side effects via scope mutations, or set 'copy_scope=False' if those mutations are " "desired. 'copy'scope' will default to 'True' in Litestar 3", category=DeprecationWarning, stacklevel=1, ) def _validate_handler_function(self) -> None: """Validate the route handler function once it's set by inspecting its return annotations.""" super()._validate_handler_function() if not self.parsed_fn_signature.return_type.is_subclass_of(NoneType): raise ImproperlyConfiguredException("ASGI handler functions should return 'None'") if any(key not in self.parsed_fn_signature.parameters for key in ("scope", "send", "receive")): raise ImproperlyConfiguredException( "ASGI handler functions should define 'scope', 'send' and 'receive' arguments" ) if not is_async_callable(self.fn): raise ImproperlyConfiguredException("Functions decorated with 'asgi' must be async functions") asgi = ASGIRouteHandler litestar-2.16.0/litestar/handlers/base.py000066400000000000000000000566241500564371300203630ustar00rootroot00000000000000from __future__ import annotations from copy import copy from functools import partial from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence, cast from litestar._signature import SignatureModel from litestar.di import Provide from litestar.dto import DTOData from litestar.exceptions import ImproperlyConfiguredException from litestar.plugins import DIPlugin, PluginRegistry from litestar.serialization import default_deserializer, default_serializer from litestar.types import ( Dependencies, Empty, ExceptionHandlersMap, Guard, Middleware, TypeDecodersSequence, TypeEncodersMap, ) from litestar.typing import FieldDefinition from litestar.utils import ensure_async_callable, get_name, normalize_path from litestar.utils.helpers import unwrap_partial from litestar.utils.signature import ParsedSignature, add_types_to_signature_namespace, merge_signature_namespaces if TYPE_CHECKING: from typing_extensions import Self from litestar._kwargs import KwargsModel from litestar.app import Litestar from litestar.connection import ASGIConnection from litestar.controller import Controller from litestar.dto import AbstractDTO from litestar.params import ParameterKwarg from litestar.router import Router from litestar.types import AnyCallable, AsyncAnyCallable, ExceptionHandler from litestar.types.empty import EmptyType from litestar.types.internal_types import PathParameterDefinition __all__ = ("BaseRouteHandler",) class BaseRouteHandler: """Base route handler. Serves as a subclass for all route handlers """ __slots__ = ( "_fn", "_parsed_data_field", "_parsed_fn_signature", "_parsed_return_field", "_resolved_data_dto", "_resolved_dependencies", "_resolved_guards", "_resolved_layered_parameters", "_resolved_return_dto", "_resolved_signature_namespace", "_resolved_type_decoders", "_resolved_type_encoders", "_signature_model", "dependencies", "dto", "exception_handlers", "guards", "middleware", "name", "opt", "owner", "paths", "return_dto", "signature_namespace", "type_decoders", "type_encoders", ) def __init__( self, path: str | Sequence[str] | None = None, *, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, exception_handlers: ExceptionHandlersMap | None = None, guards: Sequence[Guard] | None = None, middleware: Sequence[Middleware] | None = None, name: str | None = None, opt: Mapping[str, Any] | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, signature_namespace: Mapping[str, Any] | None = None, signature_types: Sequence[Any] | None = None, type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: """Initialize ``HTTPRouteHandler``. Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. middleware: A sequence of :class:`Middleware <.types.Middleware>`. name: A string identifying the route handler. opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. signature_types: A sequence of types for use in forward reference resolution during signature modelling. These types will be added to the signature namespace using their ``__name__`` attribute. type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. type_encoders: A mapping of types to callables that transform them into types supported for serialization. **kwargs: Any additional kwarg - will be set in the opt dictionary. """ self._parsed_fn_signature: ParsedSignature | EmptyType = Empty self._parsed_return_field: FieldDefinition | EmptyType = Empty self._parsed_data_field: FieldDefinition | None | EmptyType = Empty self._resolved_data_dto: type[AbstractDTO] | None | EmptyType = Empty self._resolved_dependencies: dict[str, Provide] | EmptyType = Empty self._resolved_guards: list[Guard] | EmptyType = Empty self._resolved_layered_parameters: dict[str, FieldDefinition] | EmptyType = Empty self._resolved_return_dto: type[AbstractDTO] | None | EmptyType = Empty self._resolved_signature_namespace: dict[str, Any] | EmptyType = Empty self._resolved_type_decoders: TypeDecodersSequence | EmptyType = Empty self._resolved_type_encoders: TypeEncodersMap | EmptyType = Empty self._signature_model: type[SignatureModel] | EmptyType = Empty self.dependencies = dependencies self.dto = dto self.exception_handlers = exception_handlers self.guards = guards self.middleware = middleware self.name = name self.opt = dict(opt or {}) self.opt.update(**kwargs) self.owner: Controller | Router | None = None self.return_dto = return_dto self.signature_namespace = add_types_to_signature_namespace( signature_types or [], dict(signature_namespace or {}) ) self.type_decoders = type_decoders self.type_encoders = type_encoders self.paths = ( {normalize_path(p) for p in path} if path and isinstance(path, list) else {normalize_path(path or "/")} # type: ignore[arg-type] ) def __call__(self, fn: AsyncAnyCallable) -> Self: """Replace a function with itself.""" self._fn = fn return self @property def handler_id(self) -> str: """A unique identifier used for generation of DTOs.""" return f"{self!s}::{sum(id(layer) for layer in self.ownership_layers)}" @property def default_deserializer(self) -> Callable[[Any, Any], Any]: """Get a default deserializer for the route handler. Returns: A default deserializer for the route handler. """ return partial(default_deserializer, type_decoders=self.resolve_type_decoders()) @property def default_serializer(self) -> Callable[[Any], Any]: """Get a default serializer for the route handler. Returns: A default serializer for the route handler. """ return partial(default_serializer, type_encoders=self.resolve_type_encoders()) @property def signature_model(self) -> type[SignatureModel]: """Get the signature model for the route handler. Returns: A signature model for the route handler. """ if self._signature_model is Empty: self._signature_model = SignatureModel.create( dependency_name_set=self.dependency_name_set, fn=cast("AnyCallable", self.fn), parsed_signature=self.parsed_fn_signature, data_dto=self.resolve_data_dto(), type_decoders=self.resolve_type_decoders(), ) return self._signature_model @property def fn(self) -> AsyncAnyCallable: """Get the handler function. Raises: ImproperlyConfiguredException: if handler fn is not set. Returns: Handler function """ if not hasattr(self, "_fn"): raise ImproperlyConfiguredException("No callable has been registered for this handler") return self._fn @property def parsed_fn_signature(self) -> ParsedSignature: """Return the parsed signature of the handler function. This method is memoized so the computation occurs only once. Returns: A ParsedSignature instance """ if self._parsed_fn_signature is Empty: self._parsed_fn_signature = ParsedSignature.from_fn( unwrap_partial(self.fn), self.resolve_signature_namespace() ) return self._parsed_fn_signature @property def parsed_return_field(self) -> FieldDefinition: if self._parsed_return_field is Empty: self._parsed_return_field = self.parsed_fn_signature.return_type return self._parsed_return_field @property def parsed_data_field(self) -> FieldDefinition | None: if self._parsed_data_field is Empty: self._parsed_data_field = self.parsed_fn_signature.parameters.get("data") return self._parsed_data_field @property def handler_name(self) -> str: """Get the name of the handler function. Raises: ImproperlyConfiguredException: if handler fn is not set. Returns: Name of the handler function """ return get_name(unwrap_partial(self.fn)) @property def dependency_name_set(self) -> set[str]: """Set of all dependency names provided in the handler's ownership layers.""" layered_dependencies = (layer.dependencies or {} for layer in self.ownership_layers) return {name for layer in layered_dependencies for name in layer} # pyright: ignore @property def ownership_layers(self) -> list[Self | Controller | Router]: """Return the handler layers from the app down to the route handler. ``app -> ... -> route handler`` """ layers = [] cur: Any = self while cur: layers.append(cur) cur = cur.owner return list(reversed(layers)) @property def app(self) -> Litestar: return cast("Litestar", self.ownership_layers[0]) def resolve_type_encoders(self) -> TypeEncodersMap: """Return a merged type_encoders mapping. This method is memoized so the computation occurs only once. Returns: A dict of type encoders """ if self._resolved_type_encoders is Empty: self._resolved_type_encoders = {} for layer in self.ownership_layers: if type_encoders := getattr(layer, "type_encoders", None): self._resolved_type_encoders.update(type_encoders) return cast("TypeEncodersMap", self._resolved_type_encoders) def resolve_type_decoders(self) -> TypeDecodersSequence: """Return a merged type_encoders mapping. This method is memoized so the computation occurs only once. Returns: A dict of type encoders """ if self._resolved_type_decoders is Empty: self._resolved_type_decoders = [] for layer in self.ownership_layers: if type_decoders := getattr(layer, "type_decoders", None): self._resolved_type_decoders.extend(list(type_decoders)) return cast("TypeDecodersSequence", self._resolved_type_decoders) def resolve_layered_parameters(self) -> dict[str, FieldDefinition]: """Return all parameters declared above the handler.""" if self._resolved_layered_parameters is Empty: parameter_kwargs: dict[str, ParameterKwarg] = {} for layer in self.ownership_layers: parameter_kwargs.update(getattr(layer, "parameters", {}) or {}) self._resolved_layered_parameters = { key: FieldDefinition.from_kwarg(name=key, annotation=parameter.annotation, kwarg_definition=parameter) for key, parameter in parameter_kwargs.items() } return self._resolved_layered_parameters def resolve_guards(self) -> list[Guard]: """Return all guards in the handlers scope, starting from highest to current layer.""" if self._resolved_guards is Empty: self._resolved_guards = [] for layer in self.ownership_layers: self._resolved_guards.extend(layer.guards or []) # pyright: ignore self._resolved_guards = cast( "list[Guard]", [ensure_async_callable(guard) for guard in self._resolved_guards] ) return self._resolved_guards def _get_plugin_registry(self) -> PluginRegistry | None: from litestar.app import Litestar root_owner = self.ownership_layers[0] if isinstance(root_owner, Litestar): return root_owner.plugins return None def resolve_dependencies(self) -> dict[str, Provide]: """Return all dependencies correlating to handler function's kwargs that exist in the handler's scope.""" plugin_registry = self._get_plugin_registry() if self._resolved_dependencies is Empty: self._resolved_dependencies = {} for layer in self.ownership_layers: for key, provider in (layer.dependencies or {}).items(): self._resolved_dependencies[key] = self._resolve_dependency( key=key, provider=provider, plugin_registry=plugin_registry ) return self._resolved_dependencies def _resolve_dependency( self, key: str, provider: Provide | AnyCallable, plugin_registry: PluginRegistry | None ) -> Provide: if not isinstance(provider, Provide): provider = Provide(provider) if self._resolved_dependencies is not Empty: # pragma: no cover self._validate_dependency_is_unique(dependencies=self._resolved_dependencies, key=key, provider=provider) if not getattr(provider, "parsed_fn_signature", None): dependency = unwrap_partial(provider.dependency) plugin: DIPlugin | None = None if plugin_registry: plugin = next( (p for p in plugin_registry.di if isinstance(p, DIPlugin) and p.has_typed_init(dependency)), None, ) if plugin: signature, init_type_hints = plugin.get_typed_init(dependency) provider.parsed_fn_signature = ParsedSignature.from_signature(signature, init_type_hints) else: provider.parsed_fn_signature = ParsedSignature.from_fn(dependency, self.resolve_signature_namespace()) if not getattr(provider, "signature_model", None): provider.signature_model = SignatureModel.create( dependency_name_set=self.dependency_name_set, fn=provider.dependency, parsed_signature=provider.parsed_fn_signature, data_dto=self.resolve_data_dto(), type_decoders=self.resolve_type_decoders(), ) return provider def resolve_middleware(self) -> list[Middleware]: """Build the middleware stack for the RouteHandler and return it. The middlewares are added from top to bottom (``app -> router -> controller -> route handler``) and then reversed. """ resolved_middleware: list[Middleware] = [] for layer in self.ownership_layers: resolved_middleware.extend(layer.middleware or []) # pyright: ignore return list(reversed(resolved_middleware)) def resolve_exception_handlers(self) -> ExceptionHandlersMap: """Resolve the exception_handlers by starting from the route handler and moving up. This method is memoized so the computation occurs only once. """ resolved_exception_handlers: dict[int | type[Exception], ExceptionHandler] = {} for layer in self.ownership_layers: resolved_exception_handlers.update(layer.exception_handlers or {}) # pyright: ignore return resolved_exception_handlers def resolve_opts(self) -> None: """Build the route handler opt dictionary by going from top to bottom. When merging keys from multiple layers, if the same key is defined by multiple layers, the value from the layer closest to the response handler will take precedence. """ opt: dict[str, Any] = {} for layer in self.ownership_layers: opt.update(layer.opt or {}) # pyright: ignore self.opt = opt def resolve_signature_namespace(self) -> dict[str, Any]: """Build the route handler signature namespace dictionary by going from top to bottom. When merging keys from multiple layers, if the same key is defined by multiple layers, the value from the layer closest to the response handler will take precedence. """ if self._resolved_layered_parameters is Empty: ns: dict[str, Any] = {} for layer in self.ownership_layers: merge_signature_namespaces( signature_namespace=ns, additional_signature_namespace=layer.signature_namespace ) self._resolved_signature_namespace = ns return cast("dict[str, Any]", self._resolved_signature_namespace) def resolve_data_dto(self) -> type[AbstractDTO] | None: """Resolve the data_dto by starting from the route handler and moving up. If a handler is found it is returned, otherwise None is set. This method is memoized so the computation occurs only once. Returns: An optional :class:`DTO type <.dto.base_dto.AbstractDTO>` """ if self._resolved_data_dto is Empty: if data_dtos := cast( "list[type[AbstractDTO] | None]", [layer.dto for layer in self.ownership_layers if layer.dto is not Empty], ): data_dto: type[AbstractDTO] | None = data_dtos[-1] elif self.parsed_data_field and ( plugins_for_data_type := [ plugin for plugin in self.app.plugins.serialization if self.parsed_data_field.match_predicate_recursively(plugin.supports_type) ] ): data_dto = plugins_for_data_type[0].create_dto_for_type(self.parsed_data_field) else: data_dto = None if self.parsed_data_field and data_dto: data_dto.create_for_field_definition( field_definition=self.parsed_data_field, handler_id=self.handler_id, ) self._resolved_data_dto = data_dto return self._resolved_data_dto def resolve_return_dto(self) -> type[AbstractDTO] | None: """Resolve the return_dto by starting from the route handler and moving up. If a handler is found it is returned, otherwise None is set. This method is memoized so the computation occurs only once. Returns: An optional :class:`DTO type <.dto.base_dto.AbstractDTO>` """ if self._resolved_return_dto is Empty: if return_dtos := cast( "list[type[AbstractDTO] | None]", [layer.return_dto for layer in self.ownership_layers if layer.return_dto is not Empty], ): return_dto: type[AbstractDTO] | None = return_dtos[-1] elif plugins_for_return_type := [ plugin for plugin in self.app.plugins.serialization if self.parsed_return_field.match_predicate_recursively(plugin.supports_type) ]: return_dto = plugins_for_return_type[0].create_dto_for_type(self.parsed_return_field) else: return_dto = self.resolve_data_dto() if return_dto and return_dto.is_supported_model_type_field(self.parsed_return_field): return_dto.create_for_field_definition( field_definition=self.parsed_return_field, handler_id=self.handler_id, ) self._resolved_return_dto = return_dto else: self._resolved_return_dto = None return self._resolved_return_dto async def authorize_connection(self, connection: ASGIConnection) -> None: """Ensure the connection is authorized by running all the route guards in scope.""" for guard in self.resolve_guards(): await guard(connection, copy(self)) # type: ignore[misc] @staticmethod def _validate_dependency_is_unique(dependencies: dict[str, Provide], key: str, provider: Provide) -> None: """Validate that a given provider has not been already defined under a different key.""" for dependency_key, value in dependencies.items(): if provider == value: raise ImproperlyConfiguredException( f"Provider for key {key} is already defined under the different key {dependency_key}. " f"If you wish to override a provider, it must have the same key." ) def on_registration(self, app: Litestar) -> None: """Called once per handler when the app object is instantiated. Args: app: The :class:`Litestar<.app.Litestar>` app object. Returns: None """ self._validate_handler_function() self.resolve_dependencies() self.resolve_guards() self.resolve_middleware() self.resolve_opts() self.resolve_data_dto() self.resolve_return_dto() def _validate_handler_function(self) -> None: """Validate the route handler function once set by inspecting its return annotations.""" if ( self.parsed_data_field is not None and self.parsed_data_field.is_subclass_of(DTOData) and not self.resolve_data_dto() ): raise ImproperlyConfiguredException( f"Handler function {self.handler_name} has a data parameter that is a subclass of DTOData but no " "DTO has been registered for it." ) def __str__(self) -> str: """Return a unique identifier for the route handler. Returns: A string """ target: type[AsyncAnyCallable] | AsyncAnyCallable # pyright: ignore target = unwrap_partial(self.fn) if not hasattr(target, "__qualname__"): target = type(target) return f"{target.__module__}.{target.__qualname__}" def create_kwargs_model( self, path_parameters: dict[str, PathParameterDefinition], ) -> KwargsModel: """Create a `KwargsModel` for a given route handler.""" from litestar._kwargs import KwargsModel return KwargsModel.create_for_signature_model( signature_model=self.signature_model, parsed_signature=self.parsed_fn_signature, dependencies=self.resolve_dependencies(), path_parameters=set(path_parameters.keys()), layered_parameters=self.resolve_layered_parameters(), ) litestar-2.16.0/litestar/handlers/http_handlers/000077500000000000000000000000001500564371300217215ustar00rootroot00000000000000litestar-2.16.0/litestar/handlers/http_handlers/__init__.py000066400000000000000000000004071500564371300240330ustar00rootroot00000000000000from __future__ import annotations from .base import HTTPRouteHandler, route from .decorators import delete, get, head, patch, post, put __all__ = ( "HTTPRouteHandler", "delete", "get", "head", "patch", "post", "put", "route", ) litestar-2.16.0/litestar/handlers/http_handlers/_utils.py000066400000000000000000000154601500564371300236000ustar00rootroot00000000000000from __future__ import annotations from functools import lru_cache from inspect import isawaitable from typing import TYPE_CHECKING, Any, Sequence, cast from litestar.enums import HttpMethod from litestar.exceptions import ValidationException from litestar.response import Response from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT from litestar.types.builtin_types import NoneType if TYPE_CHECKING: from litestar.app import Litestar from litestar.background_tasks import BackgroundTask, BackgroundTasks from litestar.connection import Request from litestar.datastructures import Cookie, ResponseHeader from litestar.types import AfterRequestHookHandler, ASGIApp, AsyncAnyCallable, Method, TypeEncodersMap from litestar.typing import FieldDefinition __all__ = ( "create_data_handler", "create_generic_asgi_response_handler", "create_response_handler", "get_default_status_code", "is_empty_response_annotation", "normalize_headers", "normalize_http_method", ) def create_data_handler( after_request: AfterRequestHookHandler | None, background: BackgroundTask | BackgroundTasks | None, cookies: frozenset[Cookie], headers: frozenset[ResponseHeader], media_type: str, response_class: type[Response], status_code: int, type_encoders: TypeEncodersMap | None, ) -> AsyncAnyCallable: """Create a handler function for arbitrary data. Args: after_request: An after request handler. background: A background task or background tasks. cookies: A set of pre-defined cookies. headers: A set of response headers. media_type: The response media type. response_class: The response class to use. status_code: The response status code. type_encoders: A mapping of types to encoder functions. Returns: A handler function. """ async def handler( data: Any, request: Request[Any, Any, Any], app: Litestar, **kwargs: Any, ) -> ASGIApp: if isawaitable(data): data = await data response = response_class( background=background, content=data, media_type=media_type, status_code=status_code, type_encoders=type_encoders, ) if after_request: response = await after_request(response) # type: ignore[arg-type,misc] return response.to_asgi_response(app=None, request=request, headers=normalize_headers(headers), cookies=cookies) # pyright: ignore return handler def create_generic_asgi_response_handler(after_request: AfterRequestHookHandler | None) -> AsyncAnyCallable: """Create a handler function for Responses. Args: after_request: An after request handler. Returns: A handler function. """ async def handler(data: ASGIApp, **kwargs: Any) -> ASGIApp: return await after_request(data) if after_request else data # type: ignore[arg-type, misc, no-any-return] return handler @lru_cache(1024) def normalize_headers(headers: frozenset[ResponseHeader]) -> dict[str, str]: """Given a dictionary of ResponseHeader, filter them and return a dictionary of values. Args: headers: A dictionary of :class:`ResponseHeader ` values Returns: A string keyed dictionary of normalized values """ return { header.name: cast("str", header.value) # we know value to be a string at this point because we validate it # that it's not None when initializing a header with documentation_only=True for header in headers if not header.documentation_only } def create_response_handler( after_request: AfterRequestHookHandler | None, background: BackgroundTask | BackgroundTasks | None, cookies: frozenset[Cookie], headers: frozenset[ResponseHeader], media_type: str, status_code: int, type_encoders: TypeEncodersMap | None, ) -> AsyncAnyCallable: """Create a handler function for Litestar Responses. Args: after_request: An after request handler. background: A background task or background tasks. cookies: A set of pre-defined cookies. headers: A set of response headers. media_type: The response media type. status_code: The response status code. type_encoders: A mapping of types to encoder functions. Returns: A handler function. """ normalized_headers = normalize_headers(headers) cookie_list = list(cookies) async def handler( data: Response, app: Litestar, request: Request, **kwargs: Any, # kwargs is for return dto ) -> ASGIApp: response = await after_request(data) if after_request else data # type:ignore[arg-type,misc] return response.to_asgi_response( # type: ignore[no-any-return] app=None, background=background, cookies=cookie_list, headers=normalized_headers, media_type=media_type, request=request, status_code=status_code, type_encoders=type_encoders, ) return handler def normalize_http_method(http_methods: HttpMethod | Method | Sequence[HttpMethod | Method]) -> set[Method]: """Normalize HTTP method(s) into a set of upper-case method names. Args: http_methods: A value for http method. Returns: A normalized set of http methods. """ output: set[str] = set() if isinstance(http_methods, str): http_methods = [http_methods] # pyright: ignore for method in http_methods: method_name = method.value.upper() if isinstance(method, HttpMethod) else method.upper() if method_name not in HTTP_METHOD_NAMES: raise ValidationException(f"Invalid HTTP method: {method_name}") output.add(method_name) return cast("set[Method]", output) def get_default_status_code(http_methods: set[Method]) -> int: """Return the default status code for a given set of HTTP methods. Args: http_methods: A set of method strings Returns: A status code """ if HttpMethod.POST in http_methods: return HTTP_201_CREATED if HttpMethod.DELETE in http_methods: return HTTP_204_NO_CONTENT return HTTP_200_OK def is_empty_response_annotation(return_annotation: FieldDefinition) -> bool: """Return whether the return annotation is an empty response. Args: return_annotation: A return annotation. Returns: Whether the return annotation is an empty response. """ return return_annotation.is_subclass_of(NoneType) or ( return_annotation.is_subclass_of(Response) and return_annotation.has_inner_subclass_of(NoneType) ) HTTP_METHOD_NAMES = {m.value for m in HttpMethod} litestar-2.16.0/litestar/handlers/http_handlers/base.py000066400000000000000000000747131500564371300232210ustar00rootroot00000000000000from __future__ import annotations from enum import Enum from typing import TYPE_CHECKING, AnyStr, Mapping, Sequence, TypedDict, cast from litestar._layers.utils import narrow_response_cookies, narrow_response_headers from litestar.connection import Request from litestar.datastructures.cookie import Cookie from litestar.datastructures.response_header import ResponseHeader from litestar.enums import HttpMethod, MediaType from litestar.exceptions import ( HTTPException, ImproperlyConfiguredException, ) from litestar.handlers.base import BaseRouteHandler from litestar.handlers.http_handlers._utils import ( create_data_handler, create_generic_asgi_response_handler, create_response_handler, get_default_status_code, is_empty_response_annotation, normalize_http_method, ) from litestar.openapi.spec import Operation from litestar.response import Response from litestar.status_codes import HTTP_204_NO_CONTENT, HTTP_304_NOT_MODIFIED from litestar.types import ( AfterRequestHookHandler, AfterResponseHookHandler, AnyCallable, ASGIApp, BeforeRequestHookHandler, CacheKeyBuilder, Dependencies, Empty, EmptyType, ExceptionHandlersMap, Guard, Method, Middleware, ResponseCookies, ResponseHeaders, TypeEncodersMap, ) from litestar.utils import ensure_async_callable from litestar.utils.predicates import is_async_callable from litestar.utils.warnings import warn_implicit_sync_to_thread, warn_sync_to_thread_with_async_callable if TYPE_CHECKING: from typing import Any, Awaitable, Callable from litestar.app import Litestar from litestar.background_tasks import BackgroundTask, BackgroundTasks from litestar.config.response_cache import CACHE_FOREVER from litestar.datastructures import CacheControlHeader, ETag from litestar.dto import AbstractDTO from litestar.openapi.datastructures import ResponseSpec from litestar.openapi.spec import SecurityRequirement from litestar.types.callable_types import AsyncAnyCallable, OperationIDCreator from litestar.types.composite_types import TypeDecodersSequence __all__ = ("HTTPRouteHandler", "route") class ResponseHandlerMap(TypedDict): default_handler: Callable[[Any], Awaitable[ASGIApp]] | EmptyType response_type_handler: Callable[[Any], Awaitable[ASGIApp]] | EmptyType class HTTPRouteHandler(BaseRouteHandler): """HTTP Route Decorator. Use this decorator to decorate an HTTP handler with multiple methods. """ __slots__ = ( "_resolved_after_response", "_resolved_before_request", "_resolved_include_in_schema", "_resolved_request_class", "_resolved_request_max_body_size", "_resolved_response_class", "_resolved_security", "_resolved_tags", "_response_handler_mapping", "after_request", "after_response", "background", "before_request", "cache", "cache_control", "cache_key_builder", "content_encoding", "content_media_type", "deprecated", "description", "etag", "has_sync_callable", "http_methods", "include_in_schema", "media_type", "operation_class", "operation_id", "raises", "request_class", "request_max_body_size", "response_class", "response_cookies", "response_description", "response_headers", "responses", "security", "status_code", "summary", "sync_to_thread", "tags", "template_name", ) has_sync_callable: bool def __init__( self, path: str | Sequence[str] | None = None, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, background: BackgroundTask | BackgroundTasks | None = None, before_request: BeforeRequestHookHandler | None = None, cache: bool | int | type[CACHE_FOREVER] = False, cache_control: CacheControlHeader | None = None, cache_key_builder: CacheKeyBuilder | None = None, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, guards: Sequence[Guard] | None = None, http_method: HttpMethod | Method | Sequence[HttpMethod | Method], media_type: MediaType | str | None = None, middleware: Sequence[Middleware] | None = None, name: str | None = None, opt: Mapping[str, Any] | None = None, request_class: type[Request] | None = None, request_max_body_size: int | None | EmptyType = Empty, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, status_code: int | None = None, sync_to_thread: bool | None = None, # OpenAPI related attributes content_encoding: str | None = None, content_media_type: str | None = None, deprecated: bool = False, description: str | None = None, include_in_schema: bool | EmptyType = Empty, operation_class: type[Operation] = Operation, operation_id: str | OperationIDCreator | None = None, raises: Sequence[type[HTTPException]] | None = None, response_description: str | None = None, responses: Mapping[int, ResponseSpec] | None = None, signature_namespace: Mapping[str, Any] | None = None, security: Sequence[SecurityRequirement] | None = None, summary: str | None = None, tags: Sequence[str] | None = None, type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: """Initialize ``HTTPRouteHandler``. Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed to any route handler. If this function returns a value, the request will not reach the route handler, and instead this value will be used. after_response: A sync or async function called after the response has been awaited. It receives the :class:`Request <.connection.Request>` object and should not return any values. background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. Defaults to ``None``. before_request: A sync or async function called immediately before calling the route handler. Receives the :class:`Request <.connection.Request>` instance and any non-``None`` return value is used for the response, bypassing the route handler. cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number of seconds (e.g. ``120``) to cache the response. cache_control: A ``cache-control`` header of type :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization of the cache key if caching is configured on the application level. dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. http_method: An :class:`http method string <.types.Method>`, a member of the enum :class:`HttpMethod <.enums.HttpMethod>` or a list of these that correlates to the methods the route handler function should handle. media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a valid IANA Media-Type. middleware: A sequence of :class:`Middleware <.types.Middleware>`. name: A string identifying the route handler. opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's default request. request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded, a '413 - Request Entity Too Large' error response is returned. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` instances. responses: A mapping of additional status codes and a description of their expected content. This information will be included in the OpenAPI schema return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. status_code: An http status code for the response. Defaults to ``200`` for ``GET``, ``PUT`` and ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. For mixed method requests it will check for ``POST`` and ``DELETE`` first then defaults to ``200``. sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the main event loop. This has an effect only for sync handler functions. See using sync handler functions. content_encoding: A string describing the encoding of the content, e.g. ``"base64"``. content_media_type: A string designating the media-type of the content, e.g. ``"image/png"``. deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. description: Text used for the route's schema description section. include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. This list should describe all exceptions raised within the route handler's function/method. The Litestar ValidationException will be added automatically for the schema if any validation is involved. response_description: Text used for the route's response schema description section. security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. summary: Text used for the route's schema summary section. tags: A sequence of string tags that will be appended to the OpenAPI schema. type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. type_encoders: A mapping of types to callables that transform them into types supported for serialization. **kwargs: Any additional kwarg - will be set in the opt dictionary. """ if not http_method: raise ImproperlyConfiguredException("An http_method kwarg is required") self.http_methods = normalize_http_method(http_methods=http_method) self.status_code = status_code or get_default_status_code(http_methods=self.http_methods) super().__init__( path=path, dependencies=dependencies, dto=dto, exception_handlers=exception_handlers, guards=guards, middleware=middleware, name=name, opt=opt, return_dto=return_dto, signature_namespace=signature_namespace, type_decoders=type_decoders, type_encoders=type_encoders, **kwargs, ) self.after_request = ensure_async_callable(after_request) if after_request else None # pyright: ignore self.after_response = ensure_async_callable(after_response) if after_response else None self.background = background self.before_request = ensure_async_callable(before_request) if before_request else None self.cache = cache self.cache_control = cache_control self.cache_key_builder = cache_key_builder self.etag = etag self.media_type: MediaType | str = media_type or "" self.request_class = request_class self.response_class = response_class self.response_cookies: Sequence[Cookie] | None = narrow_response_cookies(response_cookies) self.response_headers: Sequence[ResponseHeader] | None = narrow_response_headers(response_headers) self.request_max_body_size = request_max_body_size self.sync_to_thread = sync_to_thread # OpenAPI related attributes self.content_encoding = content_encoding self.content_media_type = content_media_type self.deprecated = deprecated self.description = description self.include_in_schema = include_in_schema self.operation_class = operation_class self.operation_id = operation_id self.raises = raises self.response_description = response_description self.summary = summary self.tags = tags self.security = security self.responses = responses # memoized attributes, defaulted to Empty self._resolved_after_response: AsyncAnyCallable | None | EmptyType = Empty self._resolved_before_request: AsyncAnyCallable | None | EmptyType = Empty self._response_handler_mapping: ResponseHandlerMap = {"default_handler": Empty, "response_type_handler": Empty} self._resolved_include_in_schema: bool | EmptyType = Empty self._resolved_response_class: type[Response] | EmptyType = Empty self._resolved_request_class: type[Request] | EmptyType = Empty self._resolved_security: list[SecurityRequirement] | EmptyType = Empty self._resolved_tags: list[str] | EmptyType = Empty self._resolved_request_max_body_size: int | EmptyType | None = Empty def __call__(self, fn: AnyCallable) -> HTTPRouteHandler: """Replace a function with itself.""" if not is_async_callable(fn): if self.sync_to_thread is None: warn_implicit_sync_to_thread(fn, stacklevel=3) elif self.sync_to_thread is not None: warn_sync_to_thread_with_async_callable(fn, stacklevel=3) super().__call__(fn) return self def resolve_request_class(self) -> type[Request]: """Return the closest custom Request class in the owner graph or the default Request class. This method is memoized so the computation occurs only once. Returns: The default :class:`Request <.connection.Request>` class for the route handler. """ if self._resolved_request_class is Empty: self._resolved_request_class = next( (layer.request_class for layer in reversed(self.ownership_layers) if layer.request_class is not None), Request, ) return cast("type[Request]", self._resolved_request_class) def resolve_response_class(self) -> type[Response]: """Return the closest custom Response class in the owner graph or the default Response class. This method is memoized so the computation occurs only once. Returns: The default :class:`Response <.response.Response>` class for the route handler. """ if self._resolved_response_class is Empty: self._resolved_response_class = next( (layer.response_class for layer in reversed(self.ownership_layers) if layer.response_class is not None), Response, ) return cast("type[Response]", self._resolved_response_class) def resolve_response_headers(self) -> frozenset[ResponseHeader]: """Return all header parameters in the scope of the handler function. Returns: A dictionary mapping keys to :class:`ResponseHeader <.datastructures.ResponseHeader>` instances. """ resolved_response_headers: dict[str, ResponseHeader] = {} for layer in self.ownership_layers: if layer_response_headers := layer.response_headers: if isinstance(layer_response_headers, Mapping): # this can't happen unless you manually set response_headers on an instance, which would result in a # type-checking error on everything but the controller. We cover this case nevertheless resolved_response_headers.update( {name: ResponseHeader(name=name, value=value) for name, value in layer_response_headers.items()} ) else: resolved_response_headers.update({h.name: h for h in layer_response_headers}) for extra_header in ("cache_control", "etag"): if header_model := getattr(layer, extra_header, None): resolved_response_headers[header_model.HEADER_NAME] = ResponseHeader( name=header_model.HEADER_NAME, value=header_model.to_header(), documentation_only=header_model.documentation_only, ) return frozenset(resolved_response_headers.values()) def resolve_response_cookies(self) -> frozenset[Cookie]: """Return a list of Cookie instances. Filters the list to ensure each cookie key is unique. Returns: A list of :class:`Cookie <.datastructures.Cookie>` instances. """ response_cookies: set[Cookie] = set() for layer in reversed(self.ownership_layers): if layer_response_cookies := layer.response_cookies: if isinstance(layer_response_cookies, Mapping): # this can't happen unless you manually set response_cookies on an instance, which would result in a # type-checking error on everything but the controller. We cover this case nevertheless response_cookies.update( {Cookie(key=key, value=value) for key, value in layer_response_cookies.items()} ) else: response_cookies.update(cast("set[Cookie]", layer_response_cookies)) return frozenset(response_cookies) def resolve_before_request(self) -> AsyncAnyCallable | None: """Resolve the before_handler handler by starting from the route handler and moving up. If a handler is found it is returned, otherwise None is set. This method is memoized so the computation occurs only once. Returns: An optional :class:`before request lifecycle hook handler <.types.BeforeRequestHookHandler>` """ if self._resolved_before_request is Empty: before_request_handlers = [layer.before_request for layer in self.ownership_layers if layer.before_request] self._resolved_before_request = before_request_handlers[-1] if before_request_handlers else None return cast("AsyncAnyCallable | None", self._resolved_before_request) def resolve_after_response(self) -> AsyncAnyCallable | None: """Resolve the after_response handler by starting from the route handler and moving up. If a handler is found it is returned, otherwise None is set. This method is memoized so the computation occurs only once. Returns: An optional :class:`after response lifecycle hook handler <.types.AfterResponseHookHandler>` """ if self._resolved_after_response is Empty: after_response_handlers: list[AsyncAnyCallable] = [ layer.after_response # type: ignore[misc] for layer in self.ownership_layers if layer.after_response ] self._resolved_after_response = after_response_handlers[-1] if after_response_handlers else None return cast("AsyncAnyCallable | None", self._resolved_after_response) def resolve_include_in_schema(self) -> bool: """Resolve the 'include_in_schema' property by starting from the route handler and moving up. If 'include_in_schema' is found in any of the ownership layers, the last value found is returned. If not found in any layer, the default value ``True`` is returned. Returns: bool: The resolved 'include_in_schema' property. """ if self._resolved_include_in_schema is Empty: include_in_schemas = [ i.include_in_schema for i in self.ownership_layers if isinstance(i.include_in_schema, bool) ] self._resolved_include_in_schema = include_in_schemas[-1] if include_in_schemas else True return self._resolved_include_in_schema def resolve_security(self) -> list[SecurityRequirement]: """Resolve the security property by starting from the route handler and moving up. Security requirements are additive, so the security requirements of the route handler are the sum of all security requirements of the ownership layers. Returns: list[SecurityRequirement]: The resolved security property. """ if self._resolved_security is Empty: self._resolved_security = [] for layer in self.ownership_layers: if isinstance(layer.security, Sequence): self._resolved_security.extend(layer.security) return self._resolved_security def resolve_tags(self) -> list[str]: """Resolve the tags property by starting from the route handler and moving up. Tags are additive, so the tags of the route handler are the sum of all tags of the ownership layers. Returns: list[str]: A sorted list of unique tags. """ if self._resolved_tags is Empty: tag_set = set() for layer in self.ownership_layers: for tag in layer.tags or []: tag_set.add(tag) self._resolved_tags = sorted(tag_set) return self._resolved_tags def resolve_request_max_body_size(self) -> int | None: if (resolved_limits := self._resolved_request_max_body_size) is not Empty: return resolved_limits max_body_size = self._resolved_request_max_body_size = next( # pyright: ignore ( max_body_size for layer in reversed(self.ownership_layers) if (max_body_size := layer.request_max_body_size) is not Empty ), Empty, ) if max_body_size is Empty: raise ImproperlyConfiguredException( "'request_max_body_size' set to 'Empty' on all layers. To omit a limit, " "set 'request_max_body_size=None'" ) return max_body_size def get_response_handler(self, is_response_type_data: bool = False) -> Callable[[Any], Awaitable[ASGIApp]]: """Resolve the response_handler function for the route handler. This method is memoized so the computation occurs only once. Args: is_response_type_data: Whether to return a handler for 'Response' instances. Returns: Async Callable to handle an HTTP Request """ if self._response_handler_mapping["default_handler"] is Empty: after_request_handlers: list[AsyncAnyCallable] = [ layer.after_request # type: ignore[misc] for layer in self.ownership_layers if layer.after_request ] after_request = cast( "AfterRequestHookHandler | None", after_request_handlers[-1] if after_request_handlers else None, ) media_type = self.media_type.value if isinstance(self.media_type, Enum) else self.media_type response_class = self.resolve_response_class() headers = self.resolve_response_headers() cookies = self.resolve_response_cookies() type_encoders = self.resolve_type_encoders() return_type = self.parsed_fn_signature.return_type return_annotation = return_type.annotation self._response_handler_mapping["response_type_handler"] = response_type_handler = create_response_handler( after_request=after_request, background=self.background, cookies=cookies, headers=headers, media_type=media_type, status_code=self.status_code, type_encoders=type_encoders, ) if return_type.is_subclass_of(Response): self._response_handler_mapping["default_handler"] = response_type_handler elif is_async_callable(return_annotation) or return_annotation is ASGIApp: self._response_handler_mapping["default_handler"] = create_generic_asgi_response_handler( after_request=after_request ) else: self._response_handler_mapping["default_handler"] = create_data_handler( after_request=after_request, background=self.background, cookies=cookies, headers=headers, media_type=media_type, response_class=response_class, status_code=self.status_code, type_encoders=type_encoders, ) return cast( "Callable[[Any], Awaitable[ASGIApp]]", self._response_handler_mapping["response_type_handler"] if is_response_type_data else self._response_handler_mapping["default_handler"], ) async def to_response(self, app: Litestar, data: Any, request: Request) -> ASGIApp: """Return a :class:`Response <.response.Response>` from the handler by resolving and calling it. Args: app: The :class:`Litestar ` app instance data: Either an instance of a :class:`Response <.response.Response>`, a Response instance or an arbitrary value. request: A :class:`Request <.connection.Request>` instance Returns: A Response instance """ if return_dto_type := self.resolve_return_dto(): data = return_dto_type(request).data_to_encodable_type(data) response_handler = self.get_response_handler(is_response_type_data=isinstance(data, Response)) return await response_handler(app=app, data=data, request=request) # type: ignore[call-arg] def on_registration(self, app: Litestar) -> None: super().on_registration(app) self.resolve_after_response() self.resolve_include_in_schema() self.has_sync_callable = not is_async_callable(self.fn) if self.has_sync_callable and self.sync_to_thread: self._fn = ensure_async_callable(self.fn) self.has_sync_callable = False def _validate_handler_function(self) -> None: """Validate the route handler function once it is set by inspecting its return annotations.""" super()._validate_handler_function() return_type = self.parsed_fn_signature.return_type if return_type.annotation is Empty: raise ImproperlyConfiguredException( f"A return value of a route handler function {self} should be type annotated. " "If your function doesn't return a value, annotate it as returning 'None'." ) if ( self.status_code < 200 or self.status_code in {HTTP_204_NO_CONTENT, HTTP_304_NOT_MODIFIED} ) and not is_empty_response_annotation(return_type): raise ImproperlyConfiguredException( "A status code 204, 304 or in the range below 200 does not support a response body. " f"If {self} should return a value, change the route handler status code to an appropriate value.", ) if not self.media_type: if return_type.is_subclass_of((str, bytes)) or return_type.annotation is AnyStr: self.media_type = MediaType.TEXT elif not return_type.is_subclass_of(Response): self.media_type = MediaType.JSON if "socket" in self.parsed_fn_signature.parameters: raise ImproperlyConfiguredException("The 'socket' kwarg is not supported with http handlers") if "data" in self.parsed_fn_signature.parameters and "GET" in self.http_methods: raise ImproperlyConfiguredException("'data' kwarg is unsupported for 'GET' request handlers") if (body_param := self.parsed_fn_signature.parameters.get("body")) and not body_param.is_subclass_of(bytes): raise ImproperlyConfiguredException( f"Invalid type annotation for 'body' parameter in route handler {self}. 'body' will always receive the " f"raw request body as bytes but was annotated with '{body_param.raw!r}'. If you want to receive " "processed request data, use the 'data' parameter." ) route = HTTPRouteHandler litestar-2.16.0/litestar/handlers/http_handlers/decorators.py000066400000000000000000002036421500564371300244470ustar00rootroot00000000000000from __future__ import annotations import warnings from typing import TYPE_CHECKING from litestar.enums import HttpMethod, MediaType from litestar.exceptions import HTTPException, ImproperlyConfiguredException from litestar.openapi.spec import Operation from litestar.response.file import ASGIFileResponse, File from litestar.types import Empty, TypeDecodersSequence from litestar.utils import is_class_and_subclass from ._utils import is_empty_response_annotation from .base import HTTPRouteHandler if TYPE_CHECKING: from typing import Any, Mapping, Sequence from litestar.background_tasks import BackgroundTask, BackgroundTasks from litestar.config.response_cache import CACHE_FOREVER from litestar.connection import Request from litestar.datastructures import CacheControlHeader, ETag from litestar.dto import AbstractDTO from litestar.openapi.datastructures import ResponseSpec from litestar.openapi.spec import SecurityRequirement from litestar.response import Response from litestar.types import ( AfterRequestHookHandler, AfterResponseHookHandler, BeforeRequestHookHandler, CacheKeyBuilder, Dependencies, EmptyType, ExceptionHandlersMap, Guard, Middleware, ResponseCookies, ResponseHeaders, TypeEncodersMap, ) from litestar.types.callable_types import OperationIDCreator __all__ = ("delete", "get", "head", "patch", "post", "put") MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP = "semantic route handlers cannot define http_method" def _subclass_warning() -> None: warnings.warn( "Semantic HTTP route handler classes are deprecated and will be replaced by " "functional decorators in Litestar 3.0.", category=DeprecationWarning, stacklevel=2, ) class delete(HTTPRouteHandler): """DELETE Route Decorator. Use this decorator to decorate an HTTP handler for DELETE requests. """ def __init__( self, path: str | None | Sequence[str] = None, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, background: BackgroundTask | BackgroundTasks | None = None, before_request: BeforeRequestHookHandler | None = None, cache: bool | int | type[CACHE_FOREVER] = False, cache_control: CacheControlHeader | None = None, cache_key_builder: CacheKeyBuilder | None = None, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, guards: Sequence[Guard] | None = None, media_type: MediaType | str | None = None, middleware: Sequence[Middleware] | None = None, name: str | None = None, opt: Mapping[str, Any] | None = None, request_class: type[Request] | None = None, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, signature_namespace: Mapping[str, Any] | None = None, status_code: int | None = None, sync_to_thread: bool | None = None, # OpenAPI related attributes content_encoding: str | None = None, content_media_type: str | None = None, deprecated: bool = False, description: str | None = None, include_in_schema: bool | EmptyType = Empty, operation_class: type[Operation] = Operation, operation_id: str | OperationIDCreator | None = None, raises: Sequence[type[HTTPException]] | None = None, response_description: str | None = None, responses: Mapping[int, ResponseSpec] | None = None, security: Sequence[SecurityRequirement] | None = None, summary: str | None = None, tags: Sequence[str] | None = None, type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: """Initialize ``delete`` Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed to any route handler. If this function returns a value, the request will not reach the route handler, and instead this value will be used. after_response: A sync or async function called after the response has been awaited. It receives the :class:`Request <.connection.Request>` object and should not return any values. background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. Defaults to ``None``. before_request: A sync or async function called immediately before calling the route handler. Receives the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, bypassing the route handler. cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number of seconds (e.g. ``120``) to cache the response. cache_control: A ``cache-control`` header of type :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization of the cache key if caching is configured on the application level. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. http_method: An :class:`http method string <.types.Method>`, a member of the enum :class:`HttpMethod ` or a list of these that correlates to the methods the route handler function should handle. media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a valid IANA Media-Type. middleware: A sequence of :class:`Middleware <.types.Middleware>`. name: A string identifying the route handler. opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's default request. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` instances. responses: A mapping of additional status codes and a description of their expected content. This information will be included in the OpenAPI schema return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the main event loop. This has an effect only for sync handler functions. See using sync handler functions. content_encoding: A string describing the encoding of the content, e.g. ``base64``. content_media_type: A string designating the media-type of the content, e.g. ``image/png``. deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. description: Text used for the route's schema description section. include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. This list should describe all exceptions raised within the route handler's function/method. The Litestar ValidationException will be added automatically for the schema if any validation is involved. response_description: Text used for the route's response schema description section. security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. summary: Text used for the route's schema summary section. tags: A sequence of string tags that will be appended to the OpenAPI schema. type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. type_encoders: A mapping of types to callables that transform them into types supported for serialization. **kwargs: Any additional kwarg - will be set in the opt dictionary. """ if "http_method" in kwargs: raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) super().__init__( after_request=after_request, after_response=after_response, background=background, before_request=before_request, cache=cache, cache_control=cache_control, cache_key_builder=cache_key_builder, content_encoding=content_encoding, content_media_type=content_media_type, dependencies=dependencies, deprecated=deprecated, description=description, dto=dto, etag=etag, exception_handlers=exception_handlers, guards=guards, http_method=HttpMethod.DELETE, include_in_schema=include_in_schema, media_type=media_type, middleware=middleware, name=name, operation_class=operation_class, operation_id=operation_id, opt=opt, path=path, raises=raises, request_class=request_class, response_class=response_class, response_cookies=response_cookies, response_description=response_description, response_headers=response_headers, responses=responses, return_dto=return_dto, security=security, signature_namespace=signature_namespace, status_code=status_code, summary=summary, sync_to_thread=sync_to_thread, tags=tags, type_decoders=type_decoders, type_encoders=type_encoders, **kwargs, ) def __init_subclass__(cls, **kwargs: Any) -> None: _subclass_warning() class get(HTTPRouteHandler): """GET Route Decorator. Use this decorator to decorate an HTTP handler for GET requests. """ def __init__( self, path: str | None | Sequence[str] = None, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, background: BackgroundTask | BackgroundTasks | None = None, before_request: BeforeRequestHookHandler | None = None, cache: bool | int | type[CACHE_FOREVER] = False, cache_control: CacheControlHeader | None = None, cache_key_builder: CacheKeyBuilder | None = None, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, guards: Sequence[Guard] | None = None, media_type: MediaType | str | None = None, middleware: Sequence[Middleware] | None = None, name: str | None = None, opt: Mapping[str, Any] | None = None, request_class: type[Request] | None = None, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, signature_namespace: Mapping[str, Any] | None = None, status_code: int | None = None, sync_to_thread: bool | None = None, # OpenAPI related attributes content_encoding: str | None = None, content_media_type: str | None = None, deprecated: bool = False, description: str | None = None, include_in_schema: bool | EmptyType = Empty, operation_class: type[Operation] = Operation, operation_id: str | OperationIDCreator | None = None, raises: Sequence[type[HTTPException]] | None = None, response_description: str | None = None, responses: Mapping[int, ResponseSpec] | None = None, security: Sequence[SecurityRequirement] | None = None, summary: str | None = None, tags: Sequence[str] | None = None, type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: """Initialize ``get``. Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed to any route handler. If this function returns a value, the request will not reach the route handler, and instead this value will be used. after_response: A sync or async function called after the response has been awaited. It receives the :class:`Request <.connection.Request>` object and should not return any values. background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. Defaults to ``None``. before_request: A sync or async function called immediately before calling the route handler. Receives the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, bypassing the route handler. cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number of seconds (e.g. ``120``) to cache the response. cache_control: A ``cache-control`` header of type :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization of the cache key if caching is configured on the application level. dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. http_method: An :class:`http method string <.types.Method>`, a member of the enum :class:`HttpMethod ` or a list of these that correlates to the methods the route handler function should handle. media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a valid IANA Media-Type. middleware: A sequence of :class:`Middleware <.types.Middleware>`. name: A string identifying the route handler. opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's default request. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` instances. responses: A mapping of additional status codes and a description of their expected content. This information will be included in the OpenAPI schema return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the main event loop. This has an effect only for sync handler functions. See using sync handler functions. content_encoding: A string describing the encoding of the content, e.g. ``base64``. content_media_type: A string designating the media-type of the content, e.g. ``image/png``. deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. description: Text used for the route's schema description section. include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. This list should describe all exceptions raised within the route handler's function/method. The Litestar ValidationException will be added automatically for the schema if any validation is involved. response_description: Text used for the route's response schema description section. security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. summary: Text used for the route's schema summary section. tags: A sequence of string tags that will be appended to the OpenAPI schema. type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. type_encoders: A mapping of types to callables that transform them into types supported for serialization. **kwargs: Any additional kwarg - will be set in the opt dictionary. """ if "http_method" in kwargs: raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) super().__init__( after_request=after_request, after_response=after_response, background=background, before_request=before_request, cache=cache, cache_control=cache_control, cache_key_builder=cache_key_builder, content_encoding=content_encoding, content_media_type=content_media_type, dependencies=dependencies, deprecated=deprecated, description=description, dto=dto, etag=etag, exception_handlers=exception_handlers, guards=guards, http_method=HttpMethod.GET, include_in_schema=include_in_schema, media_type=media_type, middleware=middleware, name=name, operation_class=operation_class, operation_id=operation_id, opt=opt, path=path, raises=raises, request_class=request_class, response_class=response_class, response_cookies=response_cookies, response_description=response_description, response_headers=response_headers, responses=responses, return_dto=return_dto, security=security, signature_namespace=signature_namespace, status_code=status_code, summary=summary, sync_to_thread=sync_to_thread, tags=tags, type_decoders=type_decoders, type_encoders=type_encoders, **kwargs, ) def __init_subclass__(cls, **kwargs: Any) -> None: _subclass_warning() class head(HTTPRouteHandler): """HEAD Route Decorator. Use this decorator to decorate an HTTP handler for HEAD requests. """ def __init__( self, path: str | None | Sequence[str] = None, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, background: BackgroundTask | BackgroundTasks | None = None, before_request: BeforeRequestHookHandler | None = None, cache: bool | int | type[CACHE_FOREVER] = False, cache_control: CacheControlHeader | None = None, cache_key_builder: CacheKeyBuilder | None = None, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, guards: Sequence[Guard] | None = None, media_type: MediaType | str | None = None, middleware: Sequence[Middleware] | None = None, name: str | None = None, opt: Mapping[str, Any] | None = None, request_class: type[Request] | None = None, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, signature_namespace: Mapping[str, Any] | None = None, status_code: int | None = None, sync_to_thread: bool | None = None, # OpenAPI related attributes content_encoding: str | None = None, content_media_type: str | None = None, deprecated: bool = False, description: str | None = None, include_in_schema: bool | EmptyType = Empty, operation_class: type[Operation] = Operation, operation_id: str | OperationIDCreator | None = None, raises: Sequence[type[HTTPException]] | None = None, response_description: str | None = None, responses: Mapping[int, ResponseSpec] | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, security: Sequence[SecurityRequirement] | None = None, summary: str | None = None, tags: Sequence[str] | None = None, type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: """Initialize ``head``. Notes: - A response to a head request cannot include a body. See: [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD). Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed to any route handler. If this function returns a value, the request will not reach the route handler, and instead this value will be used. after_response: A sync or async function called after the response has been awaited. It receives the :class:`Request <.connection.Request>` object and should not return any values. background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. Defaults to ``None``. before_request: A sync or async function called immediately before calling the route handler. Receives the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, bypassing the route handler. cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number of seconds (e.g. ``120``) to cache the response. cache_control: A ``cache-control`` header of type :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization of the cache key if caching is configured on the application level. dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. http_method: An :class:`http method string <.types.Method>`, a member of the enum :class:`HttpMethod ` or a list of these that correlates to the methods the route handler function should handle. media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a valid IANA Media-Type. middleware: A sequence of :class:`Middleware <.types.Middleware>`. name: A string identifying the route handler. opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's default request. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` instances. responses: A mapping of additional status codes and a description of their expected content. This information will be included in the OpenAPI schema return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the main event loop. This has an effect only for sync handler functions. See using sync handler functions. content_encoding: A string describing the encoding of the content, e.g. ``base64``. content_media_type: A string designating the media-type of the content, e.g. ``image/png``. deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. description: Text used for the route's schema description section. include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. This list should describe all exceptions raised within the route handler's function/method. The Litestar ValidationException will be added automatically for the schema if any validation is involved. response_description: Text used for the route's response schema description section. security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. summary: Text used for the route's schema summary section. tags: A sequence of string tags that will be appended to the OpenAPI schema. type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. type_encoders: A mapping of types to callables that transform them into types supported for serialization. **kwargs: Any additional kwarg - will be set in the opt dictionary. """ if "http_method" in kwargs: raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) super().__init__( after_request=after_request, after_response=after_response, background=background, before_request=before_request, cache=cache, cache_control=cache_control, cache_key_builder=cache_key_builder, content_encoding=content_encoding, content_media_type=content_media_type, dependencies=dependencies, deprecated=deprecated, description=description, dto=dto, etag=etag, exception_handlers=exception_handlers, guards=guards, http_method=HttpMethod.HEAD, include_in_schema=include_in_schema, media_type=media_type, middleware=middleware, name=name, operation_class=operation_class, operation_id=operation_id, opt=opt, path=path, raises=raises, request_class=request_class, response_class=response_class, response_cookies=response_cookies, response_description=response_description, response_headers=response_headers, responses=responses, return_dto=return_dto, security=security, signature_namespace=signature_namespace, status_code=status_code, summary=summary, sync_to_thread=sync_to_thread, tags=tags, type_decoders=type_decoders, type_encoders=type_encoders, **kwargs, ) def __init_subclass__(cls, **kwargs: Any) -> None: _subclass_warning() def _validate_handler_function(self) -> None: """Validate the route handler function once it is set by inspecting its return annotations.""" super()._validate_handler_function() # we allow here File and File because these have special setting for head responses field_definition = self.parsed_fn_signature.return_type if not ( is_empty_response_annotation(field_definition) or is_class_and_subclass(field_definition.annotation, File) or is_class_and_subclass(field_definition.annotation, ASGIFileResponse) ): raise ImproperlyConfiguredException( f"{self}: Handlers for 'HEAD' requests must not return a value. Either return 'None' or a response type without a body." ) class patch(HTTPRouteHandler): """PATCH Route Decorator. Use this decorator to decorate an HTTP handler for PATCH requests. """ def __init__( self, path: str | None | Sequence[str] = None, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, background: BackgroundTask | BackgroundTasks | None = None, before_request: BeforeRequestHookHandler | None = None, cache: bool | int | type[CACHE_FOREVER] = False, cache_control: CacheControlHeader | None = None, cache_key_builder: CacheKeyBuilder | None = None, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, guards: Sequence[Guard] | None = None, media_type: MediaType | str | None = None, middleware: Sequence[Middleware] | None = None, name: str | None = None, opt: Mapping[str, Any] | None = None, request_class: type[Request] | None = None, request_max_body_size: int | None | EmptyType = Empty, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, signature_namespace: Mapping[str, Any] | None = None, status_code: int | None = None, sync_to_thread: bool | None = None, # OpenAPI related attributes content_encoding: str | None = None, content_media_type: str | None = None, deprecated: bool = False, description: str | None = None, include_in_schema: bool | EmptyType = Empty, operation_class: type[Operation] = Operation, operation_id: str | OperationIDCreator | None = None, raises: Sequence[type[HTTPException]] | None = None, response_description: str | None = None, responses: Mapping[int, ResponseSpec] | None = None, security: Sequence[SecurityRequirement] | None = None, summary: str | None = None, tags: Sequence[str] | None = None, type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: """Initialize ``patch``. Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed to any route handler. If this function returns a value, the request will not reach the route handler, and instead this value will be used. after_response: A sync or async function called after the response has been awaited. It receives the :class:`Request <.connection.Request>` object and should not return any values. background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. Defaults to ``None``. before_request: A sync or async function called immediately before calling the route handler. Receives the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, bypassing the route handler. cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number of seconds (e.g. ``120``) to cache the response. cache_control: A ``cache-control`` header of type :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization of the cache key if caching is configured on the application level. dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. http_method: An :class:`http method string <.types.Method>`, a member of the enum :class:`HttpMethod ` or a list of these that correlates to the methods the route handler function should handle. media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a valid IANA Media-Type. middleware: A sequence of :class:`Middleware <.types.Middleware>`. name: A string identifying the route handler. opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's default request. request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded, a '413 - Request Entity Too Large' error response is returned. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` instances. responses: A mapping of additional status codes and a description of their expected content. This information will be included in the OpenAPI schema return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the main event loop. This has an effect only for sync handler functions. See using sync handler functions. content_encoding: A string describing the encoding of the content, e.g. ``base64``. content_media_type: A string designating the media-type of the content, e.g. ``image/png``. deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. description: Text used for the route's schema description section. include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. This list should describe all exceptions raised within the route handler's function/method. The Litestar ValidationException will be added automatically for the schema if any validation is involved. response_description: Text used for the route's response schema description section. security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. summary: Text used for the route's schema summary section. tags: A sequence of string tags that will be appended to the OpenAPI schema. type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. type_encoders: A mapping of types to callables that transform them into types supported for serialization. **kwargs: Any additional kwarg - will be set in the opt dictionary. """ if "http_method" in kwargs: raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) super().__init__( after_request=after_request, after_response=after_response, background=background, before_request=before_request, cache=cache, cache_control=cache_control, cache_key_builder=cache_key_builder, content_encoding=content_encoding, content_media_type=content_media_type, dependencies=dependencies, deprecated=deprecated, description=description, dto=dto, etag=etag, exception_handlers=exception_handlers, guards=guards, http_method=HttpMethod.PATCH, include_in_schema=include_in_schema, media_type=media_type, middleware=middleware, name=name, operation_class=operation_class, operation_id=operation_id, opt=opt, path=path, raises=raises, request_class=request_class, request_max_body_size=request_max_body_size, response_class=response_class, response_cookies=response_cookies, response_description=response_description, response_headers=response_headers, responses=responses, return_dto=return_dto, security=security, signature_namespace=signature_namespace, status_code=status_code, summary=summary, sync_to_thread=sync_to_thread, tags=tags, type_decoders=type_decoders, type_encoders=type_encoders, **kwargs, ) def __init_subclass__(cls, **kwargs: Any) -> None: _subclass_warning() class post(HTTPRouteHandler): """POST Route Decorator. Use this decorator to decorate an HTTP handler for POST requests. """ def __init__( self, path: str | None | Sequence[str] = None, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, background: BackgroundTask | BackgroundTasks | None = None, before_request: BeforeRequestHookHandler | None = None, cache: bool | int | type[CACHE_FOREVER] = False, cache_control: CacheControlHeader | None = None, cache_key_builder: CacheKeyBuilder | None = None, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, guards: Sequence[Guard] | None = None, media_type: MediaType | str | None = None, middleware: Sequence[Middleware] | None = None, name: str | None = None, opt: Mapping[str, Any] | None = None, request_class: type[Request] | None = None, request_max_body_size: int | None | EmptyType = Empty, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, signature_namespace: Mapping[str, Any] | None = None, status_code: int | None = None, sync_to_thread: bool | None = None, # OpenAPI related attributes content_encoding: str | None = None, content_media_type: str | None = None, deprecated: bool = False, description: str | None = None, include_in_schema: bool | EmptyType = Empty, operation_class: type[Operation] = Operation, operation_id: str | OperationIDCreator | None = None, raises: Sequence[type[HTTPException]] | None = None, response_description: str | None = None, responses: Mapping[int, ResponseSpec] | None = None, security: Sequence[SecurityRequirement] | None = None, summary: str | None = None, tags: Sequence[str] | None = None, type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: """Initialize ``post`` Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed to any route handler. If this function returns a value, the request will not reach the route handler, and instead this value will be used. after_response: A sync or async function called after the response has been awaited. It receives the :class:`Request <.connection.Request>` object and should not return any values. background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. Defaults to ``None``. before_request: A sync or async function called immediately before calling the route handler. Receives the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, bypassing the route handler. cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number of seconds (e.g. ``120``) to cache the response. cache_control: A ``cache-control`` header of type :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization of the cache key if caching is configured on the application level. dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. http_method: An :class:`http method string <.types.Method>`, a member of the enum :class:`HttpMethod ` or a list of these that correlates to the methods the route handler function should handle. media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a valid IANA Media-Type. middleware: A sequence of :class:`Middleware <.types.Middleware>`. name: A string identifying the route handler. opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's default request. request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded, a '413 - Request Entity Too Large' error response is returned. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` instances. responses: A mapping of additional status codes and a description of their expected content. This information will be included in the OpenAPI schema return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the main event loop. This has an effect only for sync handler functions. See using sync handler functions. content_encoding: A string describing the encoding of the content, e.g. ``base64``. content_media_type: A string designating the media-type of the content, e.g. ``image/png``. deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. description: Text used for the route's schema description section. include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. This list should describe all exceptions raised within the route handler's function/method. The Litestar ValidationException will be added automatically for the schema if any validation is involved. response_description: Text used for the route's response schema description section. security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. summary: Text used for the route's schema summary section. tags: A sequence of string tags that will be appended to the OpenAPI schema. type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. type_encoders: A mapping of types to callables that transform them into types supported for serialization. **kwargs: Any additional kwarg - will be set in the opt dictionary. """ if "http_method" in kwargs: raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) super().__init__( after_request=after_request, after_response=after_response, background=background, before_request=before_request, cache=cache, cache_control=cache_control, cache_key_builder=cache_key_builder, content_encoding=content_encoding, content_media_type=content_media_type, dependencies=dependencies, deprecated=deprecated, description=description, dto=dto, exception_handlers=exception_handlers, etag=etag, guards=guards, http_method=HttpMethod.POST, include_in_schema=include_in_schema, media_type=media_type, middleware=middleware, name=name, operation_class=operation_class, operation_id=operation_id, opt=opt, path=path, raises=raises, request_class=request_class, request_max_body_size=request_max_body_size, response_class=response_class, response_cookies=response_cookies, response_description=response_description, response_headers=response_headers, responses=responses, return_dto=return_dto, signature_namespace=signature_namespace, security=security, status_code=status_code, summary=summary, sync_to_thread=sync_to_thread, tags=tags, type_decoders=type_decoders, type_encoders=type_encoders, **kwargs, ) def __init_subclass__(cls, **kwargs: Any) -> None: _subclass_warning() class put(HTTPRouteHandler): """PUT Route Decorator. Use this decorator to decorate an HTTP handler for PUT requests. """ def __init__( self, path: str | None | Sequence[str] = None, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, background: BackgroundTask | BackgroundTasks | None = None, before_request: BeforeRequestHookHandler | None = None, cache: bool | int | type[CACHE_FOREVER] = False, cache_control: CacheControlHeader | None = None, cache_key_builder: CacheKeyBuilder | None = None, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, guards: Sequence[Guard] | None = None, media_type: MediaType | str | None = None, middleware: Sequence[Middleware] | None = None, name: str | None = None, opt: Mapping[str, Any] | None = None, request_class: type[Request] | None = None, request_max_body_size: int | None | EmptyType = Empty, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, signature_namespace: Mapping[str, Any] | None = None, status_code: int | None = None, sync_to_thread: bool | None = None, # OpenAPI related attributes content_encoding: str | None = None, content_media_type: str | None = None, deprecated: bool = False, description: str | None = None, include_in_schema: bool | EmptyType = Empty, operation_class: type[Operation] = Operation, operation_id: str | OperationIDCreator | None = None, raises: Sequence[type[HTTPException]] | None = None, response_description: str | None = None, responses: Mapping[int, ResponseSpec] | None = None, security: Sequence[SecurityRequirement] | None = None, summary: str | None = None, tags: Sequence[str] | None = None, type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: """Initialize ``put`` Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed to any route handler. If this function returns a value, the request will not reach the route handler, and instead this value will be used. after_response: A sync or async function called after the response has been awaited. It receives the :class:`Request <.connection.Request>` object and should not return any values. background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. Defaults to ``None``. before_request: A sync or async function called immediately before calling the route handler. Receives the :class:`.connection.Request` instance and any non-``None`` return value is used for the response, bypassing the route handler. cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number of seconds (e.g. ``120``) to cache the response. cache_control: A ``cache-control`` header of type :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response. cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization of the cache key if caching is configured on the application level. dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. http_method: An :class:`http method string <.types.Method>`, a member of the enum :class:`HttpMethod ` or a list of these that correlates to the methods the route handler function should handle. media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a valid IANA Media-Type. middleware: A sequence of :class:`Middleware <.types.Middleware>`. name: A string identifying the route handler. opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's default request. request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded, a '413 - Request Entity Too Large' error response is returned. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` instances. responses: A mapping of additional status codes and a description of their expected content. This information will be included in the OpenAPI schema return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``. sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the main event loop. This has an effect only for sync handler functions. See using sync handler functions. content_encoding: A string describing the encoding of the content, e.g. ``base64``. content_media_type: A string designating the media-type of the content, e.g. ``image/png``. deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema. description: Text used for the route's schema description section. include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema. operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId. raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation. This list should describe all exceptions raised within the route handler's function/method. The Litestar ValidationException will be added automatically for the schema if any validation is involved. response_description: Text used for the route's response schema description section. security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint. summary: Text used for the route's schema summary section. tags: A sequence of string tags that will be appended to the OpenAPI schema. type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. type_encoders: A mapping of types to callables that transform them into types supported for serialization. **kwargs: Any additional kwarg - will be set in the opt dictionary. """ if "http_method" in kwargs: raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP) super().__init__( after_request=after_request, after_response=after_response, background=background, before_request=before_request, cache=cache, cache_control=cache_control, cache_key_builder=cache_key_builder, content_encoding=content_encoding, content_media_type=content_media_type, dependencies=dependencies, deprecated=deprecated, description=description, dto=dto, exception_handlers=exception_handlers, etag=etag, guards=guards, http_method=HttpMethod.PUT, include_in_schema=include_in_schema, media_type=media_type, middleware=middleware, name=name, operation_class=operation_class, operation_id=operation_id, opt=opt, path=path, raises=raises, request_class=request_class, request_max_body_size=request_max_body_size, response_class=response_class, response_cookies=response_cookies, response_description=response_description, response_headers=response_headers, responses=responses, return_dto=return_dto, security=security, signature_namespace=signature_namespace, status_code=status_code, summary=summary, sync_to_thread=sync_to_thread, tags=tags, type_decoders=type_decoders, type_encoders=type_encoders, **kwargs, ) def __init_subclass__(cls, **kwargs: Any) -> None: _subclass_warning() litestar-2.16.0/litestar/handlers/websocket_handlers/000077500000000000000000000000001500564371300227305ustar00rootroot00000000000000litestar-2.16.0/litestar/handlers/websocket_handlers/__init__.py000066400000000000000000000010761500564371300250450ustar00rootroot00000000000000from __future__ import annotations from litestar.handlers.websocket_handlers.listener import ( WebsocketListener, WebsocketListenerRouteHandler, websocket_listener, ) from litestar.handlers.websocket_handlers.route_handler import WebsocketRouteHandler, websocket from litestar.handlers.websocket_handlers.stream import send_websocket_stream, websocket_stream __all__ = ( "WebsocketListener", "WebsocketListenerRouteHandler", "WebsocketRouteHandler", "send_websocket_stream", "websocket", "websocket_listener", "websocket_stream", ) litestar-2.16.0/litestar/handlers/websocket_handlers/_utils.py000066400000000000000000000140441500564371300246040ustar00rootroot00000000000000from __future__ import annotations from functools import wraps from inspect import Parameter, Signature from typing import TYPE_CHECKING, Any, Callable, Coroutine, Dict from msgspec.json import Encoder as JsonEncoder from litestar.di import Provide from litestar.serialization import decode_json from litestar.types.builtin_types import NoneType from litestar.utils import ensure_async_callable from litestar.utils.helpers import unwrap_partial if TYPE_CHECKING: from litestar import WebSocket from litestar.handlers.websocket_handlers.listener import WebsocketListenerRouteHandler from litestar.types import AnyCallable from litestar.utils.signature import ParsedSignature def create_handle_receive(listener: WebsocketListenerRouteHandler) -> Callable[[WebSocket], Coroutine[Any, None, None]]: if data_dto := listener.resolve_data_dto(): async def handle_receive(socket: WebSocket) -> Any: received_data = await socket.receive_data(mode=listener._receive_mode) return data_dto(socket).decode_bytes( received_data.encode("utf-8") if isinstance(received_data, str) else received_data ) elif listener.parsed_data_field and listener.parsed_data_field.annotation is str: async def handle_receive(socket: WebSocket) -> Any: received_data = await socket.receive_data(mode=listener._receive_mode) return received_data.decode("utf-8") if isinstance(received_data, bytes) else received_data elif listener.parsed_data_field and listener.parsed_data_field.annotation is bytes: async def handle_receive(socket: WebSocket) -> Any: received_data = await socket.receive_data(mode=listener._receive_mode) return received_data.encode("utf-8") if isinstance(received_data, str) else received_data else: async def handle_receive(socket: WebSocket) -> Any: received_data = await socket.receive_data(mode=listener._receive_mode) return decode_json(value=received_data, type_decoders=socket.route_handler.resolve_type_decoders()) return handle_receive def create_handle_send( listener: WebsocketListenerRouteHandler, ) -> Callable[[WebSocket, Any], Coroutine[None, None, None]]: json_encoder = JsonEncoder(enc_hook=listener.default_serializer) if return_dto := listener.resolve_return_dto(): async def handle_send(socket: WebSocket, data: Any) -> None: encoded_data = return_dto(socket).data_to_encodable_type(data) data = json_encoder.encode(encoded_data) await socket.send_data(data=data, mode=listener._send_mode) elif listener.parsed_return_field.is_subclass_of((str, bytes)) or ( listener.parsed_return_field.is_optional and listener.parsed_return_field.has_inner_subclass_of((str, bytes)) ): async def handle_send(socket: WebSocket, data: Any) -> None: await socket.send_data(data=data, mode=listener._send_mode) else: async def handle_send(socket: WebSocket, data: Any) -> None: data = json_encoder.encode(data) await socket.send_data(data=data, mode=listener._send_mode) return handle_send class ListenerHandler: __slots__ = ("_can_send_data", "_fn", "_listener", "_pass_socket") def __init__( self, listener: WebsocketListenerRouteHandler, fn: AnyCallable, parsed_signature: ParsedSignature, namespace: dict[str, Any], ) -> None: self._can_send_data = not parsed_signature.return_type.is_subclass_of(NoneType) self._fn = ensure_async_callable(fn) self._listener = listener self._pass_socket = "socket" in parsed_signature.parameters async def __call__( self, *args: Any, socket: WebSocket, connection_lifespan_dependencies: Dict[str, Any], # noqa: UP006 **kwargs: Any, ) -> None: lifespan_manager = self._listener._connection_lifespan or self._listener.default_connection_lifespan handle_send = self._listener.resolve_send_handler() if self._can_send_data else None handle_receive = self._listener.resolve_receive_handler() if self._pass_socket: kwargs["socket"] = socket async with lifespan_manager(**connection_lifespan_dependencies): while True: received_data = await handle_receive(socket) data = await self._fn(*args, data=received_data, **kwargs) if handle_send: await handle_send(socket, data) def create_handler_signature(callback_signature: Signature) -> Signature: """Creates a :class:`Signature` for the handler function for signature modelling. This is required for two reasons: 1. the :class:`.handlers.WebsocketHandler` signature model cannot contain the ``data`` parameter, which is required for :class:`.handlers.websocket_listener` handlers. 2. the :class;`.handlers.WebsocketHandler` signature model must include the ``socket`` parameter, which is optional for :class:`.handlers.websocket_listener` handlers. Args: callback_signature: The :class:`Signature` of the listener callback. Returns: The :class:`Signature` for the listener callback as required for signature modelling. """ new_params = [p for p in callback_signature.parameters.values() if p.name != "data"] if "socket" not in callback_signature.parameters: new_params.append(Parameter(name="socket", kind=Parameter.KEYWORD_ONLY, annotation="WebSocket")) new_params.append( Parameter(name="connection_lifespan_dependencies", kind=Parameter.KEYWORD_ONLY, annotation="Dict[str, Any]") ) return callback_signature.replace(parameters=new_params) def create_stub_dependency(src: AnyCallable) -> Provide: """Create a stub dependency, accepting any kwargs defined in ``src``, and wrap it in ``Provide`` """ src = unwrap_partial(src) @wraps(src) async def stub(**kwargs: Any) -> Dict[str, Any]: # noqa: UP006 return kwargs return Provide(stub) litestar-2.16.0/litestar/handlers/websocket_handlers/listener.py000066400000000000000000000452421500564371300251360ustar00rootroot00000000000000from __future__ import annotations from abc import ABC, abstractmethod from contextlib import AbstractAsyncContextManager, asynccontextmanager from typing import ( TYPE_CHECKING, Any, AsyncGenerator, Callable, Dict, Mapping, Optional, cast, overload, ) from litestar._signature import SignatureModel from litestar.connection import WebSocket from litestar.exceptions import ImproperlyConfiguredException, WebSocketDisconnect from litestar.types import ( AnyCallable, Dependencies, Empty, EmptyType, ExceptionHandler, Guard, Middleware, TypeEncodersMap, ) from litestar.utils import ensure_async_callable from litestar.utils.signature import ParsedSignature, get_fn_type_hints from ._utils import ( ListenerHandler, create_handle_receive, create_handle_send, create_handler_signature, create_stub_dependency, ) from .route_handler import WebsocketRouteHandler if TYPE_CHECKING: from typing import Coroutine from typing_extensions import Self from litestar import Router from litestar.dto import AbstractDTO from litestar.types.asgi_types import WebSocketMode from litestar.types.composite_types import TypeDecodersSequence __all__ = ("WebsocketListener", "WebsocketListenerRouteHandler", "websocket_listener") class WebsocketListenerRouteHandler(WebsocketRouteHandler): """A websocket listener that automatically accepts a connection, handles disconnects, invokes a callback function every time new data is received and sends any data returned """ __slots__ = { # noqa: RUF023 "connection_accept_handler": "Callback to accept a WebSocket connection. By default, calls WebSocket.accept", "on_accept": "Callback invoked after a WebSocket connection has been accepted", "on_disconnect": "Callback invoked after a WebSocket connection has been closed", "_connection_lifespan": None, "_receive_handler": None, "_receive_mode": None, "_send_handler": None, "_send_mode": None, } @overload def __init__( self, path: str | list[str] | None = None, *, connection_lifespan: Callable[..., AbstractAsyncContextManager[Any]] | None = None, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, guards: list[Guard] | None = None, middleware: list[Middleware] | None = None, receive_mode: WebSocketMode = "text", send_mode: WebSocketMode = "text", name: str | None = None, opt: dict[str, Any] | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, signature_namespace: Mapping[str, Any] | None = None, type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, websocket_class: type[WebSocket] | None = None, **kwargs: Any, ) -> None: ... @overload def __init__( self, path: str | list[str] | None = None, *, connection_accept_handler: Callable[[WebSocket], Coroutine[Any, Any, None]] = WebSocket.accept, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, guards: list[Guard] | None = None, middleware: list[Middleware] | None = None, receive_mode: WebSocketMode = "text", send_mode: WebSocketMode = "text", name: str | None = None, on_accept: AnyCallable | None = None, on_disconnect: AnyCallable | None = None, opt: dict[str, Any] | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, signature_namespace: Mapping[str, Any] | None = None, type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, websocket_class: type[WebSocket] | None = None, **kwargs: Any, ) -> None: ... def __init__( self, path: str | list[str] | None = None, *, connection_accept_handler: Callable[[WebSocket], Coroutine[Any, Any, None]] = WebSocket.accept, connection_lifespan: Callable[..., AbstractAsyncContextManager[Any]] | None = None, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, guards: list[Guard] | None = None, middleware: list[Middleware] | None = None, receive_mode: WebSocketMode = "text", send_mode: WebSocketMode = "text", name: str | None = None, on_accept: AnyCallable | None = None, on_disconnect: AnyCallable | None = None, opt: dict[str, Any] | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, signature_namespace: Mapping[str, Any] | None = None, type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, websocket_class: type[WebSocket] | None = None, **kwargs: Any, ) -> None: """Initialize ``WebsocketRouteHandler`` Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` connection_accept_handler: A callable that accepts a :class:`WebSocket <.connection.WebSocket>` instance and returns a coroutine that when awaited, will accept the connection. Defaults to ``WebSocket.accept``. connection_lifespan: An asynchronous context manager, handling the lifespan of the connection. By default, it calls the ``connection_accept_handler``, ``on_connect`` and ``on_disconnect``. Can request any dependencies, for example the :class:`WebSocket <.connection.WebSocket>` connection dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. middleware: A sequence of :class:`Middleware <.types.Middleware>`. receive_mode: Websocket mode to receive data in, either `text` or `binary`. send_mode: Websocket mode to receive data in, either `text` or `binary`. name: A string identifying the route handler. on_accept: Callback invoked after a connection has been accepted. Can request any dependencies, for example the :class:`WebSocket <.connection.WebSocket>` connection on_disconnect: Callback invoked after a connection has been closed. Can request any dependencies, for example the :class:`WebSocket <.connection.WebSocket>` connection opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. type_encoders: A mapping of types to callables that transform them into types supported for serialization. **kwargs: Any additional kwarg - will be set in the opt dictionary. websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's default websocket class. """ if connection_lifespan and any([on_accept, on_disconnect, connection_accept_handler is not WebSocket.accept]): raise ImproperlyConfiguredException( "connection_lifespan can not be used with connection hooks " "(on_accept, on_disconnect, connection_accept_handler)", ) self._receive_mode: WebSocketMode = receive_mode self._send_mode: WebSocketMode = send_mode self._connection_lifespan = connection_lifespan self._send_handler: Callable[[WebSocket, Any], Coroutine[None, None, None]] | EmptyType = Empty self._receive_handler: Callable[[WebSocket], Any] | EmptyType = Empty self.connection_accept_handler = connection_accept_handler self.on_accept = ensure_async_callable(on_accept) if on_accept else None self.on_disconnect = ensure_async_callable(on_disconnect) if on_disconnect else None self.type_decoders = type_decoders self.type_encoders = type_encoders self.websocket_class = websocket_class listener_dependencies = dict(dependencies or {}) listener_dependencies["connection_lifespan_dependencies"] = create_stub_dependency( connection_lifespan or self.default_connection_lifespan ) if self.on_accept: listener_dependencies["on_accept_dependencies"] = create_stub_dependency(self.on_accept) if self.on_disconnect: listener_dependencies["on_disconnect_dependencies"] = create_stub_dependency(self.on_disconnect) super().__init__( path=path, dependencies=listener_dependencies, exception_handlers=exception_handlers, guards=guards, middleware=middleware, name=name, opt=opt, signature_namespace=signature_namespace, dto=dto, return_dto=return_dto, type_decoders=type_decoders, type_encoders=type_encoders, websocket_class=websocket_class, **kwargs, ) def __call__(self, fn: AnyCallable) -> Self: parsed_signature = ParsedSignature.from_fn(fn, self.resolve_signature_namespace()) if "data" not in parsed_signature.parameters: raise ImproperlyConfiguredException("Websocket listeners must accept a 'data' parameter") for param in ("request", "body"): if param in parsed_signature.parameters: raise ImproperlyConfiguredException(f"The {param} kwarg is not supported with websocket listeners") # we are manipulating the signature of the decorated function below, so we must store the original values for # use elsewhere. self._parsed_return_field = parsed_signature.return_type self._parsed_data_field = parsed_signature.parameters.get("data") self._parsed_fn_signature = ParsedSignature.from_signature( create_handler_signature(parsed_signature.original_signature), fn_type_hints={ **get_fn_type_hints(fn, namespace=self.resolve_signature_namespace()), **get_fn_type_hints(ListenerHandler.__call__, namespace=self.resolve_signature_namespace()), }, ) return super().__call__( ListenerHandler( listener=self, fn=fn, parsed_signature=parsed_signature, namespace=self.resolve_signature_namespace() ) ) def _validate_handler_function(self) -> None: """Validate the route handler function once it's set by inspecting its return annotations.""" # validation occurs in the call method @property def signature_model(self) -> type[SignatureModel]: """Get the signature model for the route handler. Returns: A signature model for the route handler. """ if self._signature_model is Empty: self._signature_model = SignatureModel.create( dependency_name_set=self.dependency_name_set, fn=cast("AnyCallable", self.fn), parsed_signature=self.parsed_fn_signature, type_decoders=self.resolve_type_decoders(), ) return self._signature_model @asynccontextmanager async def default_connection_lifespan( self, socket: WebSocket, on_accept_dependencies: Optional[Dict[str, Any]] = None, # noqa: UP006, UP007 on_disconnect_dependencies: Optional[Dict[str, Any]] = None, # noqa: UP006, UP007 ) -> AsyncGenerator[None, None]: """Handle the connection lifespan of a :class:`WebSocket <.connection.WebSocket>`. Args: socket: The :class:`WebSocket <.connection.WebSocket>` connection on_accept_dependencies: Dependencies requested by the :attr:`on_accept` hook on_disconnect_dependencies: Dependencies requested by the :attr:`on_disconnect` hook By, default this will - Call :attr:`connection_accept_handler` to accept a connection - Call :attr:`on_accept` if defined after a connection has been accepted - Call :attr:`on_disconnect` upon leaving the context """ await self.connection_accept_handler(socket) if self.on_accept: await self.on_accept(**(on_accept_dependencies or {})) try: yield except WebSocketDisconnect: pass finally: if self.on_disconnect: await self.on_disconnect(**(on_disconnect_dependencies or {})) def resolve_receive_handler(self) -> Callable[[WebSocket], Any]: if self._receive_handler is Empty: self._receive_handler = create_handle_receive(self) return self._receive_handler def resolve_send_handler(self) -> Callable[[WebSocket, Any], Coroutine[None, None, None]]: if self._send_handler is Empty: self._send_handler = create_handle_send(self) return self._send_handler websocket_listener = WebsocketListenerRouteHandler class WebsocketListener(ABC): path: str | list[str] | None = None """A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/``""" dependencies: Dependencies | None = None """A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.""" dto: type[AbstractDTO] | None | EmptyType = Empty """:class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data""" exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None """A mapping of status codes and/or exception types to handler functions.""" guards: list[Guard] | None = None """A sequence of :class:`Guard <.types.Guard>` callables.""" middleware: list[Middleware] | None = None """A sequence of :class:`Middleware <.types.Middleware>`.""" receive_mode: WebSocketMode = "text" """:class:`WebSocket <.connection.WebSocket>` mode to receive data in, either ``text`` or ``binary``.""" send_mode: WebSocketMode = "text" """Websocket mode to send data in, either `text` or `binary`.""" name: str | None = None """A string identifying the route handler.""" opt: dict[str, Any] | None = None """ A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. """ return_dto: type[AbstractDTO] | None | EmptyType = Empty """:class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data.""" signature_namespace: Mapping[str, Any] | None = None """ A mapping of names to types for use in forward reference resolution during signature modelling. """ type_decoders: TypeDecodersSequence | None = None """ type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. """ type_encoders: TypeEncodersMap | None = None """ type_encoders: A mapping of types to callables that transform them into types supported for serialization. """ websocket_class: type[WebSocket] | None = None """ websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's default websocket class. """ def __init__(self, owner: Router) -> None: """Initialize a WebsocketListener instance. Args: owner: The :class:`Router <.router.Router>` instance that owns this listener. """ self._owner = owner def to_handler(self) -> WebsocketListenerRouteHandler: on_accept = self.on_accept if self.on_accept != WebsocketListener.on_accept else None on_disconnect = self.on_disconnect if self.on_disconnect != WebsocketListener.on_disconnect else None handler = WebsocketListenerRouteHandler( dependencies=self.dependencies, dto=self.dto, exception_handlers=self.exception_handlers, guards=self.guards, middleware=self.middleware, send_mode=self.send_mode, receive_mode=self.receive_mode, name=self.name, on_accept=on_accept, on_disconnect=on_disconnect, opt=self.opt, path=self.path, return_dto=self.return_dto, signature_namespace=self.signature_namespace, type_decoders=self.type_decoders, type_encoders=self.type_encoders, websocket_class=self.websocket_class, )(self.on_receive) handler.owner = self._owner return handler def on_accept(self, *args: Any, **kwargs: Any) -> Any: """Called after a :class:`WebSocket <.connection.WebSocket>` connection has been accepted. Can receive any dependencies """ def on_disconnect(self, *args: Any, **kwargs: Any) -> Any: """Called after a :class:`WebSocket <.connection.WebSocket>` connection has been disconnected. Can receive any dependencies """ @abstractmethod def on_receive(self, *args: Any, **kwargs: Any) -> Any: """Called after data has been received from the WebSocket. This should take a ``data`` argument, receiving the processed WebSocket data, and can additionally include handler dependencies such as ``state``, or other regular dependencies. Data returned from this function will be serialized and sent via the socket according to handler configuration. """ raise NotImplementedError litestar-2.16.0/litestar/handlers/websocket_handlers/route_handler.py000066400000000000000000000106371500564371300261440ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Mapping from litestar.connection import WebSocket from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers import BaseRouteHandler from litestar.types.builtin_types import NoneType from litestar.utils.predicates import is_async_callable if TYPE_CHECKING: from litestar.types import Dependencies, ExceptionHandler, Guard, Middleware class WebsocketRouteHandler(BaseRouteHandler): """Websocket route handler decorator. Use this decorator to decorate websocket handler functions. """ __slots__ = ("websocket_class",) def __init__( self, path: str | list[str] | None = None, *, dependencies: Dependencies | None = None, exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, guards: list[Guard] | None = None, middleware: list[Middleware] | None = None, name: str | None = None, opt: dict[str, Any] | None = None, signature_namespace: Mapping[str, Any] | None = None, websocket_class: type[WebSocket] | None = None, **kwargs: Any, ) -> None: """Initialize ``WebsocketRouteHandler`` Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. middleware: A sequence of :class:`Middleware <.types.Middleware>`. name: A string identifying the route handler. opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. type_encoders: A mapping of types to callables that transform them into types supported for serialization. websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's default websocket class. **kwargs: Any additional kwarg - will be set in the opt dictionary. """ self.websocket_class = websocket_class super().__init__( path=path, dependencies=dependencies, exception_handlers=exception_handlers, guards=guards, middleware=middleware, name=name, opt=opt, signature_namespace=signature_namespace, **kwargs, ) def resolve_websocket_class(self) -> type[WebSocket]: """Return the closest custom WebSocket class in the owner graph or the default Websocket class. This method is memoized so the computation occurs only once. Returns: The default :class:`WebSocket <.connection.WebSocket>` class for the route handler. """ return next( (layer.websocket_class for layer in reversed(self.ownership_layers) if layer.websocket_class is not None), WebSocket, ) def _validate_handler_function(self) -> None: """Validate the route handler function once it's set by inspecting its return annotations.""" super()._validate_handler_function() if not self.parsed_fn_signature.return_type.is_subclass_of(NoneType): raise ImproperlyConfiguredException(f"{self}: WebSocket handlers must return 'None'") if "socket" not in self.parsed_fn_signature.parameters: raise ImproperlyConfiguredException(f"{self}: WebSocket handlers must define a 'socket' parameter") for param in ("request", "body", "data"): if param in self.parsed_fn_signature.parameters: raise ImproperlyConfiguredException( f"{self}: The {param} kwarg is not supported with websocket handlers" ) if not is_async_callable(self.fn): raise ImproperlyConfiguredException(f"{self}: WebSocket handler functions must be asynchronous") websocket = WebsocketRouteHandler litestar-2.16.0/litestar/handlers/websocket_handlers/stream.py000066400000000000000000000332241500564371300246010ustar00rootroot00000000000000from __future__ import annotations import dataclasses import functools import warnings from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Mapping, cast import anyio from msgspec.json import Encoder as JsonEncoder from typing_extensions import Self from litestar.exceptions import ImproperlyConfiguredException, LitestarWarning, WebSocketDisconnect from litestar.handlers.websocket_handlers.route_handler import WebsocketRouteHandler from litestar.types import Empty from litestar.types.builtin_types import NoneType from litestar.typing import FieldDefinition from litestar.utils.signature import ParsedSignature if TYPE_CHECKING: from litestar import Litestar, WebSocket from litestar.dto import AbstractDTO from litestar.types import Dependencies, EmptyType, ExceptionHandler, Guard, Middleware, TypeEncodersMap from litestar.types.asgi_types import WebSocketMode async def send_websocket_stream( socket: WebSocket, stream: AsyncGenerator[Any, Any], *, close: bool = True, mode: WebSocketMode = "text", send_handler: Callable[[WebSocket, Any], Awaitable[Any]] | None = None, listen_for_disconnect: bool = False, warn_on_data_discard: bool = True, ) -> None: """Stream data to the ``socket`` from an asynchronous generator. Example: Sending the current time to the connected client every 0.5 seconds: .. code-block:: python async def stream_current_time() -> AsyncGenerator[str, None]: while True: yield str(time.time()) await asyncio.sleep(0.5) @websocket("/time") async def time_handler(socket: WebSocket) -> None: await socket.accept() await send_websocket_stream( socket, stream_current_time(), listen_for_disconnect=True, ) Args: socket: The :class:`~litestar.connection.WebSocket` to send to stream: An asynchronous generator yielding data to send close: If ``True``, close the socket after the generator is exhausted mode: WebSocket mode to use for sending when no ``send_handler`` is specified send_handler: Callable to handle the send process. If ``None``, defaults to ``type(socket).send_data`` listen_for_disconnect: If ``True``, listen for client disconnects in the background. If a client disconnects, stop the generator and cancel sending data. Should always be ``True`` unless disconnects are handled elsewhere, for example by reading data from the socket concurrently. Should never be set to ``True`` when reading data from socket concurrently, as it can lead to data loss warn_on_data_discard: If ``True`` and ``listen_for_disconnect=True``, warn if during listening for client disconnects, data is received from the socket """ if send_handler is None: send_handler = functools.partial(type(socket).send_data, mode=mode) async def send_stream() -> None: try: # client might have disconnected elsewhere, so we stop sending while socket.connection_state != "disconnect": await send_handler(socket, await stream.__anext__()) except StopAsyncIteration: pass if listen_for_disconnect: # wrap 'send_stream' and disconnect listener, so they'll cancel the other once # one of the finishes async def wrapped_stream() -> None: await send_stream() # stream exhausted, we can stop listening for a disconnect tg.cancel_scope.cancel() async def disconnect_listener() -> None: try: # run this in a loop - we might receive other data than disconnects. # listen_for_disconnect is explicitly not safe when consuming WS data # in other places, so discarding that data here is fine while True: await socket.receive_data("text") if warn_on_data_discard: warnings.warn( "received data from websocket while listening for client " "disconnect in a websocket_stream. listen_for_disconnect " "is not safe to use when attempting to receive data from " "the same socket concurrently with a websocket_stream. set " "listen_for_disconnect=False if you're attempting to " "receive data from this socket or set " "warn_on_data_discard=False to disable this warning", stacklevel=2, category=LitestarWarning, ) except WebSocketDisconnect: # client disconnected, we can stop streaming tg.cancel_scope.cancel() async with anyio.create_task_group() as tg: tg.start_soon(wrapped_stream) tg.start_soon(disconnect_listener) else: await send_stream() if close and socket.connection_state != "disconnect": await socket.close() def websocket_stream( path: str | list[str] | None = None, *, dependencies: Dependencies | None = None, exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None, guards: list[Guard] | None = None, middleware: list[Middleware] | None = None, name: str | None = None, opt: dict[str, Any] | None = None, signature_namespace: Mapping[str, Any] | None = None, websocket_class: type[WebSocket] | None = None, mode: WebSocketMode = "text", return_dto: type[AbstractDTO] | None | EmptyType = Empty, type_encoders: TypeEncodersMap | None = None, listen_for_disconnect: bool = True, warn_on_data_discard: bool = True, **kwargs: Any, ) -> Callable[[Callable[..., AsyncGenerator[Any, Any]]], WebsocketRouteHandler]: """Create a WebSocket handler that accepts a connection and sends data to it from an async generator. Example: Sending the current time to the connected client every 0.5 seconds: .. code-block:: python @websocket_stream("/time") async def send_time() -> AsyncGenerator[str, None]: while True: yield str(time.time()) await asyncio.sleep(0.5) Args: path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/`` dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. middleware: A sequence of :class:`Middleware <.types.Middleware>`. name: A string identifying the route handler. opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's default websocket class. mode: WebSocket mode used for sending return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. type_encoders: A mapping of types to callables that transform them into types supported for serialization. listen_for_disconnect: If ``True``, listen for client disconnects in the background. If a client disconnects, stop the generator and cancel sending data. Should always be ``True`` unless disconnects are handled elsewhere, for example by reading data from the socket concurrently. Should never be set to ``True`` when reading data from socket concurrently, as it can lead to data loss warn_on_data_discard: If ``True`` and ``listen_for_disconnect=True``, warn if during listening for client disconnects, data is received from the socket **kwargs: Any additional kwarg - will be set in the opt dictionary. """ def decorator(fn: Callable[..., AsyncGenerator[Any, Any]]) -> WebsocketRouteHandler: return WebSocketStreamHandler( path=path, dependencies=dependencies, exception_handlers=exception_handlers, guard=guards, middleware=middleware, name=name, opt=opt, signature_namespace=signature_namespace, websocket_class=websocket_class, return_dto=return_dto, type_encoders=type_encoders, **kwargs, )( _WebSocketStreamOptions( generator_fn=fn, send_mode=mode, listen_for_disconnect=listen_for_disconnect, warn_on_data_discard=warn_on_data_discard, ) ) return decorator class WebSocketStreamHandler(WebsocketRouteHandler): __slots__ = ("_ws_stream_options",) _ws_stream_options: _WebSocketStreamOptions def __call__(self, fn: _WebSocketStreamOptions) -> Self: # type: ignore[override] self._ws_stream_options = fn self._fn = self._ws_stream_options.generator_fn # type: ignore[assignment] return self def on_registration(self, app: Litestar) -> None: parsed_handler_signature = parsed_stream_fn_signature = ParsedSignature.from_fn( self.fn, self.resolve_signature_namespace() ) if not parsed_stream_fn_signature.return_type.is_subclass_of(AsyncGenerator): raise ImproperlyConfiguredException( f"Route handler {self}: 'websocket_stream' handlers must return an " f"'AsyncGenerator', not {type(parsed_stream_fn_signature.return_type.raw)!r}" ) # important not to use 'self._ws_stream_options.generator_fn' here; This would # break in cases the decorator has been used inside a controller, as it would # be a reference to the unbound method. The bound method is patched in later # after the controller has been initialized. This is a workaround that should # go away with v3.0's static handlers stream_fn = cast(Callable[..., AsyncGenerator[Any, Any]], self.fn) # construct a fake signature for the kwargs modelling, using the generator # function passed to the handler as a base, to include all the dependencies, # params, injection kwargs, etc. + 'socket', so DI works properly, but the # signature looks to kwargs/signature modelling like a plain '@websocket' # handler that returns 'None' parsed_handler_signature = dataclasses.replace( parsed_handler_signature, return_type=FieldDefinition.from_annotation(NoneType) ) receives_socket_parameter = "socket" in parsed_stream_fn_signature.parameters if not receives_socket_parameter: parsed_handler_signature = dataclasses.replace( parsed_handler_signature, parameters={ **parsed_handler_signature.parameters, "socket": FieldDefinition.from_annotation("WebSocket", name="socket"), }, ) self._parsed_fn_signature = parsed_handler_signature self._parsed_return_field = parsed_stream_fn_signature.return_type.inner_types[0] json_encoder = JsonEncoder(enc_hook=self.default_serializer) return_dto = self.resolve_return_dto() # make sure the closure doesn't capture self._ws_stream / self send_mode: WebSocketMode = self._ws_stream_options.send_mode # pyright: ignore listen_for_disconnect = self._ws_stream_options.listen_for_disconnect warn_on_data_discard = self._ws_stream_options.warn_on_data_discard async def send_handler(socket: WebSocket, data: Any) -> None: if isinstance(data, (str, bytes)): await socket.send_data(data=data, mode=send_mode) return if return_dto: encoded_data = return_dto(socket).data_to_encodable_type(data) data = json_encoder.encode(encoded_data) await socket.send_data(data=data, mode=send_mode) return data = json_encoder.encode(data) await socket.send_data(data=data, mode=send_mode) @functools.wraps(stream_fn) async def handler_fn(*args: Any, socket: WebSocket, **kw: Any) -> None: if receives_socket_parameter: kw["socket"] = socket await send_websocket_stream( socket=socket, stream=stream_fn(*args, **kw), mode=send_mode, close=True, listen_for_disconnect=listen_for_disconnect, warn_on_data_discard=warn_on_data_discard, send_handler=send_handler, ) self._fn = handler_fn super().on_registration(app) class _WebSocketStreamOptions: def __init__( self, generator_fn: Callable[..., AsyncGenerator[Any, Any]], listen_for_disconnect: bool, warn_on_data_discard: bool, send_mode: WebSocketMode, ) -> None: self.generator_fn = generator_fn self.listen_for_disconnect = listen_for_disconnect self.warn_on_data_discard = warn_on_data_discard self.send_mode = send_mode litestar-2.16.0/litestar/logging/000077500000000000000000000000001500564371300167105ustar00rootroot00000000000000litestar-2.16.0/litestar/logging/__init__.py000066400000000000000000000002231500564371300210160ustar00rootroot00000000000000from .config import BaseLoggingConfig, LoggingConfig, StructLoggingConfig __all__ = ("BaseLoggingConfig", "LoggingConfig", "StructLoggingConfig") litestar-2.16.0/litestar/logging/_utils.py000066400000000000000000000010711500564371300205600ustar00rootroot00000000000000from __future__ import annotations from typing import Any __all__ = ("resolve_handlers",) def resolve_handlers(handlers: list[Any]) -> list[Any]: """Convert list of string of handlers to the object of respective handler. Indexing the list performs the evaluation of the object. Args: handlers: An instance of 'ConvertingList' Returns: A list of resolved handlers. Notes: Due to missing typing in 'typeshed' we cannot type this as ConvertingList for now. """ return [handlers[i] for i in range(len(handlers))] litestar-2.16.0/litestar/logging/config.py000066400000000000000000000511241500564371300205320ustar00rootroot00000000000000from __future__ import annotations import sys from abc import ABC, abstractmethod from dataclasses import dataclass, field, fields from importlib.util import find_spec from logging import INFO from typing import TYPE_CHECKING, Any, Callable, Literal, Union, cast from litestar.exceptions import ImproperlyConfiguredException, MissingDependencyException from litestar.serialization.msgspec_hooks import _msgspec_json_encoder from litestar.utils.dataclass import simple_asdict from litestar.utils.deprecation import deprecated, warn_deprecation __all__ = ("BaseLoggingConfig", "LoggingConfig", "StructLoggingConfig") if TYPE_CHECKING: from collections.abc import Iterable from typing import NoReturn # these imports are duplicated on purpose so sphinx autodoc can find and link them from structlog.types import BindableLogger, Processor, WrappedLogger from structlog.typing import EventDict from litestar.types import Logger, Scope from litestar.types.callable_types import ExceptionLoggingHandler, GetLogger default_handlers: dict[str, dict[str, Any]] = { "console": { "class": "logging.StreamHandler", "level": "DEBUG", "formatter": "standard", }, "queue_listener": { "class": "litestar.logging.standard.QueueListenerHandler", "level": "DEBUG", "formatter": "standard", }, } if sys.version_info >= (3, 12, 0): default_handlers["queue_listener"].update( { "class": "logging.handlers.QueueHandler", "queue": { "()": "queue.Queue", "maxsize": -1, }, "listener": "litestar.logging.standard.LoggingQueueListener", "handlers": ["console"], } ) # do not format twice, the console handler will do the job del default_handlers["queue_listener"]["formatter"] default_picologging_handlers: dict[str, dict[str, Any]] = { "console": { "class": "picologging.StreamHandler", "level": "DEBUG", "formatter": "standard", }, "queue_listener": { "class": "litestar.logging.picologging.QueueListenerHandler", "level": "DEBUG", "formatter": "standard", }, } def _get_default_formatters() -> dict[str, dict[str, Any]]: return { "standard": {"format": "%(levelname)s - %(asctime)s - %(name)s - %(module)s - %(message)s"}, } def _get_default_loggers() -> dict[str, dict[str, Any]]: return { "litestar": {"level": "INFO", "handlers": ["queue_listener"], "propagate": False}, } def get_logger_placeholder(_: str | None = None) -> NoReturn: """Raise: An :class:`ImproperlyConfiguredException <.exceptions.ImproperlyConfiguredException>`""" raise ImproperlyConfiguredException( "cannot call '.get_logger' without passing 'logging_config' to the Litestar constructor first" ) def _get_default_logging_module() -> str: if find_spec("picologging"): return "picologging" return "logging" def _get_default_handlers(logging_module: str) -> dict[str, dict[str, Any]]: """Return the default logging handlers for the config. Returns: A dictionary of logging handlers """ if logging_module == "picologging": return default_picologging_handlers return default_handlers def _default_exception_logging_handler_factory( is_struct_logger: bool, traceback_line_limit: int, ) -> ExceptionLoggingHandler: """Create an exception logging handler function. Args: is_struct_logger: Whether the logger is a structlog instance. traceback_line_limit: Maximal number of lines to log from the traceback. This parameter is deprecated and ignored. Returns: An exception logging handler. """ if traceback_line_limit != -1: warn_deprecation( version="2.9.0", deprecated_name="traceback_line_limit", kind="parameter", info="The value is ignored. Use a custom 'exception_logging_handler' instead.", removal_in="3.0", ) if is_struct_logger: def _default_exception_logging_handler(logger: Logger, scope: Scope, tb: list[str]) -> None: logger.exception( "Uncaught exception", connection_type=scope["type"], path=scope["path"], ) else: def _default_exception_logging_handler(logger: Logger, scope: Scope, tb: list[str]) -> None: logger.exception( "Uncaught exception (connection_type=%s, path=%s):", scope["type"], scope["path"], ) return _default_exception_logging_handler class BaseLoggingConfig(ABC): """Abstract class that should be extended by logging configs.""" __slots__ = ("exception_logging_handler", "log_exceptions", "traceback_line_limit") log_exceptions: Literal["always", "debug", "never"] """Should exceptions be logged, defaults to log exceptions when ``app.debug == True``'""" traceback_line_limit: int """Max number of lines to print for exception traceback. .. deprecated:: 2.9.0 This parameter is deprecated and ignored. It will be removed in a future release. """ exception_logging_handler: ExceptionLoggingHandler | None """Handler function for logging exceptions.""" disable_stack_trace: set[Union[int, type[Exception]]] # noqa: UP007 """Set of http status codes and exceptions to disable stack trace logging for.""" @abstractmethod def configure(self) -> GetLogger: """Return logger with the given configuration. Returns: A 'logging.getLogger' like function. """ raise NotImplementedError("abstract method") @staticmethod def set_level(logger: Any, level: int) -> None: """Provides a consistent interface to call `setLevel` for all loggers.""" raise NotImplementedError("abstract method") @dataclass class LoggingConfig(BaseLoggingConfig): """Configuration class for standard logging.""" logging_module: str = field(default_factory=_get_default_logging_module) """Logging module. ``logging`` and ``picologging`` are supported. ``picologging`` will be used by default if installed.""" version: Literal[1] = field(default=1) """The only valid value at present is 1.""" incremental: bool = field(default=False) """Whether the configuration is to be interpreted as incremental to the existing configuration. Notes: - This option is ignored for 'picologging' """ disable_existing_loggers: bool = field(default=False) """Whether any existing non-root loggers are to be disabled.""" filters: dict[str, dict[str, Any]] | None = field(default=None) """A dict in which each key is a filter id and each value is a dict describing how to configure the corresponding Filter_ instance. .. _Filter: https://docs.python.org/3/library/logging.html#filter-objects """ propagate: bool = field(default=True) """If messages must propagate to handlers higher up the logger hierarchy from this logger. .. deprecated:: 2.10.0 This parameter is deprecated. It will be removed in a future release. Use ``propagate`` at the logger level. """ formatters: dict[str, dict[str, Any]] = field(default_factory=_get_default_formatters) """A dict in which each key is a formatter and each value is a dict describing how to configure the corresponding Formatter_ instance. A ``standard`` formatter is provided. .. _Formatter: https://docs.python.org/3/library/logging.html#formatter-objects """ handlers: dict[str, dict[str, Any]] = field(default_factory=dict) """A dict in which each key is a handler id and each value is a dict describing how to configure the corresponding Handler_ instance. Two handlers are provided, ``console`` and ``queue_listener``. .. _Handler: https://docs.python.org/3/library/logging.html#handler-objects """ loggers: dict[str, dict[str, Any]] = field(default_factory=_get_default_loggers) """A dict in which each key is a logger name and each value is a dict describing how to configure the corresponding Logger_ instance. A ``litestar`` logger is mandatory and will be configured as required. .. _Logger: https://docs.python.org/3/library/logging.html#logger-objects """ root: dict[str, dict[str, Any] | list[Any] | str] = field( default_factory=lambda: { "handlers": ["queue_listener"], "level": "INFO", } ) """This will be the configuration for the root logger. Processing of the configuration will be as for any logger, except that the propagate setting will not be applicable. """ configure_root_logger: bool = field(default=True) """Should the root logger be configured, defaults to True for ease of configuration.""" log_exceptions: Literal["always", "debug", "never"] = field(default="debug") """Should exceptions be logged, defaults to log exceptions when 'app.debug == True'""" disable_stack_trace: set[Union[int, type[Exception]]] = field(default_factory=set) # noqa: UP007 """Set of http status codes and exceptions to disable stack trace logging for.""" traceback_line_limit: int = field(default=-1) """Max number of lines to print for exception traceback. .. deprecated:: 2.9.0 This parameter is deprecated and ignored. It will be removed in a future release. """ exception_logging_handler: ExceptionLoggingHandler | None = field(default=None) """Handler function for logging exceptions.""" def __post_init__(self) -> None: if "standard" not in self.formatters: self.formatters["standard"] = _get_default_formatters()["standard"] if "console" not in self.handlers: self.handlers["console"] = _get_default_handlers(self.logging_module)["console"] if "queue_listener" not in self.handlers: self.handlers["queue_listener"] = _get_default_handlers(self.logging_module)["queue_listener"] if "litestar" not in self.loggers: self.loggers["litestar"] = _get_default_loggers()["litestar"] if self.log_exceptions != "never" and self.exception_logging_handler is None: self.exception_logging_handler = _default_exception_logging_handler_factory( is_struct_logger=False, traceback_line_limit=self.traceback_line_limit ) def configure(self) -> GetLogger: """Return logger with the given configuration. Returns: A 'logging.getLogger' like function. """ excluded_fields: set[str] = { "logging_module", "configure_root_logger", "exception_logging_handler", "log_exceptions", "propagate", "traceback_line_limit", "disable_stack_trace", } if not self.configure_root_logger: excluded_fields.add("root") if self.logging_module == "picologging": try: from picologging import ( # pyright: ignore[reportMissingImports,reportGeneralTypeIssues] config, # pyright: ignore[reportMissingImports,reportGeneralTypeIssues] getLogger, # pyright: ignore[reportMissingImports,reportGeneralTypeIssues] ) except ImportError as e: raise MissingDependencyException("picologging") from e excluded_fields.add("incremental") else: from logging import config, getLogger # type: ignore[no-redef,assignment,unused-ignore] values = { _field.name: getattr(self, _field.name) for _field in fields(self) if getattr(self, _field.name) is not None and _field.name not in excluded_fields } config.dictConfig(values) return cast("Callable[[str], Logger]", getLogger) @staticmethod def set_level(logger: Logger, level: int) -> None: """Provides a consistent interface to call `setLevel` for all loggers.""" logger.setLevel(level) class StructlogEventFilter: """Remove keys from the log event. Add an instance to the processor chain. .. code-block:: python :caption: Examples structlog.configure( ..., processors=[ ..., EventFilter(["color_message"]), ..., ], ) """ def __init__(self, filter_keys: Iterable[str]) -> None: """Initialize the EventFilter. Args: filter_keys: Iterable of string keys to be excluded from the log event. """ self.filter_keys = filter_keys def __call__(self, _: WrappedLogger, __: str, event_dict: EventDict) -> EventDict: """Receive the log event, and filter keys. Args: _ (): __ (): event_dict (): The data to be logged. Returns: The log event with any key in `self.filter_keys` removed. """ for key in self.filter_keys: event_dict.pop(key, None) return event_dict def default_json_serializer(value: EventDict, **_: Any) -> bytes: return _msgspec_json_encoder.encode(value) def stdlib_json_serializer(value: EventDict, **_: Any) -> str: # pragma: no cover return _msgspec_json_encoder.encode(value).decode("utf-8") def default_structlog_processors( as_json: bool = True, json_serializer: Callable[[Any], Any] = default_json_serializer ) -> list[Processor]: # pyright: ignore """Set the default processors for structlog. Returns: An optional list of processors. """ try: import structlog from structlog.dev import RichTracebackFormatter if as_json: return [ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, structlog.processors.format_exc_info, structlog.processors.TimeStamper(fmt="iso"), structlog.processors.JSONRenderer(serializer=json_serializer), ] return [ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, structlog.processors.TimeStamper(fmt="iso"), structlog.dev.ConsoleRenderer( colors=True, exception_formatter=RichTracebackFormatter(max_frames=1, show_locals=False, width=80) ), ] except ImportError: return [] def default_structlog_standard_lib_processors(as_json: bool = True) -> list[Processor]: # pyright: ignore """Set the default processors for structlog stdlib. Returns: An optional list of processors. """ try: import structlog from structlog.dev import RichTracebackFormatter if as_json: return [ structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_log_level, structlog.stdlib.ExtraAdder(), StructlogEventFilter(["color_message"]), structlog.stdlib.ProcessorFormatter.remove_processors_meta, structlog.processors.JSONRenderer(serializer=stdlib_json_serializer), ] return [ structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_log_level, structlog.stdlib.ExtraAdder(), StructlogEventFilter(["color_message"]), structlog.stdlib.ProcessorFormatter.remove_processors_meta, structlog.dev.ConsoleRenderer( colors=True, exception_formatter=RichTracebackFormatter(max_frames=1, show_locals=False, width=80) ), ] except ImportError: return [] def default_logger_factory(as_json: bool = True) -> Callable[..., WrappedLogger] | None: """Set the default logger factory for structlog. Returns: An optional logger factory. """ try: import structlog if as_json: return structlog.BytesLoggerFactory() return structlog.WriteLoggerFactory() except ImportError: return None @dataclass class StructLoggingConfig(BaseLoggingConfig): """Configuration class for structlog. Notes: - requires ``structlog`` to be installed. """ processors: list[Processor] | None = field(default=None) # pyright: ignore """Iterable of structlog logging processors.""" standard_lib_logging_config: LoggingConfig | None = field(default=None) # pyright: ignore """Optional customized standard logging configuration. Use this when you need to modify the standard library outside of the Structlog pre-configured implementation. """ wrapper_class: type[BindableLogger] | None = field(default=None) # pyright: ignore """Structlog bindable logger.""" context_class: dict[str, Any] | None = None """Context class (a 'contextvar' context) for the logger.""" logger_factory: Callable[..., WrappedLogger] | None = field(default=None) # pyright: ignore """Logger factory to use.""" cache_logger_on_first_use: bool = field(default=True) """Whether to cache the logger configuration and reuse.""" log_exceptions: Literal["always", "debug", "never"] = field(default="debug") """Should exceptions be logged, defaults to log exceptions when 'app.debug == True'""" disable_stack_trace: set[Union[int, type[Exception]]] = field(default_factory=set) # noqa: UP007 """Set of http status codes and exceptions to disable stack trace logging for.""" traceback_line_limit: int = field(default=-1) """Max number of lines to print for exception traceback. .. deprecated:: 2.9.0 This parameter is deprecated and ignored. It will be removed in a future release. """ exception_logging_handler: ExceptionLoggingHandler | None = field(default=None) """Handler function for logging exceptions.""" pretty_print_tty: bool = field(default=True) """Pretty print log output when run from an interactive terminal.""" def __post_init__(self) -> None: if self.processors is None: self.processors = default_structlog_processors(as_json=self.as_json()) if self.logger_factory is None: self.logger_factory = default_logger_factory(as_json=self.as_json()) if self.log_exceptions != "never" and self.exception_logging_handler is None: self.exception_logging_handler = _default_exception_logging_handler_factory( is_struct_logger=True, traceback_line_limit=self.traceback_line_limit ) try: import structlog if self.standard_lib_logging_config is None: self.standard_lib_logging_config = LoggingConfig( formatters={ "standard": { "()": structlog.stdlib.ProcessorFormatter, "processors": default_structlog_standard_lib_processors(as_json=self.as_json()), } } ) except ImportError: self.standard_lib_logging_config = LoggingConfig() def as_json(self) -> bool: return not (sys.stderr.isatty() and self.pretty_print_tty) def configure(self) -> GetLogger: """Return logger with the given configuration. Returns: A 'logging.getLogger' like function. """ try: import structlog except ImportError as e: raise MissingDependencyException("structlog") from e structlog.configure( **{ k: v for k, v in simple_asdict(self).items() if k not in ( "standard_lib_logging_config", "log_exceptions", "traceback_line_limit", "exception_logging_handler", "pretty_print_tty", "disable_stack_trace", ) } ) return structlog.get_logger @staticmethod def set_level(logger: Logger, level: int) -> None: """Provides a consistent interface to call `setLevel` for all loggers.""" try: import structlog structlog.configure(wrapper_class=structlog.make_filtering_bound_logger(level)) except ImportError: """""" return @deprecated(version="2.6.0", removal_in="3.0.0", alternative="`StructLoggingConfig.set_level`") def default_wrapper_class(log_level: int = INFO) -> type[BindableLogger] | None: # pragma: no cover # pyright: ignore try: # pragma: no cover import structlog return structlog.make_filtering_bound_logger(log_level) except ImportError: return None litestar-2.16.0/litestar/logging/picologging.py000066400000000000000000000025371500564371300215720ustar00rootroot00000000000000from __future__ import annotations import atexit from queue import Queue from typing import Any from litestar.exceptions import MissingDependencyException from litestar.logging._utils import resolve_handlers __all__ = ("QueueListenerHandler",) try: import picologging # noqa: F401 # pyright: ignore[reportMissingImports] except ImportError as e: raise MissingDependencyException("picologging") from e from picologging import StreamHandler # pyright: ignore[reportMissingImports] from picologging.handlers import QueueHandler, QueueListener # pyright: ignore[reportMissingImports] class QueueListenerHandler(QueueHandler): # type: ignore[misc,unused-ignore] """Configure queue listener and handler to support non-blocking logging configuration.""" def __init__(self, handlers: list[Any] | None = None) -> None: """Initialize ``QueueListenerHandler``. Args: handlers: Optional 'ConvertingList' Notes: - Requires ``picologging`` to be installed. """ super().__init__(Queue(-1)) handlers = resolve_handlers(handlers) if handlers else [StreamHandler()] # pyright: ignore[reportGeneralTypeIssues] self.listener = QueueListener(self.queue, *handlers) # pyright: ignore[reportGeneralTypeIssues] self.listener.start() atexit.register(self.listener.stop) litestar-2.16.0/litestar/logging/standard.py000066400000000000000000000035141500564371300210650ustar00rootroot00000000000000from __future__ import annotations import atexit from logging import Handler, LogRecord, StreamHandler from logging.handlers import QueueHandler, QueueListener from queue import Queue from typing import Any from litestar.logging._utils import resolve_handlers __all__ = ("LoggingQueueListener", "QueueListenerHandler") class LoggingQueueListener(QueueListener): """Custom ``QueueListener`` which starts and stops the listening process.""" def __init__(self, queue: Queue[LogRecord], *handlers: Handler, respect_handler_level: bool = False) -> None: """Initialize ``LoggingQueueListener``. Args: queue: The queue to send messages to *handlers: A list of handlers which will handle entries placed on the queue respect_handler_level: If ``respect_handler_level`` is ``True``, a handler's level is respected (compared with the level for the message) when deciding whether to pass messages to that handler """ super().__init__(queue, *handlers, respect_handler_level=respect_handler_level) self.start() atexit.register(self.stop) class QueueListenerHandler(QueueHandler): """Configure queue listener and handler to support non-blocking logging configuration. .. caution:: This handler doesn't work with Python >= 3.12 and ``logging.config.dictConfig``. It might be deprecated in the future. Please use ``logging.QueueHandler`` instead. """ def __init__(self, handlers: list[Any] | None = None) -> None: """Initialize ``QueueListenerHandler``. Args: handlers: Optional 'ConvertingList' """ super().__init__(Queue(-1)) handlers = resolve_handlers(handlers) if handlers else [StreamHandler()] self.listener = LoggingQueueListener(self.queue, *handlers) # type: ignore[arg-type] litestar-2.16.0/litestar/middleware/000077500000000000000000000000001500564371300173775ustar00rootroot00000000000000litestar-2.16.0/litestar/middleware/__init__.py000066400000000000000000000006531500564371300215140ustar00rootroot00000000000000from litestar.middleware.authentication import ( AbstractAuthenticationMiddleware, AuthenticationResult, ) from litestar.middleware.base import ( AbstractMiddleware, ASGIMiddleware, DefineMiddleware, MiddlewareProtocol, ) __all__ = ( "ASGIMiddleware", "AbstractAuthenticationMiddleware", "AbstractMiddleware", "AuthenticationResult", "DefineMiddleware", "MiddlewareProtocol", ) litestar-2.16.0/litestar/middleware/_internal/000077500000000000000000000000001500564371300213525ustar00rootroot00000000000000litestar-2.16.0/litestar/middleware/_internal/__init__.py000066400000000000000000000000001500564371300234510ustar00rootroot00000000000000litestar-2.16.0/litestar/middleware/_internal/cors.py000066400000000000000000000124251500564371300226760ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar.constants import DEFAULT_ALLOWED_CORS_HEADERS from litestar.datastructures import Headers, MutableScopeHeaders from litestar.enums import HttpMethod, MediaType, ScopeType from litestar.middleware.base import AbstractMiddleware from litestar.response import Response from litestar.status_codes import HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST if TYPE_CHECKING: from litestar.config.cors import CORSConfig from litestar.types import ASGIApp, Message, Receive, Scope, Send __all__ = ("CORSMiddleware",) class CORSMiddleware(AbstractMiddleware): """CORS Middleware.""" def __init__(self, app: ASGIApp, config: CORSConfig) -> None: """Middleware that adds CORS validation to the application. Args: app: The ``next`` ASGI app to call. config: An instance of :class:`CORSConfig ` """ super().__init__(app=app, scopes={ScopeType.HTTP}) self.config = config async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI callable. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ headers = Headers.from_scope(scope=scope) origin = headers.get("origin") if scope["type"] == ScopeType.HTTP and scope["method"] == HttpMethod.OPTIONS and origin: request = scope["litestar_app"].request_class(scope=scope, receive=receive, send=send) asgi_response = self._create_preflight_response(origin=origin, request_headers=headers).to_asgi_response( app=None, request=request ) await asgi_response(scope, receive, send) elif origin: await self.app(scope, receive, self.send_wrapper(send=send, origin=origin, has_cookie="cookie" in headers)) else: await self.app(scope, receive, send) def send_wrapper(self, send: Send, origin: str, has_cookie: bool) -> Send: """Wrap ``send`` to ensure that state is not disconnected. Args: has_cookie: Boolean flag dictating if the connection has a cookie set. origin: The value of the ``Origin`` header. send: The ASGI send function. Returns: An ASGI send function. """ async def wrapped_send(message: Message) -> None: if message["type"] == "http.response.start": message.setdefault("headers", []) headers = MutableScopeHeaders.from_message(message=message) headers.update(self.config.simple_headers) if (self.config.is_allow_all_origins and has_cookie) or ( not self.config.is_allow_all_origins and self.config.is_origin_allowed(origin=origin) ): headers["Access-Control-Allow-Origin"] = origin headers["Vary"] = "Origin" headers["Access-Control-Allow-Headers"] = ", ".join(sorted(set(self.config.allow_headers))) headers["Access-Control-Allow-Methods"] = ", ".join(sorted(set(self.config.allow_methods))) await send(message) return wrapped_send def _create_preflight_response(self, origin: str, request_headers: Headers) -> Response[str | None]: pre_flight_method = request_headers.get("Access-Control-Request-Method") failures = [] if not self.config.is_allow_all_methods and ( pre_flight_method and pre_flight_method not in self.config.allow_methods ): failures.append("method") response_headers = self.config.preflight_headers.copy() if not self.config.is_origin_allowed(origin): failures.append("Origin") elif response_headers.get("Access-Control-Allow-Origin") != "*": response_headers["Access-Control-Allow-Origin"] = origin pre_flight_requested_headers = [ header.strip() for header in request_headers.get("Access-Control-Request-Headers", "").split(",") if header.strip() ] if pre_flight_requested_headers: if self.config.is_allow_all_headers: response_headers["Access-Control-Allow-Headers"] = ", ".join( sorted(set(pre_flight_requested_headers) | DEFAULT_ALLOWED_CORS_HEADERS) # pyright: ignore ) else: all_allowed_headers = set(self.config.allow_headers).union( default_header.lower() for default_header in DEFAULT_ALLOWED_CORS_HEADERS ) if any(header.lower() not in all_allowed_headers for header in pre_flight_requested_headers): failures.append("headers") return ( Response( content=f"Disallowed CORS {', '.join(failures)}", status_code=HTTP_400_BAD_REQUEST, media_type=MediaType.TEXT, ) if failures else Response( content=None, status_code=HTTP_204_NO_CONTENT, media_type=MediaType.TEXT, headers=response_headers, ) ) litestar-2.16.0/litestar/middleware/_internal/exceptions/000077500000000000000000000000001500564371300235335ustar00rootroot00000000000000litestar-2.16.0/litestar/middleware/_internal/exceptions/__init__.py000066400000000000000000000002061500564371300256420ustar00rootroot00000000000000from litestar.middleware._internal.exceptions.middleware import ExceptionHandlerMiddleware __all__ = ("ExceptionHandlerMiddleware",) litestar-2.16.0/litestar/middleware/_internal/exceptions/middleware.py000066400000000000000000000237111500564371300262260ustar00rootroot00000000000000from __future__ import annotations from inspect import getmro from sys import exc_info from traceback import format_exception from typing import TYPE_CHECKING, Any, Type, Union, cast from litestar.enums import ScopeType from litestar.exceptions import HTTPException, LitestarException, WebSocketException from litestar.exceptions.responses import create_exception_response from litestar.exceptions.responses._debug_response import ( create_debug_response, ) from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR from litestar.utils.deprecation import warn_deprecation from litestar.utils.empty import value_or_raise from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from starlette.exceptions import HTTPException as StarletteHTTPException from litestar import Response from litestar.app import Litestar from litestar.connection import Request from litestar.handlers import BaseRouteHandler from litestar.logging import BaseLoggingConfig from litestar.types import ( ASGIApp, ExceptionHandler, ExceptionHandlersMap, Logger, Message, Receive, Scope, Send, ) from litestar.types.asgi_types import WebSocketCloseEvent __all__ = ("ExceptionHandlerMiddleware",) def get_exception_handler(exception_handlers: ExceptionHandlersMap, exc: Exception) -> ExceptionHandler | None: """Given a dictionary that maps exceptions and status codes to handler functions, and an exception, returns the appropriate handler if existing. Status codes are given preference over exception type. If no status code match exists, each class in the MRO of the exception type is checked and the first matching handler is returned. Finally, if a ``500`` handler is registered, it will be returned for any exception that isn't a subclass of :class:`HTTPException `. Args: exception_handlers: Mapping of status codes and exception types to handlers. exc: Exception Instance to be resolved to a handler. Returns: Optional exception handler callable. """ if not exception_handlers: return None default_handler: ExceptionHandler | None = None if isinstance(exc, HTTPException): if exception_handler := exception_handlers.get(exc.status_code): return exception_handler else: default_handler = exception_handlers.get(HTTP_500_INTERNAL_SERVER_ERROR) return next( (exception_handlers[cast("Type[Exception]", cls)] for cls in getmro(type(exc)) if cls in exception_handlers), default_handler, ) def _starlette_exception_handler(request: Request[Any, Any, Any], exc: StarletteHTTPException) -> Response: return create_exception_response( request=request, exc=HTTPException( detail=exc.detail, status_code=exc.status_code, headers=exc.headers, # type: ignore[arg-type] ), ) class ExceptionHandlerMiddleware: """Middleware used to wrap an ASGIApp inside a try catch block and handle any exceptions raised. This used in multiple layers of Litestar. """ def __init__( self, app: ASGIApp, debug: bool | None, exception_handlers: ExceptionHandlersMap | None = None ) -> None: """Initialize ``ExceptionHandlerMiddleware``. Args: app: The ``next`` ASGI app to call. debug: Whether ``debug`` mode is enabled. Deprecated. Debug mode will be inferred from the request scope exception_handlers: A dictionary mapping status codes and/or exception types to handler functions. .. deprecated:: 2.0.0 The ``debug`` parameter is deprecated. It will be inferred from the request scope .. deprecated:: 2.9.0 The ``exception_handlers`` parameter is deprecated. It will be inferred from the application or the route handler. """ self.app = app self.exception_handlers = exception_handlers self.debug = debug if debug is not None: warn_deprecation( "2.0.0", deprecated_name="debug", kind="parameter", info="Debug mode will be inferred from the request scope", removal_in="3.0.0", ) if exception_handlers is not None: warn_deprecation( "2.9.0", deprecated_name="exception_handlers", kind="parameter", info="It will be inferred from the application or the route handler", removal_in="3.0.0", ) self._get_debug = self._get_debug_scope if debug is None else lambda *a: debug @staticmethod def _get_debug_scope(scope: Scope) -> bool: return scope["litestar_app"].debug async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI-callable. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ scope_state = ScopeState.from_scope(scope) async def capture_response_started(event: Message) -> None: if event["type"] == "http.response.start": scope_state.response_started = True await send(event) try: await self.app(scope, receive, capture_response_started) except Exception as e: if scope_state.response_started: raise LitestarException("Exception caught after response started") from e litestar_app = scope["litestar_app"] if litestar_app.logging_config and (logger := litestar_app.logger): self.handle_exception_logging(logger=logger, logging_config=litestar_app.logging_config, scope=scope) for hook in litestar_app.after_exception: await hook(e, scope) if litestar_app.pdb_on_exception: litestar_app.debugger_module.post_mortem() if scope["type"] == ScopeType.HTTP: await self.handle_request_exception( litestar_app=litestar_app, scope=scope, receive=receive, send=send, exc=e ) else: await self.handle_websocket_exception(send=send, exc=e) async def handle_request_exception( self, litestar_app: Litestar, scope: Scope, receive: Receive, send: Send, exc: Exception ) -> None: """Handle exception raised inside 'http' scope routes. Args: litestar_app: The litestar app instance. scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. exc: The caught exception. Returns: None. """ exception_handlers = ( value_or_raise(ScopeState.from_scope(scope).exception_handlers) if self.exception_handlers is None else self.exception_handlers ) exception_handler = get_exception_handler(exception_handlers, exc) or self.default_http_exception_handler request: Request[Any, Any, Any] = litestar_app.request_class(scope=scope, receive=receive, send=send) response = exception_handler(request, exc) route_handler: BaseRouteHandler | None = scope.get("route_handler") type_encoders = route_handler.resolve_type_encoders() if route_handler else litestar_app.type_encoders await response.to_asgi_response(app=None, request=request, type_encoders=type_encoders)( scope=scope, receive=receive, send=send ) @staticmethod async def handle_websocket_exception(send: Send, exc: Exception) -> None: """Handle exception raised inside 'websocket' scope routes. Args: send: The ASGI send function. exc: The caught exception. Returns: None. """ code = 4000 + HTTP_500_INTERNAL_SERVER_ERROR reason = "Internal Server Error" if isinstance(exc, WebSocketException): code = exc.code reason = exc.detail elif isinstance(exc, LitestarException): reason = exc.detail event: WebSocketCloseEvent = {"type": "websocket.close", "code": code, "reason": reason} await send(event) def default_http_exception_handler(self, request: Request, exc: Exception) -> Response[Any]: """Handle an HTTP exception by returning the appropriate response. Args: request: An HTTP Request instance. exc: The caught exception. Returns: An HTTP response. """ status_code = exc.status_code if isinstance(exc, HTTPException) else HTTP_500_INTERNAL_SERVER_ERROR if status_code == HTTP_500_INTERNAL_SERVER_ERROR and self._get_debug_scope(request.scope): return create_debug_response(request=request, exc=exc) return create_exception_response(request=request, exc=exc) def handle_exception_logging(self, logger: Logger, logging_config: BaseLoggingConfig, scope: Scope) -> None: """Handle logging - if the litestar app has a logging config in place. Args: logger: A logger instance. logging_config: Logging Config instance. scope: The ASGI connection scope. Returns: None """ exc = exc_info() exc_detail: set[Union[Exception, int]] = {exc[0], getattr(exc[0], "status_code", None)} # type: ignore[arg-type] # noqa: UP007 if ( ( logging_config.log_exceptions == "always" or (logging_config.log_exceptions == "debug" and self._get_debug_scope(scope)) ) and logging_config.exception_logging_handler and exc_detail.isdisjoint(logging_config.disable_stack_trace) ): logging_config.exception_logging_handler(logger, scope, format_exception(*exc)) litestar-2.16.0/litestar/middleware/_utils.py000066400000000000000000000055271500564371300212610ustar00rootroot00000000000000from __future__ import annotations import re from typing import TYPE_CHECKING, Iterable, Pattern, Sequence from litestar.exceptions import ImproperlyConfiguredException __all__ = ("build_exclude_path_pattern", "should_bypass_middleware") from litestar.utils.warnings import warn_middleware_excluded_on_all_routes if TYPE_CHECKING: from litestar.types import Method, Scope, Scopes def build_exclude_path_pattern( *, exclude: str | Iterable[str] | None = None, middleware_cls: type | None = None, ) -> Pattern | None: """Build single path pattern from list of patterns to opt-out from middleware processing. Args: exclude: A pattern or a list of patterns. middleware_cls: Middleware class this is being called from - used for creating more informative warnings Returns: An optional pattern to match against scope["path"] to opt-out from middleware processing. """ if exclude is None: return None try: pattern = re.compile("|".join(exclude)) if not isinstance(exclude, str) else re.compile(exclude) if pattern.match("/") and pattern.match("/982c7064-6ac7-44b7-9be5-07a2ff6d8a92"): # match a UUID to ensure that it matches paths greedily and not just a literal / warn_middleware_excluded_on_all_routes(pattern, middleware_cls=middleware_cls) return pattern except re.error as e: # pragma: no cover raise ImproperlyConfiguredException( "Unable to compile exclude patterns for middleware. Please make sure you passed a valid regular expression." ) from e def should_bypass_middleware( *, exclude_http_methods: Sequence[Method] | None = None, exclude_opt_key: str | None = None, exclude_path_pattern: Pattern | None = None, scope: Scope, scopes: Scopes, ) -> bool: """Determine weather a middleware should be bypassed. Args: exclude_http_methods: A sequence of http methods that do not require authentication. exclude_opt_key: Key in ``opt`` with which a route handler can "opt-out" of a middleware. exclude_path_pattern: If this pattern matches scope["path"], the middleware should be bypassed. scope: The ASGI scope. scopes: A set with the ASGI scope types that are supported by the middleware. Returns: A boolean indicating if a middleware should be bypassed """ if scope["type"] not in scopes: return True if exclude_opt_key and scope["route_handler"].opt.get(exclude_opt_key): return True if exclude_http_methods and scope.get("method") in exclude_http_methods: return True return bool( exclude_path_pattern and exclude_path_pattern.findall( scope["raw_path"].decode() if getattr(scope.get("route_handler", {}), "is_mount", False) else scope["path"] ) ) litestar-2.16.0/litestar/middleware/allowed_hosts.py000066400000000000000000000057151500564371300226300ustar00rootroot00000000000000from __future__ import annotations import re from typing import TYPE_CHECKING, Pattern from litestar.datastructures import URL, MutableScopeHeaders from litestar.middleware.base import AbstractMiddleware from litestar.response.base import ASGIResponse from litestar.response.redirect import ASGIRedirectResponse from litestar.status_codes import HTTP_400_BAD_REQUEST __all__ = ("AllowedHostsMiddleware",) if TYPE_CHECKING: from litestar.config.allowed_hosts import AllowedHostsConfig from litestar.types import ASGIApp, Receive, Scope, Send class AllowedHostsMiddleware(AbstractMiddleware): """Middleware ensuring the host of a request originated in a trusted host.""" def __init__(self, app: ASGIApp, config: AllowedHostsConfig) -> None: """Initialize ``AllowedHostsMiddleware``. Args: app: The ``next`` ASGI app to call. config: An instance of AllowedHostsConfig. """ super().__init__(app=app, exclude=config.exclude, exclude_opt_key=config.exclude_opt_key, scopes=config.scopes) self.allowed_hosts_regex: Pattern | None = None self.redirect_domains: Pattern | None = None if any(host == "*" for host in config.allowed_hosts): return allowed_hosts: set[str] = { rf".*\.{host.replace('*.', '')}$" if host.startswith("*.") else host for host in config.allowed_hosts } self.allowed_hosts_regex = re.compile("|".join(sorted(allowed_hosts))) # pyright: ignore if config.www_redirect and ( redirect_domains := {host.replace("www.", "") for host in config.allowed_hosts if host.startswith("www.")} ): self.redirect_domains = re.compile("|".join(sorted(redirect_domains))) # pyright: ignore async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI callable. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ if self.allowed_hosts_regex is None: await self.app(scope, receive, send) return headers = MutableScopeHeaders(scope=scope) if host := headers.get("host", headers.get("x-forwarded-host", "")).split(":")[0]: if self.allowed_hosts_regex.fullmatch(host): await self.app(scope, receive, send) return if self.redirect_domains is not None and self.redirect_domains.fullmatch(host): url = URL.from_scope(scope) redirect_url = url.with_replacements(netloc=f"www.{url.netloc}") redirect_response = ASGIRedirectResponse(path=str(redirect_url)) await redirect_response(scope, receive, send) return response = ASGIResponse(body=b'{"message":"invalid host header"}', status_code=HTTP_400_BAD_REQUEST) await response(scope, receive, send) litestar-2.16.0/litestar/middleware/authentication.py000066400000000000000000000075371500564371300230040ustar00rootroot00000000000000from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Sequence from litestar.connection import ASGIConnection from litestar.enums import HttpMethod, ScopeType from litestar.middleware._utils import ( build_exclude_path_pattern, should_bypass_middleware, ) __all__ = ("AbstractAuthenticationMiddleware", "AuthenticationResult") if TYPE_CHECKING: from litestar.types import ASGIApp, Method, Receive, Scope, Scopes, Send @dataclass class AuthenticationResult: """Dataclass for authentication result.""" __slots__ = ("auth", "user") user: Any """The user model, this can be any value corresponding to a user of the API.""" auth: Any """The auth value, this can for example be a JWT token.""" class AbstractAuthenticationMiddleware(ABC): """Abstract AuthenticationMiddleware that allows users to create their own AuthenticationMiddleware by extending it and overriding :meth:`AbstractAuthenticationMiddleware.authenticate_request`. """ __slots__ = ( "app", "exclude", "exclude_http_methods", "exclude_opt_key", "scopes", ) def __init__( self, app: ASGIApp, exclude: str | list[str] | None = None, exclude_from_auth_key: str = "exclude_from_auth", exclude_http_methods: Sequence[Method] | None = None, scopes: Scopes | None = None, ) -> None: """Initialize ``AbstractAuthenticationMiddleware``. Args: app: An ASGIApp, this value is the next ASGI handler to call in the middleware stack. exclude: A pattern or list of patterns to skip in the authentication middleware. exclude_from_auth_key: An identifier to use on routes to disable authentication for a particular route. exclude_http_methods: A sequence of http methods that do not require authentication. scopes: ASGI scopes processed by the authentication middleware. """ self.app = app self.exclude = build_exclude_path_pattern(exclude=exclude, middleware_cls=type(self)) self.exclude_http_methods = (HttpMethod.OPTIONS,) if exclude_http_methods is None else exclude_http_methods self.exclude_opt_key = exclude_from_auth_key self.scopes = scopes or {ScopeType.HTTP, ScopeType.WEBSOCKET} async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI callable. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ if not should_bypass_middleware( exclude_http_methods=self.exclude_http_methods, exclude_opt_key=self.exclude_opt_key, exclude_path_pattern=self.exclude, scope=scope, scopes=self.scopes, ): auth_result = await self.authenticate_request(ASGIConnection(scope)) scope["user"] = auth_result.user scope["auth"] = auth_result.auth await self.app(scope, receive, send) @abstractmethod async def authenticate_request(self, connection: ASGIConnection) -> AuthenticationResult: """Receive the http connection and return an :class:`AuthenticationResult`. Notes: - This method must be overridden by subclasses. Args: connection: An :class:`ASGIConnection ` instance. Raises: NotAuthorizedException | PermissionDeniedException: if authentication fails. Returns: An instance of :class:`AuthenticationResult `. """ raise NotImplementedError("authenticate_request must be overridden by subclasses") litestar-2.16.0/litestar/middleware/base.py000066400000000000000000000217141500564371300206700ustar00rootroot00000000000000from __future__ import annotations import abc from abc import abstractmethod from typing import TYPE_CHECKING, Any, Callable, Protocol, runtime_checkable from litestar.enums import ScopeType from litestar.middleware._utils import ( build_exclude_path_pattern, should_bypass_middleware, ) from litestar.utils.deprecation import warn_deprecation __all__ = ( "ASGIMiddleware", "AbstractMiddleware", "DefineMiddleware", "MiddlewareProtocol", ) if TYPE_CHECKING: from litestar.types import Scopes from litestar.types.asgi_types import ASGIApp, Receive, Scope, Send @runtime_checkable class MiddlewareProtocol(Protocol): """Abstract middleware protocol.""" __slots__ = ("app",) app: ASGIApp async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """Execute the ASGI middleware. Called by the previous middleware in the stack if a response is not awaited prior. Upon completion, middleware should call the next ASGI handler and await it - or await a response created in its closure. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ class DefineMiddleware: """Container enabling passing ``*args`` and ``**kwargs`` to Middleware class constructors and factory functions.""" __slots__ = ("args", "kwargs", "middleware") def __init__(self, middleware: Callable[..., ASGIApp], *args: Any, **kwargs: Any) -> None: """Initialize ``DefineMiddleware``. Args: middleware: A callable that returns an ASGIApp. *args: Positional arguments to pass to the callable. **kwargs: Key word arguments to pass to the callable. Notes: The callable will be passed a kwarg ``app``, which is the next ASGI app to call in the middleware stack. It therefore must define such a kwarg. """ self.middleware = middleware self.args = args self.kwargs = kwargs def __call__(self, app: ASGIApp) -> ASGIApp: """Call the middleware constructor or factory. Args: app: An ASGIApp, this value is the next ASGI handler to call in the middleware stack. Returns: Calls :class:`DefineMiddleware.middleware <.DefineMiddleware>` and returns the ASGIApp created. """ return self.middleware(*self.args, app=app, **self.kwargs) class AbstractMiddleware: """Abstract middleware providing base functionality common to all middlewares, for dynamically engaging/bypassing the middleware based on paths, ``opt``-keys and scope types. When implementing new middleware, this class should be used as a base. """ scopes: Scopes = {ScopeType.HTTP, ScopeType.WEBSOCKET} exclude: str | list[str] | None = None exclude_opt_key: str | None = None def __init__( self, app: ASGIApp, exclude: str | list[str] | None = None, exclude_opt_key: str | None = None, scopes: Scopes | None = None, ) -> None: """Initialize the middleware. Args: app: The ``next`` ASGI app to call. exclude: A pattern or list of patterns to match against a request's path. If a match is found, the middleware will be skipped. exclude_opt_key: An identifier that is set in the route handler ``opt`` key which allows skipping the middleware. scopes: ASGI scope types, should be a set including either or both 'ScopeType.HTTP' and 'ScopeType.WEBSOCKET'. """ self.app = app self.scopes = scopes or self.scopes self.exclude_opt_key = exclude_opt_key or self.exclude_opt_key self.exclude_pattern = build_exclude_path_pattern(exclude=(exclude or self.exclude), middleware_cls=type(self)) @classmethod def __init_subclass__(cls, **kwargs: Any) -> None: if not any(c.__module__.startswith("litestar") and c is not AbstractMiddleware for c in cls.mro()): # we don't want to warn about usage of 'AbstractMiddleware' if users aren't # directly subclassing it, i.e. they're subclassing another Litestar # middleware which itself subclasses 'AbstractMiddleware' warn_deprecation( version="2.15", deprecated_name="AbstractMiddleware", kind="class", alternative="litestar.middleware.ASGIMiddleware", ) super().__init_subclass__(**kwargs) original__call__ = cls.__call__ async def wrapped_call(self: AbstractMiddleware, scope: Scope, receive: Receive, send: Send) -> None: if should_bypass_middleware( scope=scope, scopes=self.scopes, exclude_path_pattern=self.exclude_pattern, exclude_opt_key=self.exclude_opt_key, ): await self.app(scope, receive, send) else: await original__call__(self, scope, receive, send) # pyright: ignore # https://github.com/python/mypy/issues/2427#issuecomment-384229898 setattr(cls, "__call__", wrapped_call) @abstractmethod async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """Execute the ASGI middleware. Called by the previous middleware in the stack if a response is not awaited prior. Upon completion, middleware should call the next ASGI handler and await it - or await a response created in its closure. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ raise NotImplementedError("abstract method must be implemented") class ASGIMiddleware(abc.ABC): """An abstract base class to easily construct ASGI middlewares, providing functionality to dynamically skip the middleware based on ASGI ``scope["type"]``, handler ``opt`` keys or path patterns and a simple way to pass configuration to middlewares. This base class does not implement an ``__init__`` method, so subclasses are free to use it to customize the middleware's configuration. .. important:: An instance of the individual middlewares will be created *once* and used to build up the internal middleware stack. As such, middlewares should *not* be stateful, as this state will be shared across all requests. Any connection-specific state should be scoped to the `handle` implementation. Not doing so would typically lead to conflicting variable reads / writes across requests, and - most likely - bugs. .. code-block:: python class MyMiddleware(ASGIMiddleware): scopes = (ScopeType.HTTP,) exclude = ("/not/this/path",) exclude_opt_key = "exclude_my_middleware" def __init__(self, my_logger: Logger) -> None: self.logger = my_logger async def handle( self, scope: Scope, receive: Receive, send: Send, next_app: ASGIApp ) -> None: self.logger.debug("Received request for path %s", scope["path"]) await next_app(scope, receive, send) self.logger.debug("Processed request for path %s", scope["path"]) app = Litestar(..., middleware=[MyMiddleware(logger=my_logger)]) .. versionadded:: 2.15 """ scopes: tuple[ScopeType, ...] = ( ScopeType.HTTP, ScopeType.WEBSOCKET, ScopeType.ASGI, ) exclude_path_pattern: str | tuple[str, ...] | None = None exclude_opt_key: str | None = None def __call__(self, app: ASGIApp) -> ASGIApp: """Create the actual middleware callable""" handle = self.handle exclude_pattern = build_exclude_path_pattern(exclude=self.exclude_path_pattern, middleware_cls=type(self)) scopes = set(self.scopes) exclude_opt_key = self.exclude_opt_key async def middleware(scope: Scope, receive: Receive, send: Send) -> None: if should_bypass_middleware( scope=scope, scopes=scopes, # type: ignore[arg-type] exclude_opt_key=exclude_opt_key, exclude_path_pattern=exclude_pattern, ): await app(scope, receive, send) else: await handle(scope=scope, receive=receive, send=send, next_app=app) return middleware @abc.abstractmethod async def handle(self, scope: Scope, receive: Receive, send: Send, next_app: ASGIApp) -> None: """Handle ASGI call. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function next_app: The next ASGI application in the middleware stack to call """ raise NotImplementedError litestar-2.16.0/litestar/middleware/compression/000077500000000000000000000000001500564371300217405ustar00rootroot00000000000000litestar-2.16.0/litestar/middleware/compression/__init__.py000066400000000000000000000003141500564371300240470ustar00rootroot00000000000000from litestar.middleware.compression.facade import CompressionFacade from litestar.middleware.compression.middleware import CompressionMiddleware __all__ = ("CompressionFacade", "CompressionMiddleware") litestar-2.16.0/litestar/middleware/compression/brotli_facade.py000066400000000000000000000030531500564371300250710ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Literal from litestar.enums import CompressionEncoding from litestar.exceptions import MissingDependencyException from litestar.middleware.compression.facade import CompressionFacade try: from brotli import MODE_FONT, MODE_GENERIC, MODE_TEXT, Compressor except ImportError as e: raise MissingDependencyException("brotli") from e if TYPE_CHECKING: from io import BytesIO from litestar.config.compression import CompressionConfig class BrotliCompression(CompressionFacade): __slots__ = ("buffer", "compression_encoding", "compressor") encoding = CompressionEncoding.BROTLI def __init__( self, buffer: BytesIO, compression_encoding: Literal[CompressionEncoding.BROTLI] | str, config: CompressionConfig, ) -> None: self.buffer = buffer self.compression_encoding = compression_encoding modes: dict[Literal["generic", "text", "font"], int] = { "text": int(MODE_TEXT), "font": int(MODE_FONT), "generic": int(MODE_GENERIC), } self.compressor = Compressor( quality=config.brotli_quality, mode=modes[config.brotli_mode], lgwin=config.brotli_lgwin, lgblock=config.brotli_lgblock, ) def write(self, body: bytes) -> None: self.buffer.write(self.compressor.process(body)) self.buffer.write(self.compressor.flush()) def close(self) -> None: self.buffer.write(self.compressor.finish()) litestar-2.16.0/litestar/middleware/compression/facade.py000066400000000000000000000022651500564371300235220ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, ClassVar, Protocol if TYPE_CHECKING: from io import BytesIO from litestar.config.compression import CompressionConfig from litestar.enums import CompressionEncoding class CompressionFacade(Protocol): """A unified facade offering a uniform interface for different compression libraries.""" __slots__ = () encoding: ClassVar[str] """The encoding of the compression.""" def __init__( self, buffer: BytesIO, compression_encoding: CompressionEncoding | str, config: CompressionConfig ) -> None: """Initialize ``CompressionFacade``. Args: buffer: A bytes IO buffer to write the compressed data into. compression_encoding: The compression encoding used. config: The app compression config. """ ... def write(self, body: bytes) -> None: """Write compressed bytes. Args: body: Message body to process Returns: None """ ... def close(self) -> None: """Close the compression stream. Returns: None """ ... litestar-2.16.0/litestar/middleware/compression/gzip_facade.py000066400000000000000000000017451500564371300245550ustar00rootroot00000000000000from __future__ import annotations from gzip import GzipFile from typing import TYPE_CHECKING, Literal from litestar.enums import CompressionEncoding from litestar.middleware.compression.facade import CompressionFacade if TYPE_CHECKING: from io import BytesIO from litestar.config.compression import CompressionConfig class GzipCompression(CompressionFacade): __slots__ = ("buffer", "compression_encoding", "compressor") encoding = CompressionEncoding.GZIP def __init__( self, buffer: BytesIO, compression_encoding: Literal[CompressionEncoding.GZIP] | str, config: CompressionConfig ) -> None: self.buffer = buffer self.compression_encoding = compression_encoding self.compressor = GzipFile(mode="wb", fileobj=buffer, compresslevel=config.gzip_compress_level) def write(self, body: bytes) -> None: self.compressor.write(body) self.compressor.flush() def close(self) -> None: self.compressor.close() litestar-2.16.0/litestar/middleware/compression/middleware.py000066400000000000000000000153031500564371300244310ustar00rootroot00000000000000from __future__ import annotations from io import BytesIO from typing import TYPE_CHECKING, Any, Literal from litestar.datastructures import Headers, MutableScopeHeaders from litestar.enums import CompressionEncoding, ScopeType from litestar.middleware.base import AbstractMiddleware from litestar.middleware.compression.gzip_facade import GzipCompression from litestar.utils.empty import value_or_default from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from litestar.config.compression import CompressionConfig from litestar.middleware.compression.facade import CompressionFacade from litestar.types import ( ASGIApp, HTTPResponseStartEvent, Message, Receive, Scope, Send, ) try: from brotli import Compressor except ImportError: Compressor = Any class CompressionMiddleware(AbstractMiddleware): """Compression Middleware Wrapper. This is a wrapper allowing for generic compression configuration / handler middleware """ def __init__(self, app: ASGIApp, config: CompressionConfig) -> None: """Initialize ``CompressionMiddleware`` Args: app: The ``next`` ASGI app to call. config: An instance of CompressionConfig. """ super().__init__( app=app, exclude=config.exclude, exclude_opt_key=config.exclude_opt_key, scopes={ScopeType.HTTP} ) self.config = config async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI callable. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ accept_encoding = Headers.from_scope(scope).get("accept-encoding", "") config = self.config if config.compression_facade.encoding in accept_encoding: await self.app( scope, receive, self.create_compression_send_wrapper( send=send, compression_encoding=config.compression_facade.encoding, scope=scope ), ) return if config.gzip_fallback and CompressionEncoding.GZIP in accept_encoding: await self.app( scope, receive, self.create_compression_send_wrapper( send=send, compression_encoding=CompressionEncoding.GZIP, scope=scope ), ) return await self.app(scope, receive, send) def create_compression_send_wrapper( self, send: Send, compression_encoding: Literal[CompressionEncoding.BROTLI, CompressionEncoding.GZIP] | str, scope: Scope, ) -> Send: """Wrap ``send`` to handle brotli compression. Args: send: The ASGI send function. compression_encoding: The compression encoding used. scope: The ASGI connection scope Returns: An ASGI send function. """ bytes_buffer = BytesIO() facade: CompressionFacade # We can't use `self.config.compression_facade` directly if the compression is `gzip` since # it may be being used as a fallback. if compression_encoding == CompressionEncoding.GZIP: facade = GzipCompression(buffer=bytes_buffer, compression_encoding=compression_encoding, config=self.config) else: facade = self.config.compression_facade( buffer=bytes_buffer, compression_encoding=compression_encoding, config=self.config ) initial_message: HTTPResponseStartEvent | None = None started = False connection_state = ScopeState.from_scope(scope) async def send_wrapper(message: Message) -> None: """Handle and compresses the HTTP Message with brotli. Args: message (Message): An ASGI Message. """ nonlocal started nonlocal initial_message if message["type"] == "http.response.start": initial_message = message return if initial_message is not None and value_or_default(connection_state.is_cached, False): await send(initial_message) await send(message) facade.close() return if initial_message and message["type"] == "http.disconnect": facade.close() return if initial_message and message["type"] == "http.response.body": body = message["body"] more_body = message.get("more_body") if not started: started = True if more_body: headers = MutableScopeHeaders(initial_message) headers["Content-Encoding"] = compression_encoding headers.extend_header_value("vary", "Accept-Encoding") del headers["Content-Length"] connection_state.response_compressed = True facade.write(body) message["body"] = bytes_buffer.getvalue() bytes_buffer.seek(0) bytes_buffer.truncate() await send(initial_message) await send(message) elif len(body) >= self.config.minimum_size: facade.write(body) facade.close() body = bytes_buffer.getvalue() headers = MutableScopeHeaders(initial_message) headers["Content-Encoding"] = compression_encoding headers["Content-Length"] = str(len(body)) headers.extend_header_value("vary", "Accept-Encoding") message["body"] = body connection_state.response_compressed = True await send(initial_message) await send(message) else: facade.close() await send(initial_message) await send(message) else: facade.write(body) if not more_body: facade.close() message["body"] = bytes_buffer.getvalue() bytes_buffer.seek(0) bytes_buffer.truncate() if not more_body: bytes_buffer.close() await send(message) return send_wrapper litestar-2.16.0/litestar/middleware/cors.py000066400000000000000000000010661500564371300207220ustar00rootroot00000000000000from __future__ import annotations from typing import Any from litestar.middleware._internal import cors from litestar.utils.deprecation import warn_deprecation def __getattr__(name: str) -> Any: if name == "CORSMiddleware": warn_deprecation( version="2.9", deprecated_name=name, kind="class", removal_in="3.0", info="CORS middleware has been removed from the public API.", ) return cors.CORSMiddleware raise AttributeError(f"module {__name__} has no attribute {name}") litestar-2.16.0/litestar/middleware/csrf.py000066400000000000000000000147741500564371300207230ustar00rootroot00000000000000from __future__ import annotations import hashlib import hmac import secrets from secrets import compare_digest from typing import TYPE_CHECKING, Any from litestar.datastructures import MutableScopeHeaders from litestar.datastructures.cookie import Cookie from litestar.enums import RequestEncodingType, ScopeType from litestar.exceptions import PermissionDeniedException from litestar.middleware._utils import ( build_exclude_path_pattern, should_bypass_middleware, ) from litestar.middleware.base import MiddlewareProtocol from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from litestar.config.csrf import CSRFConfig from litestar.connection import Request from litestar.types import ( ASGIApp, HTTPSendMessage, Message, Receive, Scope, Scopes, Send, ) __all__ = ("CSRFMiddleware",) CSRF_SECRET_BYTES = 32 CSRF_SECRET_LENGTH = CSRF_SECRET_BYTES * 2 def generate_csrf_hash(token: str, secret: str) -> str: """Generate an HMAC that signs the CSRF token. Args: token: A hashed token. secret: A secret value. Returns: A CSRF hash. """ return hmac.new(secret.encode(), token.encode(), hashlib.sha256).hexdigest() def generate_csrf_token(secret: str) -> str: """Generate a CSRF token that includes a randomly generated string signed by an HMAC. Args: secret: A secret string. Returns: A unique CSRF token. """ token = secrets.token_hex(CSRF_SECRET_BYTES) token_hash = generate_csrf_hash(token=token, secret=secret) return token + token_hash class CSRFMiddleware(MiddlewareProtocol): """CSRF Middleware class. This Middleware protects against attacks by setting a CSRF cookie with a token and verifying it in request headers. """ scopes: Scopes = {ScopeType.HTTP} def __init__(self, app: ASGIApp, config: CSRFConfig) -> None: """Initialize ``CSRFMiddleware``. Args: app: The ``next`` ASGI app to call. config: The CSRFConfig instance. """ self.app = app self.config = config self.exclude = build_exclude_path_pattern(exclude=config.exclude, middleware_cls=type(self)) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI callable. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ if scope["type"] != ScopeType.HTTP: await self.app(scope, receive, send) return if should_bypass_middleware( scope=scope, scopes=self.scopes, exclude_opt_key=self.config.exclude_from_csrf_key, exclude_path_pattern=self.exclude, ): await self.app(scope, receive, send) return request: Request[Any, Any, Any] = scope["litestar_app"].request_class(scope=scope, receive=receive) content_type, _ = request.content_type csrf_cookie = request.cookies.get(self.config.cookie_name) existing_csrf_token = request.headers.get(self.config.header_name) if not existing_csrf_token and content_type in { RequestEncodingType.URL_ENCODED, RequestEncodingType.MULTI_PART, }: form = await request.form() existing_csrf_token = form.get("_csrf_token", None) connection_state = ScopeState.from_scope(scope) if request.method in self.config.safe_methods: token = connection_state.csrf_token = csrf_cookie or generate_csrf_token(secret=self.config.secret) await self.app(scope, receive, self.create_send_wrapper(send=send, csrf_cookie=csrf_cookie, token=token)) elif ( existing_csrf_token is not None and csrf_cookie is not None and self._csrf_tokens_match(existing_csrf_token, csrf_cookie) ): connection_state.csrf_token = existing_csrf_token await self.app(scope, receive, send) else: raise PermissionDeniedException("CSRF token verification failed") def create_send_wrapper(self, send: Send, token: str, csrf_cookie: str | None) -> Send: """Wrap ``send`` to handle CSRF validation. Args: token: The CSRF token. send: The ASGI send function. csrf_cookie: CSRF cookie. Returns: An ASGI send function. """ async def send_wrapper(message: Message) -> None: """Send function that wraps the original send to inject a cookie. Args: message: An ASGI ``Message`` Returns: None """ if csrf_cookie is None and message["type"] == "http.response.start": message.setdefault("headers", []) self._set_cookie_if_needed(message=message, token=token) await send(message) return send_wrapper def _set_cookie_if_needed(self, message: HTTPSendMessage, token: str) -> None: headers = MutableScopeHeaders.from_message(message) cookie = Cookie( key=self.config.cookie_name, value=token, path=self.config.cookie_path, secure=self.config.cookie_secure, httponly=self.config.cookie_httponly, samesite=self.config.cookie_samesite, domain=self.config.cookie_domain, ) headers.add("set-cookie", cookie.to_header(header="")) def _decode_csrf_token(self, token: str) -> str | None: """Decode a CSRF token and validate its HMAC.""" if len(token) < CSRF_SECRET_LENGTH + 1: return None token_secret = token[:CSRF_SECRET_LENGTH] existing_hash = token[CSRF_SECRET_LENGTH:] expected_hash = generate_csrf_hash(token=token_secret, secret=self.config.secret) return token_secret if compare_digest(existing_hash, expected_hash) else None def _csrf_tokens_match(self, request_csrf_token: str, cookie_csrf_token: str) -> bool: """Take the CSRF tokens from the request and the cookie and verify both are valid and identical.""" decoded_request_token = self._decode_csrf_token(request_csrf_token) decoded_cookie_token = self._decode_csrf_token(cookie_csrf_token) if decoded_request_token is None or decoded_cookie_token is None: return False return compare_digest(decoded_request_token, decoded_cookie_token) litestar-2.16.0/litestar/middleware/exceptions/000077500000000000000000000000001500564371300215605ustar00rootroot00000000000000litestar-2.16.0/litestar/middleware/exceptions/__init__.py000066400000000000000000000011601500564371300236670ustar00rootroot00000000000000from __future__ import annotations from typing import Any from litestar.middleware._internal.exceptions import middleware from litestar.utils.deprecation import warn_deprecation def __getattr__(name: str) -> Any: if name == "ExceptionHandlerMiddleware": warn_deprecation( version="2.9", deprecated_name=name, kind="class", removal_in="3.0", info="ExceptionHandlerMiddleware has been removed from the public API.", ) return middleware.ExceptionHandlerMiddleware raise AttributeError(f"module {__name__} has no attribute {name}") litestar-2.16.0/litestar/middleware/exceptions/_debug_response.py000066400000000000000000000011151500564371300252730ustar00rootroot00000000000000from __future__ import annotations from typing import Any from litestar.exceptions import responses from litestar.utils.deprecation import warn_deprecation def __getattr__(name: str) -> Any: if name == "create_debug_response": warn_deprecation( version="2.9", deprecated_name=name, kind="function", removal_in="3.0", alternative="litestar.exceptions.responses.create_debug_response", ) return responses.create_debug_response raise AttributeError(f"module {__name__} has no attribute {name}") litestar-2.16.0/litestar/middleware/exceptions/middleware.py000066400000000000000000000024631500564371300242540ustar00rootroot00000000000000from __future__ import annotations from typing import Any from litestar.exceptions import responses from litestar.middleware._internal.exceptions import middleware from litestar.utils.deprecation import warn_deprecation def __getattr__(name: str) -> Any: if name == "ExceptionHandlerMiddleware": warn_deprecation( version="2.9", deprecated_name=name, kind="class", removal_in="3.0", info="ExceptionHandlerMiddleware has been removed from the public API.", ) return middleware.ExceptionHandlerMiddleware if name == "create_exception_response": warn_deprecation( version="2.9", deprecated_name=name, kind="function", removal_in="3.0", alternative="litestar.exceptions.responses.create_exception_response", ) return responses.create_exception_response if name == "ExceptionResponseContent": warn_deprecation( version="2.9", deprecated_name=name, kind="class", removal_in="3.0", alternative="litestar.exceptions.responses.ExceptionResponseContent", ) return responses.ExceptionResponseContent raise AttributeError(f"module {__name__} has no attribute {name}") litestar-2.16.0/litestar/middleware/logging.py000066400000000000000000000323271500564371300214060ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Collection, Iterable from litestar.constants import ( HTTP_RESPONSE_BODY, HTTP_RESPONSE_START, ) from litestar.data_extractors import ( ConnectionDataExtractor, RequestExtractorField, ResponseDataExtractor, ResponseExtractorField, ) from litestar.enums import ScopeType from litestar.exceptions import ImproperlyConfiguredException from litestar.middleware.base import AbstractMiddleware, DefineMiddleware from litestar.serialization import encode_json from litestar.utils.empty import value_or_default from litestar.utils.scope import get_serializer_from_scope from litestar.utils.scope.state import ScopeState __all__ = ("LoggingMiddleware", "LoggingMiddlewareConfig") if TYPE_CHECKING: from litestar.connection import Request from litestar.types import ( ASGIApp, Logger, Message, Receive, Scope, Send, Serializer, ) try: from structlog.types import BindableLogger structlog_installed = True except ImportError: BindableLogger = object # type: ignore[assignment, misc] structlog_installed = False class LoggingMiddleware(AbstractMiddleware): """Logging middleware.""" logger: Logger def __init__(self, app: ASGIApp, config: LoggingMiddlewareConfig) -> None: """Initialize ``LoggingMiddleware``. Args: app: The ``next`` ASGI app to call. config: An instance of LoggingMiddlewareConfig. """ super().__init__( app=app, scopes={ScopeType.HTTP}, exclude=config.exclude, exclude_opt_key=config.exclude_opt_key ) self.is_struct_logger = structlog_installed self.config = config self.request_extractor = ConnectionDataExtractor( extract_body="body" in self.config.request_log_fields, extract_client="client" in self.config.request_log_fields, extract_content_type="content_type" in self.config.request_log_fields, extract_cookies="cookies" in self.config.request_log_fields, extract_headers="headers" in self.config.request_log_fields, extract_method="method" in self.config.request_log_fields, extract_path="path" in self.config.request_log_fields, extract_path_params="path_params" in self.config.request_log_fields, extract_query="query" in self.config.request_log_fields, extract_scheme="scheme" in self.config.request_log_fields, obfuscate_cookies=self.config.request_cookies_to_obfuscate, obfuscate_headers=self.config.request_headers_to_obfuscate, parse_body=self.is_struct_logger, parse_query=self.is_struct_logger, skip_parse_malformed_body=True, ) self.response_extractor = ResponseDataExtractor( extract_body="body" in self.config.response_log_fields, extract_headers="headers" in self.config.response_log_fields, extract_status_code="status_code" in self.config.response_log_fields, obfuscate_cookies=self.config.response_cookies_to_obfuscate, obfuscate_headers=self.config.response_headers_to_obfuscate, ) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI callable. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ if not hasattr(self, "logger"): self.logger = scope["litestar_app"].get_logger(self.config.logger_name) self.is_struct_logger = structlog_installed and repr(self.logger).startswith(" None: """Extract request data and log the message. Args: scope: The ASGI connection scope. receive: ASGI receive callable Returns: None """ extracted_data = await self.extract_request_data(request=scope["litestar_app"].request_class(scope, receive)) self.log_message(values=extracted_data) def log_response(self, scope: Scope) -> None: """Extract the response data and log the message. Args: scope: The ASGI connection scope. Returns: None """ extracted_data = self.extract_response_data(scope=scope) self.log_message(values=extracted_data) def log_message(self, values: dict[str, Any]) -> None: """Log a message. Args: values: Extract values to log. Returns: None """ message = values.pop("message") if self.is_struct_logger: self.logger.info(message, **values) else: value_strings = [f"{key}={value}" for key, value in values.items()] log_message = f"{message}: {', '.join(value_strings)}" self.logger.info(log_message) def _serialize_value(self, serializer: Serializer | None, value: Any) -> Any: if not self.is_struct_logger and isinstance(value, (dict, list, tuple, set)): value = encode_json(value, serializer) return value.decode("utf-8", errors="backslashreplace") if isinstance(value, bytes) else value async def extract_request_data(self, request: Request) -> dict[str, Any]: """Create a dictionary of values for the message. Args: request: A :class:`Request ` instance. Returns: An dict. """ data: dict[str, Any] = {"message": self.config.request_log_message} serializer = get_serializer_from_scope(request.scope) extracted_data = await self.request_extractor.extract(connection=request, fields=self.config.request_log_fields) for key in self.config.request_log_fields: data[key] = self._serialize_value(serializer, extracted_data.get(key)) return data def extract_response_data(self, scope: Scope) -> dict[str, Any]: """Extract data from the response. Args: scope: The ASGI connection scope. Returns: An dict. """ data: dict[str, Any] = {"message": self.config.response_log_message} serializer = get_serializer_from_scope(scope) connection_state = ScopeState.from_scope(scope) extracted_data = self.response_extractor( messages=( # NOTE: we don't pop the start message from the logging context in case # there are multiple body messages to be logged connection_state.log_context[HTTP_RESPONSE_START], connection_state.log_context.pop(HTTP_RESPONSE_BODY), ), ) response_body_compressed = value_or_default(connection_state.response_compressed, False) for key in self.config.response_log_fields: value: Any value = extracted_data.get(key) if key == "body" and response_body_compressed: if self.config.include_compressed_body: data[key] = value continue data[key] = self._serialize_value(serializer, value) return data def create_send_wrapper(self, scope: Scope, send: Send) -> Send: """Create a ``send`` wrapper, which handles logging response data. Args: scope: The ASGI connection scope. send: The ASGI send function. Returns: An ASGI send function. """ connection_state = ScopeState.from_scope(scope) async def send_wrapper(message: Message) -> None: if message["type"] == HTTP_RESPONSE_START: connection_state.log_context[HTTP_RESPONSE_START] = message elif message["type"] == HTTP_RESPONSE_BODY: connection_state.log_context[HTTP_RESPONSE_BODY] = message self.log_response(scope=scope) if not message.get("more_body"): connection_state.log_context.clear() await send(message) return send_wrapper @dataclass class LoggingMiddlewareConfig: """Configuration for ``LoggingMiddleware``""" exclude: str | list[str] | None = field(default=None) """List of paths to exclude from logging.""" exclude_opt_key: str | None = field(default=None) """An identifier to use on routes to disable logging for a particular route.""" include_compressed_body: bool = field(default=False) """Include body of compressed response in middleware. If `"body"` not set in. :attr:`response_log_fields ` this config value is ignored. """ logger_name: str = field(default="litestar") """Name of the logger to retrieve using `app.get_logger("")`.""" request_cookies_to_obfuscate: set[str] = field(default_factory=lambda: {"session"}) """Request cookie keys to obfuscate. Obfuscated values are replaced with '*****'. """ request_headers_to_obfuscate: set[str] = field(default_factory=lambda: {"Authorization", "X-API-KEY"}) """Request header keys to obfuscate. Obfuscated values are replaced with '*****'. """ response_cookies_to_obfuscate: set[str] = field(default_factory=lambda: {"session"}) """Response cookie keys to obfuscate. Obfuscated values are replaced with '*****'. """ response_headers_to_obfuscate: set[str] = field(default_factory=lambda: {"Authorization", "X-API-KEY"}) """Response header keys to obfuscate. Obfuscated values are replaced with '*****'. """ request_log_message: str = field(default="HTTP Request") """Log message to prepend when logging a request.""" response_log_message: str = field(default="HTTP Response") """Log message to prepend when logging a response.""" request_log_fields: Collection[RequestExtractorField] = field( default=( "path", "method", "content_type", "headers", "cookies", "query", "path_params", "body", ) ) """Fields to extract and log from the request. Notes: - The order of fields in the iterable determines the order of the log message logged out. Thus, re-arranging the log-message is as simple as changing the iterable. - To turn off logging of requests, use and empty iterable. """ response_log_fields: Collection[ResponseExtractorField] = field( default=( "status_code", "cookies", "headers", "body", ) ) """Fields to extract and log from the response. The order of fields in the iterable determines the order of the log message logged out. Notes: - The order of fields in the iterable determines the order of the log message logged out. Thus, re-arranging the log-message is as simple as changing the iterable. - To turn off logging of responses, use and empty iterable. """ middleware_class: type[LoggingMiddleware] = field(default=LoggingMiddleware) """Middleware class to use. Should be a subclass of [litestar.middleware.LoggingMiddleware]. """ def __post_init__(self) -> None: """Override default Pydantic type conversion for iterables. Args: value: An iterable Returns: The `value` argument cast as a tuple. """ if not isinstance(self.response_log_fields, Iterable): raise ImproperlyConfiguredException("response_log_fields must be a valid Iterable") if not isinstance(self.request_log_fields, Iterable): raise ImproperlyConfiguredException("request_log_fields must be a valid Iterable") self.response_log_fields = tuple(self.response_log_fields) self.request_log_fields = tuple(self.request_log_fields) @property def middleware(self) -> DefineMiddleware: """Use this property to insert the config into a middleware list on one of the application layers. Examples: .. code-block:: python from litestar import Litestar, Request, get from litestar.logging import LoggingConfig from litestar.middleware.logging import LoggingMiddlewareConfig logging_config = LoggingConfig() logging_middleware_config = LoggingMiddlewareConfig() @get("/") def my_handler(request: Request) -> None: ... app = Litestar( route_handlers=[my_handler], logging_config=logging_config, middleware=[logging_middleware_config.middleware], ) Returns: An instance of DefineMiddleware including ``self`` as the config kwarg value. """ return DefineMiddleware(self.middleware_class, config=self) litestar-2.16.0/litestar/middleware/rate_limit.py000066400000000000000000000247531500564371300221150ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from time import time from typing import TYPE_CHECKING, Any, Callable, Literal, cast from litestar.datastructures import MutableScopeHeaders from litestar.enums import ScopeType from litestar.exceptions import TooManyRequestsException from litestar.middleware.base import AbstractMiddleware, DefineMiddleware from litestar.serialization import decode_json, encode_json from litestar.utils import ensure_async_callable __all__ = ("CacheObject", "RateLimitConfig", "RateLimitMiddleware") if TYPE_CHECKING: from typing import Awaitable from litestar import Litestar from litestar.connection import Request from litestar.stores.base import Store from litestar.types import ASGIApp, Message, Receive, Scope, Send, SyncOrAsyncUnion DurationUnit = Literal["second", "minute", "hour", "day"] DURATION_VALUES: dict[DurationUnit, int] = {"second": 1, "minute": 60, "hour": 3600, "day": 86400} @dataclass class CacheObject: """Representation of a cached object's metadata.""" __slots__ = ("history", "reset") history: list[int] reset: int class RateLimitMiddleware(AbstractMiddleware): """Rate-limiting middleware.""" def __init__(self, app: ASGIApp, config: RateLimitConfig) -> None: """Initialize ``RateLimitMiddleware``. Args: app: The ``next`` ASGI app to call. config: An instance of RateLimitConfig. """ super().__init__( app=app, exclude=config.exclude, exclude_opt_key=config.exclude_opt_key, scopes={ScopeType.HTTP} ) self.check_throttle_handler = cast("Callable[[Request], Awaitable[bool]] | None", config.check_throttle_handler) self.config = config self.max_requests: int = config.rate_limit[1] self.unit: DurationUnit = config.rate_limit[0] async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI callable. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ app = scope["litestar_app"] request: Request[Any, Any, Any] = app.request_class(scope) store = self.config.get_store_from_app(app) if await self.should_check_request(request=request): key = self.cache_key_from_request(request=request) cache_object = await self.retrieve_cached_history(key, store) if len(cache_object.history) >= self.max_requests: raise TooManyRequestsException( headers=self.create_response_headers(cache_object=cache_object) if self.config.set_rate_limit_headers else None ) await self.set_cached_history(key=key, cache_object=cache_object, store=store) if self.config.set_rate_limit_headers: send = self.create_send_wrapper(send=send, cache_object=cache_object) await self.app(scope, receive, send) # pyright: ignore def create_send_wrapper(self, send: Send, cache_object: CacheObject) -> Send: """Create a ``send`` function that wraps the original send to inject response headers. Args: send: The ASGI send function. cache_object: A StorageObject instance. Returns: Send wrapper callable. """ async def send_wrapper(message: Message) -> None: """Wrap the ASGI ``Send`` callable. Args: message: An ASGI ``Message`` Returns: None """ if message["type"] == "http.response.start": message.setdefault("headers", []) headers = MutableScopeHeaders(message) for key, value in self.create_response_headers(cache_object=cache_object).items(): headers[key] = value await send(message) return send_wrapper def cache_key_from_request(self, request: Request[Any, Any, Any]) -> str: """Get a cache-key from a ``Request`` Args: request: A :class:`Request <.connection.Request>` instance. Returns: A cache key. """ host = request.client.host if request.client else "anonymous" identifier = request.headers.get("X-Forwarded-For") or request.headers.get("X-Real-IP") or host route_handler = request.scope["route_handler"] if getattr(route_handler, "is_mount", False): identifier += "::mount" if getattr(route_handler, "is_static", False): identifier += "::static" return f"{type(self).__name__}::{identifier}" async def retrieve_cached_history(self, key: str, store: Store) -> CacheObject: """Retrieve a list of time stamps for the given duration unit. Args: key: Cache key. store: A :class:`Store <.stores.base.Store>` Returns: An :class:`CacheObject`. """ duration = DURATION_VALUES[self.unit] now = int(time()) cached_string = await store.get(key) if cached_string: cache_object = CacheObject(**decode_json(value=cached_string)) if cache_object.reset <= now: return CacheObject(history=[], reset=now + duration) while cache_object.history and cache_object.history[-1] <= now - duration: cache_object.history.pop() return cache_object return CacheObject(history=[], reset=now + duration) async def set_cached_history(self, key: str, cache_object: CacheObject, store: Store) -> None: """Store history extended with the current timestamp in cache. Args: key: Cache key. cache_object: A :class:`CacheObject`. store: A :class:`Store <.stores.base.Store>` Returns: None """ cache_object.history = [int(time()), *cache_object.history] await store.set(key, encode_json(cache_object), expires_in=DURATION_VALUES[self.unit]) async def should_check_request(self, request: Request[Any, Any, Any]) -> bool: """Return a boolean indicating if a request should be checked for rate limiting. Args: request: A :class:`Request <.connection.Request>` instance. Returns: Boolean dictating whether the request should be checked for rate-limiting. """ if self.check_throttle_handler: return await self.check_throttle_handler(request) return True def create_response_headers(self, cache_object: CacheObject) -> dict[str, str]: """Create ratelimit response headers. Notes: * see the `IETF RateLimit draft `_ Args: cache_object:A :class:`CacheObject`. Returns: A dict of http headers. """ remaining_requests = str( self.max_requests - len(cache_object.history) if len(cache_object.history) <= self.max_requests else 0 ) return { self.config.rate_limit_policy_header_key: f"{self.max_requests}; w={DURATION_VALUES[self.unit]}", self.config.rate_limit_limit_header_key: str(self.max_requests), self.config.rate_limit_remaining_header_key: remaining_requests, self.config.rate_limit_reset_header_key: str(cache_object.reset - int(time())), } @dataclass class RateLimitConfig: """Configuration for ``RateLimitMiddleware``""" rate_limit: tuple[DurationUnit, int] """A tuple containing a time unit (second, minute, hour, day) and quantity, e.g. ("day", 1) or ("minute", 5).""" exclude: str | list[str] | None = field(default=None) """A pattern or list of patterns to skip in the rate limiting middleware.""" exclude_opt_key: str | None = field(default=None) """An identifier to use on routes to disable rate limiting for a particular route.""" check_throttle_handler: Callable[[Request[Any, Any, Any]], SyncOrAsyncUnion[bool]] | None = field(default=None) """Handler callable that receives the request instance, returning a boolean dictating whether or not the request should be checked for rate limiting. """ middleware_class: type[RateLimitMiddleware] = field(default=RateLimitMiddleware) """The middleware class to use.""" set_rate_limit_headers: bool = field(default=True) """Boolean dictating whether to set the rate limit headers on the response.""" rate_limit_policy_header_key: str = field(default="RateLimit-Policy") """Key to use for the rate limit policy header.""" rate_limit_remaining_header_key: str = field(default="RateLimit-Remaining") """Key to use for the rate limit remaining header.""" rate_limit_reset_header_key: str = field(default="RateLimit-Reset") """Key to use for the rate limit reset header.""" rate_limit_limit_header_key: str = field(default="RateLimit-Limit") """Key to use for the rate limit limit header.""" store: str = "rate_limit" """Name of the :class:`Store <.stores.base.Store>` to use""" def __post_init__(self) -> None: if self.check_throttle_handler: self.check_throttle_handler = ensure_async_callable(self.check_throttle_handler) # type: ignore[arg-type] @property def middleware(self) -> DefineMiddleware: """Use this property to insert the config into a middleware list on one of the application layers. Examples: .. code-block:: python from litestar import Litestar, Request, get from litestar.middleware.rate_limit import RateLimitConfig # limit to 10 requests per minute, excluding the schema path throttle_config = RateLimitConfig(rate_limit=("minute", 10), exclude=["/schema"]) @get("/") def my_handler(request: Request) -> None: ... app = Litestar(route_handlers=[my_handler], middleware=[throttle_config.middleware]) Returns: An instance of :class:`DefineMiddleware <.middleware.base.DefineMiddleware>` including ``self`` as the config kwarg value. """ return DefineMiddleware(self.middleware_class, config=self) def get_store_from_app(self, app: Litestar) -> Store: """Get the store defined in :attr:`store` from an :class:`Litestar <.app.Litestar>` instance.""" return app.stores.get(self.store) litestar-2.16.0/litestar/middleware/response_cache.py000066400000000000000000000046051500564371300227370ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, cast from msgspec.msgpack import encode as encode_msgpack from litestar import Request from litestar.constants import HTTP_RESPONSE_BODY, HTTP_RESPONSE_START from litestar.enums import ScopeType from litestar.utils.empty import value_or_default from litestar.utils.scope.state import ScopeState from .base import AbstractMiddleware if TYPE_CHECKING: from litestar.config.response_cache import ResponseCacheConfig from litestar.handlers import HTTPRouteHandler from litestar.types import ASGIApp, HTTPScope, Message, Receive, Scope, Send __all__ = ["ResponseCacheMiddleware"] class ResponseCacheMiddleware(AbstractMiddleware): def __init__(self, app: ASGIApp, config: ResponseCacheConfig) -> None: self.config = config super().__init__(app=app, scopes={ScopeType.HTTP}) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: route_handler = cast("HTTPRouteHandler", scope["route_handler"]) expires_in: int | None = None if route_handler.cache is True: expires_in = self.config.default_expiration elif route_handler.cache is not False and isinstance(route_handler.cache, int): expires_in = route_handler.cache connection_state = ScopeState.from_scope(scope) messages: list[Message] = [] async def wrapped_send(message: Message) -> None: if not value_or_default(connection_state.is_cached, False): if message["type"] == HTTP_RESPONSE_START: do_cache = connection_state.do_cache = self.config.cache_response_filter( cast("HTTPScope", scope), message["status"] ) if do_cache: messages.append(message) elif value_or_default(connection_state.do_cache, False): messages.append(message) if messages and message["type"] == HTTP_RESPONSE_BODY and not message.get("more_body"): key = (route_handler.cache_key_builder or self.config.key_builder)(Request(scope)) store = self.config.get_store_from_app(scope["litestar_app"]) await store.set(key, encode_msgpack(messages), expires_in=expires_in) await send(message) await self.app(scope, receive, wrapped_send) litestar-2.16.0/litestar/middleware/session/000077500000000000000000000000001500564371300210625ustar00rootroot00000000000000litestar-2.16.0/litestar/middleware/session/__init__.py000066400000000000000000000001061500564371300231700ustar00rootroot00000000000000from .base import SessionMiddleware __all__ = ("SessionMiddleware",) litestar-2.16.0/litestar/middleware/session/base.py000066400000000000000000000204471500564371300223550ustar00rootroot00000000000000from __future__ import annotations from abc import ABC, abstractmethod from typing import ( TYPE_CHECKING, Any, Awaitable, Callable, Generic, Literal, TypeVar, cast, ) from litestar.connection import ASGIConnection from litestar.enums import ScopeType from litestar.middleware.base import AbstractMiddleware, DefineMiddleware from litestar.serialization import decode_json, encode_json from litestar.utils import get_serializer_from_scope __all__ = ("BaseBackendConfig", "BaseSessionBackend", "SessionMiddleware") if TYPE_CHECKING: from litestar.types import ASGIApp, Message, Receive, Scope, Scopes, ScopeSession, Send ONE_DAY_IN_SECONDS = 60 * 60 * 24 ConfigT = TypeVar("ConfigT", bound="BaseBackendConfig") BaseSessionBackendT = TypeVar("BaseSessionBackendT", bound="BaseSessionBackend") class BaseBackendConfig(ABC, Generic[BaseSessionBackendT]): # pyright: ignore """Configuration for Session middleware backends.""" _backend_class: type[BaseSessionBackendT] # pyright: ignore key: str """Key to use for the cookie inside the header, e.g. ``session=`` where ``session`` is the cookie key and ```` is the session data. Notes: - If a session cookie exceeds 4KB in size it is split. In this case the key will be of the format ``session-{segment number}``. """ max_age: int """Maximal age of the cookie before its invalidated.""" scopes: Scopes = {ScopeType.HTTP, ScopeType.WEBSOCKET} """Scopes for the middleware - options are ``http`` and ``websocket`` with the default being both""" path: str """Path fragment that must exist in the request url for the cookie to be valid. Defaults to ``'/'``. """ domain: str | None """Domain for which the cookie is valid.""" secure: bool """Https is required for the cookie.""" httponly: bool """Forbids javascript to access the cookie via 'Document.cookie'.""" samesite: Literal["lax", "strict", "none"] """Controls whether or not a cookie is sent with cross-site requests. Defaults to ``lax``. """ exclude: str | list[str] | None """A pattern or list of patterns to skip in the session middleware.""" exclude_opt_key: str """An identifier to use on routes to disable the session middleware for a particular route.""" @property def middleware(self) -> DefineMiddleware: """Use this property to insert the config into a middleware list on one of the application layers. Examples: .. code-block:: python from os import urandom from litestar import Litestar, Request, get from litestar.middleware.sessions.cookie_backend import CookieBackendConfig session_config = CookieBackendConfig(secret=urandom(16)) @get("/") def my_handler(request: Request) -> None: ... app = Litestar(route_handlers=[my_handler], middleware=[session_config.middleware]) Returns: An instance of DefineMiddleware including ``self`` as the config kwarg value. """ return DefineMiddleware(SessionMiddleware, backend=self._backend_class(config=self)) class BaseSessionBackend(ABC, Generic[ConfigT]): """Abstract session backend defining the interface between a storage mechanism and the application :class:`SessionMiddleware`. This serves as the base class for all client- and server-side backends """ __slots__ = ("config",) def __init__(self, config: ConfigT) -> None: """Initialize ``BaseSessionBackend`` Args: config: A instance of a subclass of ``BaseBackendConfig`` """ self.config = config @staticmethod def serialize_data(data: ScopeSession, scope: Scope | None = None) -> bytes: """Serialize data into bytes for storage in the backend. Args: data: Session data of the current scope. scope: A scope, if applicable, from which to extract a serializer. Notes: - The serializer will be extracted from ``scope`` or fall back to :func:`default_serializer <.serialization.default_serializer>` Returns: ``data`` serialized as bytes. """ serializer = get_serializer_from_scope(scope) if scope else None return encode_json(data, serializer) @staticmethod def deserialize_data(data: Any) -> dict[str, Any]: """Deserialize data into a dictionary for use in the application scope. Args: data: Data to be deserialized Returns: Deserialized data as a dictionary """ return cast("dict[str, Any]", decode_json(value=data)) @abstractmethod def get_session_id(self, connection: ASGIConnection) -> str | None: """Try to fetch session id from connection ScopeState. If one does not exist, generate one. Args: connection: Originating ASGIConnection containing the scope Returns: Session id str or None if the concept of a session id does not apply. """ @abstractmethod async def store_in_message(self, scope_session: ScopeSession, message: Message, connection: ASGIConnection) -> None: """Store the necessary information in the outgoing ``Message`` Args: scope_session: Current session to store message: Outgoing send-message connection: Originating ASGIConnection containing the scope Returns: None """ @abstractmethod async def load_from_connection(self, connection: ASGIConnection) -> dict[str, Any]: """Load session data from a connection and return it as a dictionary to be used in the current application scope. Args: connection: An ASGIConnection instance Returns: The session data Notes: - This should not modify the connection's scope. The data returned by this method will be stored in the application scope by the middleware """ class SessionMiddleware(AbstractMiddleware, Generic[BaseSessionBackendT]): """Litestar session middleware for storing session data.""" def __init__(self, app: ASGIApp, backend: BaseSessionBackendT) -> None: """Initialize ``SessionMiddleware`` Args: app: An ASGI application backend: A :class:`BaseSessionBackend` instance used to store and retrieve session data """ super().__init__( app=app, exclude=backend.config.exclude, exclude_opt_key=backend.config.exclude_opt_key, scopes=backend.config.scopes, ) self.backend = backend def create_send_wrapper(self, connection: ASGIConnection) -> Callable[[Message], Awaitable[None]]: """Create a wrapper for the ASGI send function, which handles setting the cookies on the outgoing response. Args: connection: ASGIConnection Returns: None """ async def wrapped_send(message: Message) -> None: """Wrap the ``send`` function. Declared in local scope to make use of closure values. Args: message: An ASGI message. Returns: None """ if message["type"] != "http.response.start": await connection.send(message) return scope_session = connection.scope.get("session") await self.backend.store_in_message(scope_session, message, connection) await connection.send(message) return wrapped_send async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI-callable. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ connection = ASGIConnection[Any, Any, Any, Any](scope, receive=receive, send=send) scope["session"] = await self.backend.load_from_connection(connection) connection._connection_state.session_id = self.backend.get_session_id(connection) # pyright: ignore [reportGeneralTypeIssues] await self.app(scope, receive, self.create_send_wrapper(connection)) litestar-2.16.0/litestar/middleware/session/client_side.py000066400000000000000000000250671500564371300237300ustar00rootroot00000000000000from __future__ import annotations import binascii import contextlib import re import time from base64 import b64decode, b64encode from dataclasses import dataclass, field, fields from os import urandom from typing import TYPE_CHECKING, Any, Final, Literal, Mapping from litestar.datastructures import MutableScopeHeaders from litestar.datastructures.cookie import Cookie from litestar.enums import ScopeType from litestar.exceptions import ( ImproperlyConfiguredException, MissingDependencyException, ) from litestar.serialization import decode_json, encode_json from litestar.types import Empty, Scopes from litestar.utils.dataclass import extract_dataclass_items from .base import ONE_DAY_IN_SECONDS, BaseBackendConfig, BaseSessionBackend __all__ = ("ClientSideSessionBackend", "CookieBackendConfig") try: from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives.ciphers.aead import AESGCM except ImportError as e: raise MissingDependencyException("cryptography") from e if TYPE_CHECKING: from litestar.connection import ASGIConnection from litestar.types import Message, Scope, ScopeSession NONCE_SIZE = 12 CHUNK_SIZE = 4096 - 64 AAD = b"additional_authenticated_data=" SET_COOKIE_INCLUDE = {f.name for f in fields(Cookie) if f.name not in {"key", "secret"}} CLEAR_COOKIE_INCLUDE = {f.name for f in fields(Cookie) if f.name not in {"key", "secret", "max_age"}} class ClientSideSessionBackend(BaseSessionBackend["CookieBackendConfig"]): """Cookie backend for SessionMiddleware.""" __slots__ = ( "_clear_cookie_params", "_set_cookie_params", "aesgcm", "cookie_re", ) def __init__(self, config: CookieBackendConfig) -> None: """Initialize ``ClientSideSessionBackend``. Args: config: SessionCookieConfig instance. """ super().__init__(config) self.aesgcm = AESGCM(config.secret) self.cookie_re = re.compile(rf"{self.config.key}(?:-\d+)?") self._set_cookie_params: Final[Mapping[str, Any]] = dict( extract_dataclass_items(config, exclude_none=True, include=SET_COOKIE_INCLUDE) ) self._clear_cookie_params: Final[Mapping[str, Any]] = dict( extract_dataclass_items(config, exclude_none=True, include=CLEAR_COOKIE_INCLUDE) ) def dump_data(self, data: Any, scope: Scope | None = None) -> list[bytes]: """Given serializable data, including pydantic models and numpy types, dump it into a bytes string, encrypt, encode and split it into chunks of the desirable size. Args: data: Data to serialize, encrypt, encode and chunk. scope: The ASGI connection scope. Notes: - The returned list is composed of a chunks of a single base64 encoded string that is encrypted using AES-CGM. Returns: List of encoded bytes string of a maximum length equal to the ``CHUNK_SIZE`` constant. """ serialized = self.serialize_data(data, scope) associated_data = encode_json({"expires_at": round(time.time()) + self.config.max_age}) nonce = urandom(NONCE_SIZE) encrypted = self.aesgcm.encrypt(nonce, serialized, associated_data=associated_data) encoded = b64encode(nonce + encrypted + AAD + associated_data) return [encoded[i : i + CHUNK_SIZE] for i in range(0, len(encoded), CHUNK_SIZE)] def load_data(self, data: list[bytes]) -> dict[str, Any]: """Given a list of strings, decodes them into the session object. Args: data: A list of strings derived from the request's session cookie(s). Returns: A deserialized session value. """ decoded = b64decode(b"".join(data)) nonce = decoded[:NONCE_SIZE] aad_starts_from = decoded.find(AAD) associated_data = decoded[aad_starts_from:].replace(AAD, b"") if aad_starts_from != -1 else None if associated_data and decode_json(value=associated_data)["expires_at"] > round(time.time()): encrypted_session = decoded[NONCE_SIZE:aad_starts_from] decrypted = self.aesgcm.decrypt(nonce, encrypted_session, associated_data=associated_data) return self.deserialize_data(decrypted) return {} def get_cookie_keys(self, connection: ASGIConnection) -> list[str]: """Return a list of cookie-keys from the connection if they match the session-cookie pattern. Args: connection: An ASGIConnection instance Returns: A list of session-cookie keys """ return sorted(key for key in connection.cookies if self.cookie_re.fullmatch(key)) def get_cookie_key_set(self, connection: ASGIConnection) -> set[str]: """Return a set of cookie-keys from the connection if they match the session-cookie pattern. .. versionadded:: 2.8.3 Args: connection: An ASGIConnection instance Returns: A set of session-cookie keys """ return {key for key in connection.cookies if self.cookie_re.fullmatch(key)} def _create_session_cookies(self, data: list[bytes]) -> list[Cookie]: """Create a list of cookies containing the session data. If the data is split into multiple cookies, the key will be of the format ``session-{segment number}``, however if only one cookie is needed, the key will be ``session``. """ cookie_params = self._set_cookie_params if len(data) == 1: return [ Cookie( value=data[0].decode("utf-8"), key=self.config.key, **cookie_params, ) ] return [ Cookie( value=datum.decode("utf-8"), key=f"{self.config.key}-{i}", **cookie_params, ) for i, datum in enumerate(data) ] async def store_in_message(self, scope_session: ScopeSession, message: Message, connection: ASGIConnection) -> None: """Store data from ``scope_session`` in ``Message`` in the form of cookies. If the contents of ``scope_session`` are too large to fit a single cookie, it will be split across several cookies, following the naming scheme of ``-``. If the session is empty or shrinks, cookies will be cleared by setting their value to ``"null"`` Args: scope_session: Current session to store message: Outgoing send-message connection: Originating ASGIConnection containing the scope Returns: None """ scope = connection.scope headers = MutableScopeHeaders.from_message(message) connection_cookies = self.get_cookie_key_set(connection) response_cookies: set[str] = set() if scope_session and scope_session is not Empty: data = self.dump_data(scope_session, scope=scope) for cookie in self._create_session_cookies(data): headers.add("Set-Cookie", cookie.to_header(header="")) response_cookies.add(cookie.key) cookies_to_clear = connection_cookies - response_cookies else: cookies_to_clear = connection_cookies for cookie_key in cookies_to_clear: headers.add( "Set-Cookie", Cookie(value="null", key=cookie_key, expires=0, **self._clear_cookie_params).to_header(header=""), ) async def load_from_connection(self, connection: ASGIConnection) -> dict[str, Any]: """Load session data from a connection's session-cookies and return it as a dictionary. Args: connection: Originating ASGIConnection Returns: The session data """ if cookie_keys := self.get_cookie_keys(connection): data = [connection.cookies[key].encode("utf-8") for key in cookie_keys] # If these exceptions occur, the session must remain empty so do nothing. with contextlib.suppress(InvalidTag, binascii.Error): return self.load_data(data) return {} def get_session_id(self, connection: ASGIConnection) -> str | None: return None @dataclass class CookieBackendConfig(BaseBackendConfig[ClientSideSessionBackend]): # pyright: ignore """Configuration for [SessionMiddleware] middleware.""" _backend_class = ClientSideSessionBackend secret: bytes """A secret key to use for generating an encryption key. Must have a length of 16 (128 bits), 24 (192 bits) or 32 (256 bits) characters. """ key: str = field(default="session") """Key to use for the cookie inside the header, e.g. ``session=`` where ``session`` is the cookie key and ```` is the session data. Notes: - If a session cookie exceeds 4KB in size it is split. In this case the key will be of the format ``session-{segment number}``. """ max_age: int = field(default=ONE_DAY_IN_SECONDS * 14) """Maximal age of the cookie before its invalidated.""" scopes: Scopes = field(default_factory=lambda: {ScopeType.HTTP, ScopeType.WEBSOCKET}) """Scopes for the middleware - options are ``http`` and ``websocket`` with the default being both""" path: str = field(default="/") """Path fragment that must exist in the request url for the cookie to be valid. Defaults to ``'/'``. """ domain: str | None = field(default=None) """Domain for which the cookie is valid.""" secure: bool = field(default=False) """Https is required for the cookie.""" httponly: bool = field(default=True) """Forbids javascript to access the cookie via 'Document.cookie'.""" samesite: Literal["lax", "strict", "none"] = field(default="lax") """Controls whether or not a cookie is sent with cross-site requests. Defaults to ``lax``. """ exclude: str | list[str] | None = field(default=None) """A pattern or list of patterns to skip in the session middleware.""" exclude_opt_key: str = field(default="skip_session") """An identifier to use on routes to disable the session middleware for a particular route.""" def __post_init__(self) -> None: if len(self.key) < 1 or len(self.key) > 256: raise ImproperlyConfiguredException("key must be a string with a length between 1-256") if self.max_age < 1: raise ImproperlyConfiguredException("max_age must be greater than 0") if len(self.secret) not in {16, 24, 32}: raise ImproperlyConfiguredException("secret length must be 16 (128 bit), 24 (192 bit) or 32 (256 bit)") litestar-2.16.0/litestar/middleware/session/server_side.py000066400000000000000000000222531500564371300237520ustar00rootroot00000000000000from __future__ import annotations import secrets from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Literal from litestar.datastructures import Cookie, MutableScopeHeaders from litestar.enums import ScopeType from litestar.exceptions import ImproperlyConfiguredException from litestar.middleware.session.base import ONE_DAY_IN_SECONDS, BaseBackendConfig, BaseSessionBackend from litestar.types import Empty, Message, Scopes, ScopeSession from litestar.utils.dataclass import extract_dataclass_items __all__ = ("ServerSideSessionBackend", "ServerSideSessionConfig") if TYPE_CHECKING: from litestar import Litestar from litestar.connection import ASGIConnection from litestar.stores.base import Store class ServerSideSessionBackend(BaseSessionBackend["ServerSideSessionConfig"]): """Base class for server-side backends. Implements :class:`BaseSessionBackend` and defines and interface which subclasses can implement to facilitate the storage of session data. """ def __init__(self, config: ServerSideSessionConfig) -> None: """Initialize ``ServerSideSessionBackend`` Args: config: A subclass of ``ServerSideSessionConfig`` """ super().__init__(config=config) async def get(self, session_id: str, store: Store) -> bytes | None: """Retrieve data associated with ``session_id``. Args: session_id: The session-ID store: Store to retrieve the session data from Returns: The session data, if existing, otherwise ``None``. """ max_age = int(self.config.max_age) if self.config.max_age is not None else None return await store.get(session_id, renew_for=max_age if self.config.renew_on_access else None) async def set(self, session_id: str, data: bytes, store: Store) -> None: """Store ``data`` under the ``session_id`` for later retrieval. If there is already data associated with ``session_id``, replace it with ``data`` and reset its expiry time Args: session_id: The session-ID data: Serialized session data store: Store to save the session data in Returns: None """ expires_in = int(self.config.max_age) if self.config.max_age is not None else None await store.set(session_id, data, expires_in=expires_in) async def delete(self, session_id: str, store: Store) -> None: """Delete the data associated with ``session_id``. Fails silently if no such session-ID exists. Args: session_id: The session-ID store: Store to delete the session data from Returns: None """ await store.delete(session_id) def get_session_id(self, connection: ASGIConnection) -> str: """Try to fetch session id from the connection. If one does not exist, generate one. If a session ID already exists in the cookies, it is returned. If there is no ID in the cookies but one in the connection state, then the session exists but has not yet been returned to the user. Otherwise, a new session must be created. Args: connection: Originating ASGIConnection containing the scope Returns: Session id str or None if the concept of a session id does not apply. """ session_id = connection.cookies.get(self.config.key) if not session_id or session_id == "null": session_id = connection.get_session_id() if not session_id: session_id = self.generate_session_id() return session_id def generate_session_id(self) -> str: """Generate a new session-ID, with n=:attr:`session_id_bytes ` random bytes. Returns: A session-ID """ return secrets.token_hex(self.config.session_id_bytes) async def store_in_message(self, scope_session: ScopeSession, message: Message, connection: ASGIConnection) -> None: """Store the necessary information in the outgoing ``Message`` by setting a cookie containing the session-ID. If the session is empty, a null-cookie will be set. Otherwise, the serialised data will be stored using :meth:`set `, under the current session-id. If no session-ID exists, a new ID will be generated using :meth:`generate_session_id `. Args: scope_session: Current session to store message: Outgoing send-message connection: Originating ASGIConnection containing the scope Returns: None """ scope = connection.scope store = self.config.get_store_from_app(scope["app"]) headers = MutableScopeHeaders.from_message(message) session_id = self.get_session_id(connection) cookie_params = dict(extract_dataclass_items(self.config, exclude_none=True, include=Cookie.__dict__.keys())) if scope_session is Empty: await self.delete(session_id, store=store) headers.add( "Set-Cookie", Cookie(value="null", key=self.config.key, expires=0, **cookie_params).to_header(header=""), ) else: serialised_data = self.serialize_data(scope_session, scope) await self.set(session_id=session_id, data=serialised_data, store=store) headers.add( "Set-Cookie", Cookie(value=session_id, key=self.config.key, **cookie_params).to_header(header="") ) async def load_from_connection(self, connection: ASGIConnection) -> dict[str, Any]: """Load session data from a connection and return it as a dictionary to be used in the current application scope. The session-ID will be gathered from a cookie with the key set in :attr:`BaseBackendConfig.key`. If a cookie is found, its value will be used as the session-ID and data associated with this ID will be loaded using :meth:`get `. If no cookie was found or no data was loaded from the store, this will return an empty dictionary. Args: connection: An ASGIConnection instance Returns: The current session data """ if session_id := connection.cookies.get(self.config.key): store = self.config.get_store_from_app(connection.scope["app"]) data = await self.get(session_id, store=store) if data is not None: return self.deserialize_data(data) return {} @dataclass class ServerSideSessionConfig(BaseBackendConfig[ServerSideSessionBackend]): # pyright: ignore """Base configuration for server side backends.""" _backend_class = ServerSideSessionBackend session_id_bytes: int = field(default=32) """Number of bytes used to generate a random session-ID.""" renew_on_access: bool = field(default=False) """Renew expiry times of sessions when they're being accessed""" key: str = field(default="session") """Key to use for the cookie inside the header, e.g. ``session=`` where ``session`` is the cookie key and ```` is the session data. Notes: - If a session cookie exceeds 4KB in size it is split. In this case the key will be of the format ``session-{segment number}``. """ max_age: int = field(default=ONE_DAY_IN_SECONDS * 14) """Maximal age of the cookie before its invalidated.""" scopes: Scopes = field(default_factory=lambda: {ScopeType.HTTP, ScopeType.WEBSOCKET}) """Scopes for the middleware - options are ``http`` and ``websocket`` with the default being both""" path: str = field(default="/") """Path fragment that must exist in the request url for the cookie to be valid. Defaults to ``'/'``. """ domain: str | None = field(default=None) """Domain for which the cookie is valid.""" secure: bool = field(default=False) """Https is required for the cookie.""" httponly: bool = field(default=True) """Forbids javascript to access the cookie via 'Document.cookie'.""" samesite: Literal["lax", "strict", "none"] = field(default="lax") """Controls whether or not a cookie is sent with cross-site requests. Defaults to ``lax``.""" exclude: str | list[str] | None = field(default=None) """A pattern or list of patterns to skip in the session middleware.""" exclude_opt_key: str = field(default="skip_session") """An identifier to use on routes to disable the session middleware for a particular route.""" store: str = "sessions" """Name of the :class:`Store <.stores.base.Store>` to use""" def __post_init__(self) -> None: if len(self.key) < 1 or len(self.key) > 256: raise ImproperlyConfiguredException("key must be a string with a length between 1-256") if self.max_age < 1: raise ImproperlyConfiguredException("max_age must be greater than 0") def get_store_from_app(self, app: Litestar) -> Store: """Get the store defined in :attr:`store` from an :class:`Litestar <.app.Litestar>` instance""" return app.stores.get(self.store) litestar-2.16.0/litestar/openapi/000077500000000000000000000000001500564371300167155ustar00rootroot00000000000000litestar-2.16.0/litestar/openapi/__init__.py000066400000000000000000000002671500564371300210330ustar00rootroot00000000000000from .config import OpenAPIConfig from .controller import OpenAPIController from .datastructures import ResponseSpec __all__ = ("OpenAPIConfig", "OpenAPIController", "ResponseSpec") litestar-2.16.0/litestar/openapi/config.py000066400000000000000000000246051500564371300205430ustar00rootroot00000000000000from __future__ import annotations from copy import deepcopy from dataclasses import dataclass, field, fields from typing import TYPE_CHECKING, Final, Literal, Sequence from litestar._openapi.utils import default_operation_id_creator from litestar.openapi.plugins import ( JsonRenderPlugin, RapidocRenderPlugin, RedocRenderPlugin, StoplightRenderPlugin, SwaggerRenderPlugin, YamlRenderPlugin, ) from litestar.openapi.spec import ( Components, Contact, ExternalDocumentation, Info, License, OpenAPI, PathItem, Reference, SecurityRequirement, Server, Tag, ) from litestar.utils.deprecation import warn_deprecation from litestar.utils.path import normalize_path if TYPE_CHECKING: from litestar.openapi.controller import OpenAPIController from litestar.openapi.plugins import OpenAPIRenderPlugin from litestar.router import Router from litestar.types.callable_types import OperationIDCreator __all__ = ("OpenAPIConfig",) _enabled_plugin_map = { "elements": StoplightRenderPlugin, "openapi.json": JsonRenderPlugin, "openapi.yaml": YamlRenderPlugin, "openapi.yml": YamlRenderPlugin, "rapidoc": RapidocRenderPlugin, "redoc": RedocRenderPlugin, "swagger": SwaggerRenderPlugin, "oauth2-redirect.html": None, } _DEFAULT_SCHEMA_SITE: Final = "redoc" @dataclass class OpenAPIConfig: """Configuration for OpenAPI. To enable OpenAPI schema generation and serving, pass an instance of this class to the :class:`Litestar <.app.Litestar>` constructor using the ``openapi_config`` kwargs. """ title: str """Title of API documentation.""" version: str """API version, e.g. '1.0.0'.""" create_examples: bool = field(default=False) """Generate examples using the polyfactory library.""" random_seed: int = 10 """The random seed used when creating the examples to ensure deterministic generation of examples.""" contact: Contact | None = field(default=None) """API contact information, should be an :class:`Contact ` instance.""" description: str | None = field(default=None) """API description.""" external_docs: ExternalDocumentation | None = field(default=None) """Links to external documentation. Should be an instance of :class:`ExternalDocumentation `. """ license: License | None = field(default=None) """API Licensing information. Should be an instance of :class:`License `. """ security: list[SecurityRequirement] | None = field(default=None) """API Security requirements information. Should be an instance of :data:`SecurityRequirement <.openapi.spec.SecurityRequirement>`. """ components: Components | list[Components] = field(default_factory=Components) """API Components information. Should be an instance of :class:`Components ` or a list thereof. """ servers: list[Server] = field(default_factory=lambda: [Server(url="/")]) """A list of :class:`Server ` instances.""" summary: str | None = field(default=None) """A summary text.""" tags: list[Tag] | None = field(default=None) """A list of :class:`Tag ` instances.""" terms_of_service: str | None = field(default=None) """URL to page that contains terms of service.""" use_handler_docstrings: bool = field(default=False) """Draw operation description from route handler docstring if not otherwise provided.""" webhooks: dict[str, PathItem | Reference] | None = field(default=None) """A mapping of key to either :class:`PathItem ` or. :class:`Reference ` objects. """ operation_id_creator: OperationIDCreator = default_operation_id_creator """A callable that generates unique operation ids""" path: str | None = field(default=None) """Base path for the OpenAPI documentation endpoints. If no path is provided the default is ``/schema``. Ignored if :attr:`openapi_router` is provided. """ render_plugins: Sequence[OpenAPIRenderPlugin] = field(default=()) """Plugins for rendering OpenAPI documentation UIs.""" openapi_router: Router | None = None """An optional router for serving OpenAPI documentation and schema files. If provided, ``path`` is ignored. This parameter is also ignored if the deprecated :attr:`openapi_router <.openapi.OpenAPIConfig.openapi_controller>` kwarg is provided. :attr:`openapi_router` is not required, but it can be passed to customize the configuration of the router used to serve the documentation endpoints. For example, you can add middleware or guards to the router. Handlers to serve the OpenAPI schema and documentation sites are added to this router according to :attr:`render_plugins`, so routes shouldn't be added that conflict with these. """ openapi_controller: type[OpenAPIController] | None = None """Controller for generating OpenAPI routes. Must be subclass of :class:`OpenAPIController `. .. deprecated:: v2.8.0 """ root_schema_site: Literal["redoc", "swagger", "elements", "rapidoc"] | None = None """The static schema generator to use for the "root" path of ``/schema/``. .. deprecated:: v2.8.0 """ enabled_endpoints: set[str] | None = None """A set of the enabled documentation sites and schema download endpoints. .. deprecated:: v2.8.0 """ def __post_init__(self) -> None: self._issue_deprecations() self.root_schema_site = self.root_schema_site or _DEFAULT_SCHEMA_SITE self.enabled_endpoints = ( set(_enabled_plugin_map.keys()) if self.enabled_endpoints is None else self.enabled_endpoints ) if self.path: self.path = normalize_path(self.path) if self.path and self.openapi_controller is not None: self.openapi_controller = type("OpenAPIController", (self.openapi_controller,), {"path": self.path}) self.default_plugin: OpenAPIRenderPlugin | None = None if self.openapi_controller is None: if not self.render_plugins: self._plugin_backward_compatibility() else: # user is implicitly opted into the future plugin-based OpenAPI implementation # behavior by explicitly providing a list of render plugins for plugin in self.render_plugins: if plugin.has_path("/"): self.default_plugin = plugin break else: self.default_plugin = self.render_plugins[0] def _issue_deprecations(self) -> None: """Handle deprecated config options.""" deprecated_in = "v2.8.0" removed_in = "v3.0.0" if self.openapi_controller is not None: warn_deprecation( deprecated_in, "openapi_controller", "attribute", removal_in=removed_in, alternative="render_plugins", ) if self.root_schema_site is not None: warn_deprecation( deprecated_in, "root_schema_site", "attribute", removal_in=removed_in, alternative="render_plugins", info="Any 'render_plugin' with path '/' or first 'render_plugin' in list will be served at the OpenAPI root.", ) if self.enabled_endpoints is not None: warn_deprecation( deprecated_in, "enabled_endpoints", "attribute", removal_in=removed_in, alternative="render_plugins", info="Configure a 'render_plugin' to enable an endpoint.", ) def _plugin_backward_compatibility(self) -> None: """Backward compatibility for the plugin-based OpenAPI implementation. This preserves backward compatibility with the Controller-based OpenAPI implementation. We add a plugin for each enabled endpoint and set the default plugin to the plugin that has a path ending in the value of ``root_schema_site``. """ def is_default_plugin(plugin_: OpenAPIRenderPlugin) -> bool: """Return True if the plugin is the default plugin.""" root_schema_site = self.root_schema_site or _DEFAULT_SCHEMA_SITE return any(path.endswith(root_schema_site) for path in plugin_.paths) self.render_plugins = rps = [] for key in self.enabled_endpoints or (): if plugin_type := _enabled_plugin_map[key]: plugin = plugin_type() rps.append(plugin) if is_default_plugin(plugin): self.default_plugin = plugin def to_openapi_schema(self) -> OpenAPI: """Return an ``OpenAPI`` instance from the values stored in ``self``. Returns: An instance of :class:`OpenAPI `. """ if isinstance(self.components, list): merged_components = Components() for components in self.components: for key in (f.name for f in fields(components)): if value := getattr(components, key, None): merged_value_dict = getattr(merged_components, key, {}) or {} merged_value_dict.update(value) setattr(merged_components, key, merged_value_dict) self.components = merged_components return OpenAPI( external_docs=self.external_docs, security=self.security, components=deepcopy(self.components), # deepcopy prevents mutation of the config's components servers=self.servers, tags=self.tags, webhooks=self.webhooks, info=Info( title=self.title, version=self.version, description=self.description, contact=self.contact, license=self.license, summary=self.summary, terms_of_service=self.terms_of_service, ), paths={}, ) litestar-2.16.0/litestar/openapi/controller.py000066400000000000000000000554721500564371300214670ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING, Any, Callable, Final, Literal from uuid import uuid4 from litestar.constants import OPENAPI_NOT_INITIALIZED from litestar.controller import Controller from litestar.enums import MediaType, OpenAPIMediaType from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers import get from litestar.openapi.config import _DEFAULT_SCHEMA_SITE from litestar.response.base import ASGIResponse from litestar.serialization import encode_json from litestar.serialization.msgspec_hooks import decode_json from litestar.status_codes import HTTP_404_NOT_FOUND if TYPE_CHECKING: from litestar.connection.request import Request from litestar.openapi.spec.open_api import OpenAPI __all__ = ("OpenAPIController",) # NOTE: We are explicitly using a different name to the one defined in litestar.constants so that an openapi # controller can be added to a router on the same application as the openapi router. # See: https://github.com/litestar-org/litestar/issues/3337 OPENAPI_JSON_HANDLER_NAME: Final = f"{uuid4().hex}_litestar_openapi_json" class OpenAPIController(Controller): """Controller for OpenAPI endpoints.""" path: str = "/schema" """Base path for the OpenAPI documentation endpoints.""" style: str = "body { margin: 0; padding: 0 }" """Base styling of the html body.""" redoc_version: str = "next" """Redoc version to download from the CDN.""" swagger_ui_version: str = "5.18.2" """SwaggerUI version to download from the CDN.""" stoplight_elements_version: str = "7.7.18" """StopLight Elements version to download from the CDN.""" rapidoc_version: str = "9.3.4" """RapiDoc version to download from the CDN.""" favicon_url: str = "" """URL to download a favicon from.""" redoc_google_fonts: bool = True """Download google fonts via CDN. Should be set to ``False`` when not using a CDN. """ redoc_js_url: str = f"https://cdn.jsdelivr.net/npm/redoc@{redoc_version}/bundles/redoc.standalone.js" """Download url for the Redoc JS bundle.""" swagger_css_url: str = f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{swagger_ui_version}/swagger-ui.css" """Download url for the Swagger UI CSS bundle.""" swagger_ui_bundle_js_url: str = ( f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{swagger_ui_version}/swagger-ui-bundle.js" ) """Download url for the Swagger UI JS bundle.""" swagger_ui_standalone_preset_js_url: str = ( f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{swagger_ui_version}/swagger-ui-standalone-preset.js" ) """Download url for the Swagger Standalone Preset JS bundle.""" swagger_ui_init_oauth: dict[Any, Any] | bytes = {} """ JSON to initialize Swagger UI OAuth2 by calling the `initOAuth` method. Refer to the following URL for details: `Swagger-UI `_. """ stoplight_elements_css_url: str = ( f"https://unpkg.com/@stoplight/elements@{stoplight_elements_version}/styles.min.css" ) """Download url for the Stoplight Elements CSS bundle.""" stoplight_elements_js_url: str = ( f"https://unpkg.com/@stoplight/elements@{stoplight_elements_version}/web-components.min.js" ) """Download url for the Stoplight Elements JS bundle.""" rapidoc_js_url: str = f"https://unpkg.com/rapidoc@{rapidoc_version}/dist/rapidoc-min.js" """Download url for the RapiDoc JS bundle.""" # internal _dumped_json_schema: str = "" _dumped_yaml_schema: bytes = b"" # until swagger-ui supports v3.1.* of OpenAPI officially, we need to modify the schema for it and keep it # separate from the redoc version of the schema, which is unmodified. dto = None return_dto = None @staticmethod def get_schema_from_request(request: Request[Any, Any, Any]) -> OpenAPI: """Return the OpenAPI pydantic model from the request instance. Args: request: A :class:`Litestar <.connection.Request>` instance. Returns: An :class:`OpenAPI ` instance. """ return request.app.openapi_schema def should_serve_endpoint(self, request: Request[Any, Any, Any]) -> bool: """Verify that the requested path is within the enabled endpoints in the openapi_config. Args: request: To be tested if endpoint enabled. Returns: A boolean. Raises: ImproperlyConfiguredException: If the application ``openapi_config`` attribute is ``None``. """ if not request.app.openapi_config: # pragma: no cover raise ImproperlyConfiguredException("Litestar has not been instantiated with an OpenAPIConfig") asgi_root_path = set(filter(None, request.scope.get("root_path", "").split("/"))) full_request_path = set(filter(None, request.url.path.split("/"))) request_path = full_request_path.difference(asgi_root_path) root_path = set(filter(None, self.path.split("/"))) config = request.app.openapi_config enabled_endpoints = config.enabled_endpoints or set() root_schema_site = config.root_schema_site or _DEFAULT_SCHEMA_SITE if request_path == root_path and root_schema_site in enabled_endpoints: return True return bool(request_path & enabled_endpoints) @property def favicon(self) -> str: """Return favicon ```` tag, if applicable. Returns: A ```` tag if ``self.favicon_url`` is not empty, otherwise returns a placeholder meta tag. """ return f"" if self.favicon_url else "" @cached_property def render_methods_map( self, ) -> dict[Literal["redoc", "swagger", "elements", "rapidoc"], Callable[[Request], bytes]]: """Map render method names to render methods. Returns: A mapping of string keys to render methods. """ return { "redoc": self.render_redoc, "swagger": self.render_swagger_ui, "elements": self.render_stoplight_elements, "rapidoc": self.render_rapidoc, } @get( path=["/openapi.yaml", "openapi.yml"], media_type=OpenAPIMediaType.OPENAPI_YAML, include_in_schema=False, sync_to_thread=False, ) def retrieve_schema_yaml(self, request: Request[Any, Any, Any]) -> ASGIResponse: """Return the OpenAPI schema as YAML with an ``application/vnd.oai.openapi`` Content-Type header. Args: request: A :class:`Request <.connection.Request>` instance. Returns: A Response instance with the YAML object rendered into a string. """ from yaml import dump as dump_yaml if self.should_serve_endpoint(request): if not self._dumped_yaml_schema: schema_json = decode_json(self._get_schema_as_json(request)) schema_yaml = dump_yaml(schema_json, default_flow_style=False) self._dumped_yaml_schema = schema_yaml.encode("utf-8") return ASGIResponse(body=self._dumped_yaml_schema, media_type=OpenAPIMediaType.OPENAPI_YAML) return ASGIResponse(body=b"", status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML) @get( path="/openapi.json", media_type=OpenAPIMediaType.OPENAPI_JSON, include_in_schema=False, sync_to_thread=False, name=OPENAPI_JSON_HANDLER_NAME, ) def retrieve_schema_json(self, request: Request[Any, Any, Any]) -> ASGIResponse: """Return the OpenAPI schema as JSON with an ``application/vnd.oai.openapi+json`` Content-Type header. Args: request: A :class:`Request <.connection.Request>` instance. Returns: A Response instance with the JSON object rendered into a string. """ if self.should_serve_endpoint(request): return ASGIResponse( body=self._get_schema_as_json(request), media_type=OpenAPIMediaType.OPENAPI_JSON, ) return ASGIResponse(body=b"", status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML) @get(path="/", include_in_schema=False, sync_to_thread=False) def root(self, request: Request[Any, Any, Any]) -> ASGIResponse: """Render a static documentation site. The site to be rendered is based on the ``root_schema_site`` value set in the application's :class:`OpenAPIConfig <.openapi.OpenAPIConfig>`. Defaults to ``redoc``. Args: request: A :class:`Request <.connection.Request>` instance. Returns: A response with the rendered site defined in root_schema_site. Raises: ImproperlyConfiguredException: If the application ``openapi_config`` attribute is ``None``. """ config = request.app.openapi_config if not config: # pragma: no cover raise ImproperlyConfiguredException(OPENAPI_NOT_INITIALIZED) render_method = self.render_methods_map[config.root_schema_site or _DEFAULT_SCHEMA_SITE] if self.should_serve_endpoint(request): return ASGIResponse(body=render_method(request), media_type=MediaType.HTML) return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML) @get(path="/swagger", include_in_schema=False, sync_to_thread=False) def swagger_ui(self, request: Request[Any, Any, Any]) -> ASGIResponse: """Route handler responsible for rendering Swagger-UI. Args: request: A :class:`Request <.connection.Request>` instance. Returns: A response with a rendered swagger documentation site """ if self.should_serve_endpoint(request): return ASGIResponse(body=self.render_swagger_ui(request), media_type=MediaType.HTML) return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML) @get(path="/elements", media_type=MediaType.HTML, include_in_schema=False, sync_to_thread=False) def stoplight_elements(self, request: Request[Any, Any, Any]) -> ASGIResponse: """Route handler responsible for rendering StopLight Elements. Args: request: A :class:`Request <.connection.Request>` instance. Returns: A response with a rendered stoplight elements documentation site """ if self.should_serve_endpoint(request): return ASGIResponse(body=self.render_stoplight_elements(request), media_type=MediaType.HTML) return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML) @get(path="/redoc", media_type=MediaType.HTML, include_in_schema=False, sync_to_thread=False) def redoc(self, request: Request[Any, Any, Any]) -> ASGIResponse: # pragma: no cover """Route handler responsible for rendering Redoc. Args: request: A :class:`Request <.connection.Request>` instance. Returns: A response with a rendered redoc documentation site """ if self.should_serve_endpoint(request): return ASGIResponse(body=self.render_redoc(request), media_type=MediaType.HTML) return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML) @get(path="/rapidoc", media_type=MediaType.HTML, include_in_schema=False, sync_to_thread=False) def rapidoc(self, request: Request[Any, Any, Any]) -> ASGIResponse: if self.should_serve_endpoint(request): return ASGIResponse(body=self.render_rapidoc(request), media_type=MediaType.HTML) return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML) @get(path="/oauth2-redirect.html", media_type=MediaType.HTML, include_in_schema=False, sync_to_thread=False) def swagger_ui_oauth2_redirect(self, request: Request[Any, Any, Any]) -> ASGIResponse: # pragma: no cover """Route handler responsible for rendering oauth2-redirect.html page for Swagger-UI. Args: request: A :class:`Request <.connection.Request>` instance. Returns: A response with a rendered oauth2-redirect.html page for Swagger-UI. """ if self.should_serve_endpoint(request): return ASGIResponse(body=self.render_swagger_ui_oauth2_redirect(request), media_type=MediaType.HTML) return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML) def render_swagger_ui_oauth2_redirect(self, request: Request[Any, Any, Any]) -> bytes: """Render an HTML oauth2-redirect.html page for Swagger-UI. Notes: - override this method to customize the template. Args: request: A :class:`Request <.connection.Request>` instance. Returns: A rendered html string. """ return rb""" Swagger UI: OAuth2 Redirect """ def render_swagger_ui(self, request: Request[Any, Any, Any]) -> bytes: """Render an HTML page for Swagger-UI. Notes: - override this method to customize the template. Args: request: A :class:`Request <.connection.Request>` instance. Returns: A rendered html string. """ schema = self.get_schema_from_request(request) head = f""" {schema.info.title} {self.favicon} """ body = f"""
""" return f""" {head} {body} """.encode() def render_stoplight_elements(self, request: Request[Any, Any, Any]) -> bytes: """Render an HTML page for StopLight Elements. Notes: - override this method to customize the template. Args: request: A :class:`Request <.connection.Request>` instance. Returns: A rendered html string. """ schema = self.get_schema_from_request(request) head = f""" {schema.info.title} {self.favicon} """ body = f""" """ return f""" {head} {body} """.encode() def render_rapidoc(self, request: Request[Any, Any, Any]) -> bytes: # pragma: no cover schema = self.get_schema_from_request(request) head = f""" {schema.info.title} {self.favicon} """ body = f""" """ return f""" {head} {body} """.encode() def render_redoc(self, request: Request[Any, Any, Any]) -> bytes: # pragma: no cover """Render an HTML page for Redoc. Notes: - override this method to customize the template. Args: request: A :class:`Request <.connection.Request>` instance. Returns: A rendered html string. """ schema = self.get_schema_from_request(request) head = f""" {schema.info.title} {self.favicon} """ if self.redoc_google_fonts: head += """ """ head += f""" """ body = f"""
""" return f""" {head} {body} """.encode() def render_404_page(self) -> bytes: """Render an HTML 404 page. Returns: A rendered html string. """ return f""" 404 Not found {self.favicon}

Error 404

""".encode() def _get_schema_as_json(self, request: Request) -> str: """Get the schema encoded as a JSON string.""" if not self._dumped_json_schema: schema = self.get_schema_from_request(request).to_schema() json_encoded_schema = encode_json(schema, request.route_handler.default_serializer) self._dumped_json_schema = json_encoded_schema.decode("utf-8") return self._dumped_json_schema litestar-2.16.0/litestar/openapi/datastructures.py000066400000000000000000000015541500564371300223510ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING from litestar.enums import MediaType if TYPE_CHECKING: from litestar.openapi.spec import Example from litestar.types import DataContainerType __all__ = ("ResponseSpec",) @dataclass class ResponseSpec: """Container type of additional responses.""" data_container: DataContainerType | None """A model that describes the content of the response.""" generate_examples: bool = field(default=True) """Generate examples for the response content.""" description: str = field(default="Additional response") """A description of the response.""" media_type: MediaType | str = field(default=MediaType.JSON) """Response media type.""" examples: list[Example] | None = field(default=None) """A list of Example models.""" litestar-2.16.0/litestar/openapi/plugins.py000066400000000000000000000621311500564371300207530ustar00rootroot00000000000000from __future__ import annotations from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Sequence import msgspec import yaml from litestar.constants import OPENAPI_JSON_HANDLER_NAME from litestar.enums import MediaType, OpenAPIMediaType from litestar.handlers import get from litestar.serialization import encode_json, get_serializer if TYPE_CHECKING: from litestar.config.csrf import CSRFConfig from litestar.connection import Request from litestar.router import Router __all__ = ( "OpenAPIRenderPlugin", "RapidocRenderPlugin", "RedocRenderPlugin", "ScalarRenderPlugin", "StoplightRenderPlugin", "SwaggerRenderPlugin", "YamlRenderPlugin", ) _favicon_url = "https://cdn.jsdelivr.net/gh/litestar-org/branding@main/assets/Branding%20-%20PNG%20-%20Transparent/Badge%20-%20Blue%20and%20Yellow.png" _default_favicon = f"" _default_style = "" def _get_cookie_value_or_undefined(cookie_name: str) -> str: """Javascript code as a string to get the value of a cookie by name or undefined.""" return f"document.cookie.split('; ').find((row) => row.startsWith('{cookie_name}='))?.split('=')[1];" class OpenAPIRenderPlugin(ABC): """Base class for OpenAPI UI render plugins.""" paths: list[str] def __init__( self, *, path: str | Sequence[str], media_type: MediaType | OpenAPIMediaType = MediaType.HTML, favicon: str = _default_favicon, style: str = _default_style, ) -> None: """Initialize the OpenAPI UI render plugin. Args: path: Path to serve the OpenAPI UI at. media_type: Media type for the handler. favicon: Html tag for the favicon. style: Base styling of the html body. """ self.paths = [path] if isinstance(path, str) else list(path) self.media_type = media_type self.favicon = favicon self.style = style @staticmethod def render_json(request: Request, openapi_schema: dict[str, Any]) -> bytes: """Render the OpenAPI schema as JSON. Args: request: The request that triggered the render. openapi_schema: The OpenAPI schema as a dictionary. Returns: The rendered JSON. """ return encode_json(openapi_schema, serializer=get_serializer(request.route_handler.resolve_type_encoders())) @abstractmethod def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes: """Render the OpenAPI UI. Args: request: The request that triggered the render. openapi_schema: The OpenAPI schema as a dictionary. Returns: The rendered HTML. """ raise NotImplementedError @staticmethod def get_openapi_json_route(request: Request) -> str: """Get the route for the OpenAPI JSON schema. Returns: The route for the OpenAPI JSON schema. """ return request.app.route_reverse(OPENAPI_JSON_HANDLER_NAME) def receive_router(self, router: Router) -> None: """Receive the router that serves the OpenAPI UI. Can be used by plugins to additionally configure the router, e.g. to add additional routes. Args: router: The router that serves the OpenAPI UI. """ return def has_path(self, path: str) -> bool: """Check if the plugin has a path. Args: path: The path to check. Returns: True if the plugin has the path, False otherwise. """ return path in self.paths class JsonRenderPlugin(OpenAPIRenderPlugin): """Render the OpenAPI schema as JSON.""" def __init__( self, *, path: str | Sequence[str] = "/openapi.json", media_type: MediaType | OpenAPIMediaType = OpenAPIMediaType.OPENAPI_JSON, **kwargs: Any, ) -> None: """Initialize the OpenAPI UI render plugin. Args: path: Path to serve the OpenAPI UI at. media_type: Media type for the handler. **kwargs: Additional arguments to pass to the base class. """ super().__init__(path=path, media_type=media_type, **kwargs) def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes: """Render an OpenAPI schema as JSON. Args: request: The request. openapi_schema: The OpenAPI schema as a dictionary. Returns: The rendered OpenAPI schema as JSON. """ return self.render_json(request, openapi_schema) class YamlRenderPlugin(OpenAPIRenderPlugin): """Render an OpenAPI schema as YAML.""" def __init__( self, *, path: str | Sequence[str] = ("/openapi.yaml", "/openapi.yml"), media_type: MediaType | OpenAPIMediaType = OpenAPIMediaType.OPENAPI_YAML, **kwargs: Any, ) -> None: """Initialize the OpenAPI UI render plugin. Args: path: Path to serve the OpenAPI UI at. media_type: Media type for the handler. **kwargs: Additional arguments to pass to the base class. """ super().__init__(path=path, media_type=media_type, **kwargs) def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes: """Render an OpenAPI schema as YAML. Args: request: The request. openapi_schema: The OpenAPI schema as a dictionary. Returns: The rendered OpenAPI schema as YAML. """ # using msgspec.to_builtins() ensures that any examples generated by polyfactory that have the # UNSET value (possible if the examples are being generated for a partial DTO model which makes # every type a union with UNSET) are stripped out. openapi_schema = msgspec.to_builtins( openapi_schema, enc_hook=get_serializer(request.route_handler.resolve_type_encoders()) ) return yaml.dump(openapi_schema, default_flow_style=False).encode("utf-8") class RapidocRenderPlugin(OpenAPIRenderPlugin): """Render an OpenAPI schema using Rapidoc.""" def __init__( self, *, version: str = "9.3.4", js_url: str | None = None, path: str | Sequence[str] = "/rapidoc", **kwargs: Any, ) -> None: """Initialize the OpenAPI UI render plugin. Args: version: Rapidoc version to download from the CDN. If js_url is provided, this is ignored. js_url: Download url for the RapiDoc JS bundle. If not provided, the version will be used to construct the url. path: Path to serve the OpenAPI UI at. **kwargs: Additional arguments to pass to the base class. """ self.js_url = js_url or f"https://unpkg.com/rapidoc@{version}/dist/rapidoc-min.js" super().__init__(path=path, **kwargs) def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes: """Render an HTML page for Rapidoc. .. note:: Override this method to customize the template. Args: request: The request. openapi_schema: The OpenAPI schema as a dictionary. Returns: A rendered html string. """ def create_request_interceptor(csrf_config: CSRFConfig) -> str: if csrf_config.cookie_httponly: return "" return f""" """ head = f""" {openapi_schema["info"]["title"]} {self.favicon} {self.style} """ body = f""" {create_request_interceptor(request.app.csrf_config) if request.app.csrf_config else ""} """ return f""" {head} {body} """.encode() class RedocRenderPlugin(OpenAPIRenderPlugin): """Render an OpenAPI schema using Redoc.""" def __init__( self, *, version: str = "next", js_url: str | None = None, google_fonts: bool = True, path: str | Sequence[str] = "/redoc", **kwargs: Any, ) -> None: """Initialize the OpenAPI UI render plugin. Args: version: Redoc version to download from the CDN. If js_url is provided, this is ignored. js_url: Download url for the Redoc JS bundle. If not provided, the version will be used to construct the url. google_fonts: Download google fonts via CDN. Should be set to False when not using a CDN. path: Path to serve the OpenAPI UI at. **kwargs: Additional arguments to pass to the base class. """ self.js_url = js_url or f"https://cdn.jsdelivr.net/npm/redoc@{version}/bundles/redoc.standalone.js" self.google_fonts = google_fonts super().__init__(path=path, **kwargs) def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes: """Render an HTML page for Redoc. .. note:: override this method to customize the template. Args: request: The request. openapi_schema: The OpenAPI schema as a dictionary. Returns: A rendered html string. """ head = f""" {openapi_schema["info"]["title"]} {self.favicon} """ if self.google_fonts: head += """ """ head += f""" {self.style} """ body = b"".join( [ b"
", ] ) return b"".join( [ b"", head.encode(), body, b"", ] ) class ScalarRenderPlugin(OpenAPIRenderPlugin): """Plugin to render an OpenAPI schema using Scalar. .. versionadded:: 2.8.0 """ _default_css_url = "https://cdn.jsdelivr.net/gh/litestar-org/branding@main/assets/openapi/scalar.css" def __init__( self, *, version: str = "latest", js_url: str | None = None, css_url: str | None = None, path: str | Sequence[str] = "/scalar", options: dict[str, Any] | None = None, **kwargs: Any, ) -> None: """Initialize the Scalar OpenAPI UI render plugin. Args: version: Scalar version to download from the CDN. If js_url is provided, this is ignored. js_url: Download url for the Scalar JS bundle. If not provided, the version will be used to construct the url. css_url: Download url for the Scalar CSS bundle. If not provided, the Litestar-provided CSS will be used. path: Path to serve the OpenAPI UI at. options: Scalar configuration options. If not provided the default Scalar configuration will be used. **kwargs: Additional arguments to pass to the base class. """ self.js_url = js_url or f"https://cdn.jsdelivr.net/npm/@scalar/api-reference@{version}" self.css_url = css_url or self._default_css_url self.options = options super().__init__(path=path, **kwargs) def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes: """Render an HTMl page for Scalar. .. note:: Override this method to customize the template. Args: request: The request. openapi_schema: The OpenAPI schema as a dictionary. Returns: A rendered html string. """ head = f""" {openapi_schema["info"]["title"]} {self.style} {self.favicon} """ body = f""" {self.render_options()} """ return f""" {head} {body} """.encode() def render_options(self) -> str: """Render options to Scalar configuration.""" if not self.options: return "" return f""" """ class StoplightRenderPlugin(OpenAPIRenderPlugin): """Render an OpenAPI schema using StopLight Elements.""" def __init__( self, *, version: str = "7.7.18", js_url: str | None = None, css_url: str | None = None, path: str | Sequence[str] = "/elements", **kwargs: Any, ) -> None: """Initialize the OpenAPI UI render plugin. Args: version: StopLight Elements version to download from the CDN. If js_url is provided, this is ignored. js_url: Download url for the StopLight Elements JS bundle. If not provided, the version will be used to construct the url. css_url: Download url for the StopLight Elements CSS bundle. If not provided, the version will be used to construct the url. path: Path to serve the OpenAPI UI at. **kwargs: Additional arguments to pass to the base class. """ self.js_url = js_url or f"https://unpkg.com/@stoplight/elements@{version}/web-components.min.js" self.css_url = css_url or f"https://unpkg.com/@stoplight/elements@{version}/styles.min.css" super().__init__(path=path, **kwargs) def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes: """Render an HTML page for StopLight Elements. .. note:: Override this method to customize the template. Args: request: The request. openapi_schema: The OpenAPI schema as a dictionary. Returns: A rendered html string. """ head = f""" {openapi_schema["info"]["title"]} {self.favicon} {self.style} """ body = f""" """ return f""" {head} {body} """.encode() class SwaggerRenderPlugin(OpenAPIRenderPlugin): """Render an OpenAPI schema using Swagger-UI.""" def __init__( self, version: str = "5.18.2", js_url: str | None = None, css_url: str | None = None, standalone_preset_js_url: str | None = None, init_oauth: dict[str, Any] | bytes | None = None, path: str | Sequence[str] = "/swagger", **kwargs: Any, ) -> None: """Initialize the OpenAPI UI render plugin. Args: version: SwaggerUI version to download from the CDN. If js_url is provided, this is ignored. js_url: Download url for the Swagger UI JS bundle. If not provided, the version will be used to construct the url. css_url: Download url for the Swagger UI CSS bundle. If not provided, the version will be used to construct the url. standalone_preset_js_url: Download url for the Swagger Standalone Preset JS bundle. If not provided, the version will be used to construct the url. init_oauth: JSON to initialize Swagger UI OAuth2 by calling the ``initOAuth`` method. Refer to the following URL for details: `Swagger-UI `_. path: Path to serve the OpenAPI UI at. **kwargs: Additional arguments to pass to the base class. """ self.js_url = js_url or f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{version}/swagger-ui-bundle.js" self.css_url = css_url or f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{version}/swagger-ui.css" self.standalone_preset_js_url = ( standalone_preset_js_url or f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{version}/swagger-ui-standalone-preset.js" ) self.init_oauth = init_oauth or {} super().__init__(path=path, **kwargs) def render(self, request: Request, openapi_schema: dict[str, Any]) -> bytes: """Render an HTML page for Swagger-UI. Notes: - override this method to customize the template. Args: request: The request. openapi_schema: The OpenAPI schema as a dictionary. Returns: A rendered html string. """ def create_request_interceptor(csrf_config: CSRFConfig) -> bytes: if csrf_config.cookie_httponly: return b"" return f""" requestInterceptor: (request) => {{ const csrf_token = {_get_cookie_value_or_undefined(csrf_config.cookie_name)}; if (csrf_token !== undefined) {{ request.headers['{csrf_config.header_name}'] = csrf_token; }} return request; }},""".encode() head = f""" {openapi_schema["info"]["title"]} {self.favicon} {self.style} """ body = b"".join( [ b"""
""", ] ) return b"".join([b"", head.encode(), body, b""]) def receive_router(self, router: Router) -> None: """Receive the router that serves the OpenAPI UI. Adds a route to serve the OAuth2 redirect page. Args: router: The router that serves the OpenAPI UI. """ router.register( get("/oauth2-redirect.html", media_type=MediaType.HTML, sync_to_thread=False)(self.render_oauth2_redirect), ) @staticmethod def render_oauth2_redirect() -> bytes: """Render an HTML oauth2-redirect.html page for Swagger-UI. .. note:: Override this method to customize the template. Returns: A rendered html string. """ return rb""" Swagger UI: OAuth2 Redirect """ litestar-2.16.0/litestar/openapi/spec/000077500000000000000000000000001500564371300176475ustar00rootroot00000000000000litestar-2.16.0/litestar/openapi/spec/__init__.py000066400000000000000000000032331500564371300217610ustar00rootroot00000000000000from .base import BaseSchemaObject from .callback import Callback from .components import Components from .contact import Contact from .discriminator import Discriminator from .encoding import Encoding from .enums import OpenAPIFormat, OpenAPIType from .example import Example from .external_documentation import ExternalDocumentation from .header import OpenAPIHeader from .info import Info from .license import License from .link import Link from .media_type import OpenAPIMediaType from .oauth_flow import OAuthFlow from .oauth_flows import OAuthFlows from .open_api import OpenAPI from .operation import Operation from .parameter import Parameter from .path_item import PathItem from .paths import Paths from .reference import Reference from .request_body import RequestBody from .response import OpenAPIResponse from .responses import Responses from .schema import Schema from .security_requirement import SecurityRequirement from .security_scheme import SecurityScheme from .server import Server from .server_variable import ServerVariable from .tag import Tag from .xml import XML __all__ = ( "XML", "BaseSchemaObject", "Callback", "Components", "Contact", "Discriminator", "Encoding", "Example", "ExternalDocumentation", "Info", "License", "Link", "OAuthFlow", "OAuthFlows", "OpenAPI", "OpenAPIFormat", "OpenAPIHeader", "OpenAPIMediaType", "OpenAPIResponse", "OpenAPIType", "Operation", "Parameter", "PathItem", "Paths", "Reference", "RequestBody", "Responses", "Schema", "SecurityRequirement", "SecurityScheme", "Server", "ServerVariable", "Tag", ) litestar-2.16.0/litestar/openapi/spec/base.py000066400000000000000000000044501500564371300211360ustar00rootroot00000000000000from __future__ import annotations from dataclasses import asdict, dataclass, fields, is_dataclass from enum import Enum from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from collections.abc import Iterator from dataclasses import Field __all__ = ("BaseSchemaObject",) def _normalize_key(key: str) -> str: if key.endswith("_in"): return "in" if key.startswith("schema_"): return key.split("_")[1] if "_" in key: components = key.split("_") return components[0] + "".join(component.title() for component in components[1:]) return "$ref" if key == "ref" else key def _normalize_value(value: Any) -> Any: if isinstance(value, BaseSchemaObject): return value.to_schema() if is_dataclass(value): return { _normalize_value(k): _normalize_value(v) for k, v in asdict(value).items() # type: ignore[call-overload] if v is not None } if isinstance(value, dict): return {_normalize_value(k): _normalize_value(v) for k, v in value.items() if v is not None} if isinstance(value, list): return [_normalize_value(v) for v in value] return value.value if isinstance(value, Enum) else value @dataclass class BaseSchemaObject: """Base class for schema spec objects""" @property def _exclude_fields(self) -> set[str]: return set() def _iter_fields(self) -> Iterator[Field[Any]]: yield from fields(self) def to_schema(self) -> dict[str, Any]: """Transform the spec dataclass object into a string keyed dictionary. This method traverses all nested values recursively. """ result: dict[str, Any] = {} exclude = self._exclude_fields for field in self._iter_fields(): if field.name in exclude: continue value = _normalize_value(getattr(self, field.name, None)) if value is not None: if "alias" in field.metadata: if not isinstance(field.metadata["alias"], str): raise TypeError('metadata["alias"] must be a str') key = field.metadata["alias"] else: key = _normalize_key(field.name) result[key] = value return result litestar-2.16.0/litestar/openapi/spec/callback.py000066400000000000000000000017011500564371300217540ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Dict, Union if TYPE_CHECKING: from litestar.openapi.spec.path_item import PathItem from litestar.openapi.spec.reference import Reference Callback = Dict[str, Union["PathItem", "Reference"]] """A map of possible out-of band callbacks related to the parent operation. Each value in the map is a `Path Item Object `_ that describes a set of requests that may be initiated by the API provider and the expected responses. The key value used to identify the path item object is an expression, evaluated at runtime, that identifies a URL to use for the callback operation. Patterned Fields {expression}: 'PathItem' = ... A Path Item Object used to define a callback request and expected responses. A `complete example `_ is available. """ litestar-2.16.0/litestar/openapi/spec/components.py000066400000000000000000000056171500564371300224170ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING from litestar.openapi.spec.base import BaseSchemaObject __all__ = ("Components",) if TYPE_CHECKING: from litestar.openapi.spec.callback import Callback from litestar.openapi.spec.example import Example from litestar.openapi.spec.header import OpenAPIHeader from litestar.openapi.spec.link import Link from litestar.openapi.spec.parameter import Parameter from litestar.openapi.spec.path_item import PathItem from litestar.openapi.spec.reference import Reference from litestar.openapi.spec.request_body import RequestBody from litestar.openapi.spec.response import OpenAPIResponse from litestar.openapi.spec.schema import Schema from litestar.openapi.spec.security_scheme import SecurityScheme @dataclass class Components(BaseSchemaObject): """Holds a set of reusable objects for different aspects of the OAS. All objects defined within the components object will have no effect on the API unless they are explicitly referenced from properties outside the components object. """ schemas: dict[str, Schema] = field(default_factory=dict) """An object to hold reusable `Schema Objects `_""" responses: dict[str, OpenAPIResponse | Reference] | None = None """An object to hold reusable `Response Objects `_""" parameters: dict[str, Parameter | Reference] | None = None """An object to hold reusable `Parameter Objects `_""" examples: dict[str, Example | Reference] | None = None """An object to hold reusable `Example Objects `_""" request_bodies: dict[str, RequestBody | Reference] | None = None """An object to hold reusable `Request Body Objects `_""" headers: dict[str, OpenAPIHeader | Reference] | None = None """An object to hold reusable `Header Objects `_""" security_schemes: dict[str, SecurityScheme | Reference] | None = None """An object to hold reusable `Security Scheme Objects `_""" links: dict[str, Link | Reference] | None = None """An object to hold reusable `Link Objects `_""" callbacks: dict[str, Callback | Reference] | None = None """An object to hold reusable `Callback Objects `_""" path_items: dict[str, PathItem | Reference] | None = None """An object to hold reusable `Path Item Object `_""" litestar-2.16.0/litestar/openapi/spec/contact.py000066400000000000000000000011201500564371300216460ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from litestar.openapi.spec.base import BaseSchemaObject __all__ = ("Contact",) @dataclass class Contact(BaseSchemaObject): """Contact information for the exposed API.""" name: str | None = None """The identifying name of the contact person/organization.""" url: str | None = None """The URL pointing to the contact information. MUST be in the form of a URL.""" email: str | None = None """The email address of the contact person/organization. MUST be in the form of an email address.""" litestar-2.16.0/litestar/openapi/spec/discriminator.py000066400000000000000000000016661500564371300231010ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from litestar.openapi.spec.base import BaseSchemaObject __all__ = ("Discriminator",) @dataclass(unsafe_hash=True) class Discriminator(BaseSchemaObject): """When request bodies or response payloads may be one of a number of different schemas, a ``discriminator`` object can be used to aid in serialization, deserialization, and validation. The discriminator is a specific object in a schema which is used to inform the consumer of the specification of an alternative schema based on the value associated with it. When using the discriminator, _inline_ schemas will not be considered. """ property_name: str """**REQUIRED**. The name of the property in the payload that will hold the discriminator value.""" mapping: dict[str, str] | None = None """An object to hold mappings between payload values and schema names or references.""" litestar-2.16.0/litestar/openapi/spec/encoding.py000066400000000000000000000063321500564371300220130ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from litestar.openapi.spec.base import BaseSchemaObject if TYPE_CHECKING: from litestar.openapi.spec.header import OpenAPIHeader from litestar.openapi.spec.reference import Reference __all__ = ("Encoding",) @dataclass class Encoding(BaseSchemaObject): """A single encoding definition applied to a single schema property.""" content_type: str | None = None """The Content-Type for encoding a specific property. Default value depends n the property type: - for ``object``: ``application/json`` - for ``array``: the default is defined based on the inner type - for all other cases the default is ``application/octet-stream``. The value can be a specific media type (e.g. ``application/json``), a wildcard media type (e.g. ``image/*``), or a comma-separated list of the two types. """ headers: dict[str, OpenAPIHeader | Reference] | None = None """A map allowing additional information to be provided as headers, for example ``Content-Disposition``. ``Content-Type`` is described separately and SHALL be ignored in this section. This property SHALL be ignored if the request body media type is not a ``multipart``. """ style: str | None = None """Describes how a specific property value will be serialized depending on its type. See `Parameter Object `_ for details on the `style `__ property. The behavior follows the same values as ``query`` parameters, including default values. This property SHALL be ignored if the request body media type is not ``application/x-www-form-urlencoded`` or ``multipart/form-data``. If a value is explicitly defined, then the value of `contentType `_ (implicit or explicit) SHALL be ignored. """ explode: bool = False """When this is true, property values of type ``array`` or ``object`` generate separate parameters for each value of the array, or key-value-pair of the map. For other types of properties this property has no effect. When `style `_ is ``form``, the default value is ``True``. For all other styles, the default value is ``False``. This property SHALL be ignored if the request body media type is not ``application/x-www-form-urlencoded`` or ``multipart/form-data``. If a value is explicitly defined, then the value of `contentType `_ (implicit or explicit) SHALL be ignored. """ allow_reserved: bool = False """Determines whether the parameter value SHOULD allow reserved characters, as defined by :rfc:`3986` (``:/?#[]@!$&'()*+,;=``) to be included without percent-encoding. This property SHALL be ignored if the request body media type s not ``application/x-www-form-urlencoded`` or ``multipart/form-data``. If a value is explicitly defined, then the value of `contentType `_ (implicit or explicit) SHALL be ignored. """ litestar-2.16.0/litestar/openapi/spec/enums.py000066400000000000000000000017441500564371300213560ustar00rootroot00000000000000from enum import Enum __all__ = ("OpenAPIFormat", "OpenAPIType") class OpenAPIFormat(str, Enum): """Formats extracted from: https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00#page-13""" DATE = "date" DATE_TIME = "date-time" TIME = "time" DURATION = "duration" URL = "url" EMAIL = "email" IDN_EMAIL = "idn-email" HOST_NAME = "hostname" IDN_HOST_NAME = "idn-hostname" IPV4 = "ipv4" IPV6 = "ipv6" URI = "uri" URI_REFERENCE = "uri-reference" URI_TEMPLATE = "uri-template" JSON_POINTER = "json-pointer" RELATIVE_JSON_POINTER = "relative-json-pointer" IRI = "iri-reference" IRI_REFERENCE = "iri-reference" # noqa: PIE796 UUID = "uuid" REGEX = "regex" BINARY = "binary" class OpenAPIType(str, Enum): """An OopenAPI type.""" ARRAY = "array" BOOLEAN = "boolean" INTEGER = "integer" NULL = "null" NUMBER = "number" OBJECT = "object" STRING = "string" litestar-2.16.0/litestar/openapi/spec/example.py000066400000000000000000000024341500564371300216570ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import Any from litestar.openapi.spec.base import BaseSchemaObject @dataclass class Example(BaseSchemaObject): id: str | None = None """Optional ID for the example. When provided, this will be used as the key in the OpenAPI specification.""" summary: str | None = None """Short description for the example.""" description: str | None = None """Long description for the example. `CommonMark syntax `_ MAY be used for rich text representation. """ value: Any | None = None """Embedded literal example. The ``value`` field and ``externalValue`` field are mutually exclusive. To represent examples of media types that cannot naturally represented in JSON or YAML, use a string value to contain the example, escaping where necessary. """ external_value: str | None = None """A URL that points to the literal example. This provides the capability to reference examples that cannot easily be included in JSON or YAML documents. The ``value`` field and ``externalValue`` field are mutually exclusive. See the rules for resolving `Relative References `_. """ litestar-2.16.0/litestar/openapi/spec/external_documentation.py000066400000000000000000000011461500564371300247760ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from litestar.openapi.spec.base import BaseSchemaObject __all__ = ("ExternalDocumentation",) @dataclass class ExternalDocumentation(BaseSchemaObject): """Allows referencing an external resource for extended documentation.""" url: str """**REQUIRED**. The URL for the target documentation. Value MUST be in the form of a URL.""" description: str | None = None """A short description of the target documentation. `CommonMark syntax `_ MAY be used for rich text representation. """ litestar-2.16.0/litestar/openapi/spec/header.py000066400000000000000000000150501500564371300214520ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Literal from litestar.openapi.spec.base import BaseSchemaObject from litestar.utils import warn_deprecation if TYPE_CHECKING: from litestar.openapi.spec.example import Example from litestar.openapi.spec.media_type import OpenAPIMediaType from litestar.openapi.spec.reference import Reference from litestar.openapi.spec.schema import Schema __all__ = ("OpenAPIHeader",) @dataclass class OpenAPIHeader(BaseSchemaObject): """The Header Object follows the structure of the [Parameter Object](https://spec.openapis.org/oas/v3.1.0#parameterObject) with the following changes: 1. ``name`` MUST NOT be specified, it is given in the corresponding ``headers`` map. 2. ``in`` MUST NOT be specified, it is implicitly in ``header``. 3. All traits that are affected by the location MUST be applicable to a location of ``header`` (for example, `style `__). """ schema: Schema | Reference | None = None """The schema defining the type used for the parameter.""" name: Literal[""] = "" """MUST NOT be specified, it is given in the corresponding ``headers`` map.""" param_in: Literal["header"] = "header" """MUST NOT be specified, it is implicitly in ``header``.""" description: str | None = None """A brief description of the parameter. This could contain examples of use. `CommonMark syntax `_ MAY be used for rich text representation. """ required: bool = False """Determines whether this parameter is mandatory. If the `parameter location `_ is ``"path"``, this property is **REQUIRED** and its value MUST be ``True``. Otherwise, the property MAY be included and its default value is ``False``. """ deprecated: bool = False """Specifies that a parameter is deprecated and SHOULD be transitioned out of usage. Default value is ``False``.""" allow_empty_value: bool = None # type: ignore[assignment] """Sets the ability to pass empty-valued parameters. This is valid only for ``query`` parameters and allows sending a parameter with an empty value. Default value is ``False``. If `style `__ is used, and if behavior is ``n/a`` (cannot be serialized), the value of ``allowEmptyValue`` SHALL be ignored. Use of this property is NOT RECOMMENDED, as it is likely to be removed in a later revision. The rules for serialization of the parameter are specified in one of two ways.For simpler scenarios, a `schema `_ and `style `__ can describe the structure and syntax of the parameter. """ style: str | None = None """Describes how the parameter value will be serialized depending on the type of the parameter value. Default values (based on value of ``in``): - for ``query`` - ``form``; - for ``path`` - ``simple``; - for ``header`` - ``simple``; - for ``cookie`` - ``form``. """ explode: bool | None = None """When this is true, parameter values of type ``array`` or ``object`` generate separate parameters for each value of the array or key-value pair of the map. For other types of parameters this property has no effect.When `style `__ is ``form``, the default value is ``True``. For all other styles, the default value is ``False``. """ allow_reserved: bool = None # type: ignore[assignment] """Determines whether the parameter value SHOULD allow reserved characters, as defined by. :rfc:`3986` (``:/?#[]@!$&'()*+,;=``) to be included without percent-encoding. This property only applies to parameters with an ``in`` value of ``query``. The default value is ``False``. """ example: Any | None = None """Example of the parameter's potential value. The example SHOULD match the specified schema and encoding properties if present. The ``example`` field is mutually exclusive of the ``examples`` field. Furthermore, if referencing a ``schema`` that contains an example, the ``example`` value SHALL _override_ the example provided by the schema. To represent examples of media types that cannot naturally be represented in JSON or YAML, a string value can contain the example with escaping where necessary. """ examples: dict[str, Example | Reference] | None = None """Examples of the parameter's potential value. Each example SHOULD contain a value in the correct format as specified in the parameter encoding. The ``examples`` field is mutually exclusive of the ``example`` field. Furthermore, if referencing a ``schema`` that contains an example, the ``examples`` value SHALL _override_ the example provided by the schema. For more complex scenarios, the `content `_ property can define the media type and schema of the parameter. A parameter MUST contain either a ``schema`` property, or a ``content`` property, but not both. When ``example`` or ``examples`` are provided in conjunction with the ``schema`` object, the example MUST follow the prescribed serialization strategy for the parameter. """ content: dict[str, OpenAPIMediaType] | None = None """A map containing the representations for the parameter. The key is the media type and the value describes it. The map MUST only contain one entry. """ @property def _exclude_fields(self) -> set[str]: return {"name", "param_in", "allow_reserved", "allow_empty_value"} def __post_init__(self) -> None: if self.allow_reserved is None: self.allow_reserved = False # type: ignore[unreachable] else: warn_deprecation( "2.13.1", "allow_reserved", kind="parameter", removal_in="4", info="This property is invalid for headers and will be ignored", ) if self.allow_empty_value is None: self.allow_empty_value = False # type: ignore[unreachable] else: warn_deprecation( "2.13.1", "allow_empty_value", kind="parameter", removal_in="4", info="This property is invalid for headers and will be ignored", ) litestar-2.16.0/litestar/openapi/spec/info.py000066400000000000000000000026051500564371300211570ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from litestar.openapi.spec.base import BaseSchemaObject if TYPE_CHECKING: from litestar.openapi.spec.contact import Contact from litestar.openapi.spec.license import License __all__ = ("Info",) @dataclass class Info(BaseSchemaObject): """The object provides metadata about the API. The metadata MAY be used by the clients if needed, and MAY be presented in editing or documentation generation tools for convenience. """ title: str """ **REQUIRED**. The title of the API. """ version: str """ **REQUIRED**. The version of the OpenAPI document which is distinct from the `OpenAPI Specification version `_ or the API implementation version """ summary: str | None = None """A short summary of the API.""" description: str | None = None """A description of the API. `CommonMark syntax `_ MAY be used for rich text representation. """ terms_of_service: str | None = None """A URL to the Terms of Service for the API. MUST be in the form of a URL.""" contact: Contact | None = None """The contact information for the exposed API.""" license: License | None = None """The license information for the exposed API.""" litestar-2.16.0/litestar/openapi/spec/license.py000066400000000000000000000014121500564371300216410ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from litestar.openapi.spec.base import BaseSchemaObject __all__ = ("License",) @dataclass class License(BaseSchemaObject): """License information for the exposed API.""" name: str """**REQUIRED**. The license name used for the API.""" identifier: str | None = None """An `SPDX `_ license expression for the API. The ``identifier`` field is mutually exclusive of the ``url`` field. """ url: str | None = None """A URL to the license used for the API. This MUST be in the form of a URL. The ``url`` field is mutually exclusive of the ``identifier`` field. """ litestar-2.16.0/litestar/openapi/spec/link.py000066400000000000000000000054741500564371300211700ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Any from litestar.openapi.spec.base import BaseSchemaObject if TYPE_CHECKING: from litestar.openapi.spec.server import Server __all__ = ("Link",) @dataclass class Link(BaseSchemaObject): """The ``Link object`` represents a possible design-time link for a response. The presence of a link does not guarantee the caller's ability to successfully invoke it, rather it provides a known relationship and traversal mechanism between responses and other operations. Unlike _dynamic_ links (i.e. links provided **in** the response payload), the OAS linking mechanism does not require link information in the runtime response. For computing links, and providing instructions to execute them, a `runtime expression `_ is used for accessing values in an operation and using them as parameters while invoking the linked operation. """ operation_ref: str | None = None """A relative or absolute URI reference to an OAS operation. This field is mutually exclusive of the ``operationId`` field, and MUST point to an `Operation Object `_. Relative ``operationRef`` values MAY be used to locate an existing `Operation Object `_ in the OpenAPI definition. See the rules for resolving `Relative References `_ """ operation_id: str | None = None """The name of an _existing_, resolvable OAS operation, as defined with a unique ``operationId``. This field is mutually exclusive of the ``operationRef`` field. """ parameters: dict[str, Any] | None = None """A map representing parameters to pass to an operation as specified with ``operationId`` or identified via ``operationRef``. The key is the parameter name to be used, whereas the value can be a constant or an expression to be evaluated and passed to the linked operation. The parameter name can be qualified using the `parameter location `_ ``[{in}.]{name}`` for operations that use the same parameter name in different locations (e.g. path.id). """ request_body: Any | None = None """A literal value or `{expression} `_ to use as a request body when calling the target operation.""" description: str | None = None """A description of the link. `CommonMark syntax `_ MAY be used for rich text representation. """ server: Server | None = None """A server object to be used by the target operation.""" litestar-2.16.0/litestar/openapi/spec/media_type.py000066400000000000000000000035321500564371300223440ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Any from litestar.openapi.spec.base import BaseSchemaObject if TYPE_CHECKING: from litestar.openapi.spec.encoding import Encoding from litestar.openapi.spec.example import Example from litestar.openapi.spec.reference import Reference from litestar.openapi.spec.schema import Schema __all__ = ("OpenAPIMediaType",) @dataclass class OpenAPIMediaType(BaseSchemaObject): """Each Media Type Object provides schema and examples for the media type identified by its key.""" schema: Reference | Schema | None = None """The schema defining the content of the request, response, or parameter.""" example: Any | None = None """Example of the media type. The example object SHOULD be in the correct format as specified by the media type. The ``example`` field is mutually exclusive of the ``examples`` field. Furthermore, if referencing a ``schema`` which contains an example, the ``example`` value SHALL _override_ the example provided by the schema. """ examples: dict[str, Example | Reference] | None = None """Examples of the media type. Each example object SHOULD match the media type and specified schema if present. The ``examples`` field is mutually exclusive of the ``example`` field. Furthermore, if referencing a ``schema`` which contains an example, the ``examples`` value SHALL _override_ the example provided by the schema. """ encoding: dict[str, Encoding] | None = None """A map between a property name and its encoding information. The key, being the property name, MUST exist in the schema as a property. The encoding object SHALL only apply to ``requestBody`` objects when the media type is ``multipart`` or ``application/x-www-form-urlencoded``. """ litestar-2.16.0/litestar/openapi/spec/oauth_flow.py000066400000000000000000000022531500564371300223720ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from litestar.openapi.spec.base import BaseSchemaObject __all__ = ("OAuthFlow",) @dataclass class OAuthFlow(BaseSchemaObject): """Configuration details for a supported OAuth Flow.""" authorization_url: str | None = None """ **REQUIRED** for ``oauth2`` ("implicit", "authorizationCode"). The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. """ token_url: str | None = None """ **REQUIRED** for ``oauth2`` ("password", "clientCredentials", "authorizationCode"). The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. """ refresh_url: str | None = None """The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS. """ scopes: dict[str, str] | None = None """ **REQUIRED** for ``oauth2``. The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it the map MAY be empty. """ litestar-2.16.0/litestar/openapi/spec/oauth_flows.py000066400000000000000000000016011500564371300225510ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from litestar.openapi.spec.base import BaseSchemaObject if TYPE_CHECKING: from litestar.openapi.spec.oauth_flow import OAuthFlow __all__ = ("OAuthFlows",) @dataclass class OAuthFlows(BaseSchemaObject): """Allows configuration of the supported OAuth Flows.""" implicit: OAuthFlow | None = None """Configuration for the OAuth Implicit flow.""" password: OAuthFlow | None = None """Configuration for the OAuth Resource Owner Password flow.""" client_credentials: OAuthFlow | None = None """Configuration for the OAuth Client Credentials flow. Previously called ``application`` in OpenAPI 2.0.""" authorization_code: OAuthFlow | None = None """Configuration for the OAuth Authorization Code flow. Previously called ``accessCode`` in OpenAPI 2.0.""" litestar-2.16.0/litestar/openapi/spec/open_api.py000066400000000000000000000077161500564371300220260ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING from litestar.openapi.spec.base import BaseSchemaObject from litestar.openapi.spec.components import Components from litestar.openapi.spec.server import Server if TYPE_CHECKING: from litestar.openapi.spec.external_documentation import ExternalDocumentation from litestar.openapi.spec.info import Info from litestar.openapi.spec.path_item import PathItem from litestar.openapi.spec.paths import Paths from litestar.openapi.spec.reference import Reference from litestar.openapi.spec.security_requirement import SecurityRequirement from litestar.openapi.spec.tag import Tag __all__ = ("OpenAPI",) @dataclass class OpenAPI(BaseSchemaObject): """Root OpenAPI document.""" info: Info """ **REQUIRED**. Provides metadata about the API. The metadata MAY be used by tooling as required. """ openapi: str = "3.1.0" """ **REQUIRED**. This string MUST be the `version number `_ of the OpenAPI Specification that the OpenAPI document uses. The ``openapi`` field SHOULD be used by tooling to interpret the OpenAPI document. This is *not* related to the API `info.version `_ string. """ json_schema_dialect: str | None = None """The default value for the ``$schema`` keyword within `Schema Objects `_ contained within this OAS document. This MUST be in the form of a URI. """ servers: list[Server] = field(default_factory=lambda x: [Server(url="/")]) # type: ignore[misc, arg-type] """An array of Server Objects, which provide connectivity information to a target server. If the ``servers`` property is not provided, or is an empty array, the default value would be a `Server Object `_ with a `url `_ value of ``/``. """ paths: Paths | None = None """The available paths and operations for the API.""" webhooks: dict[str, PathItem | Reference] | None = None """The incoming webhooks that MAY be received as part of this API and that the API consumer MAY choose to implement. Closely related to the ``callbacks`` feature, this section describes requests initiated other than by an API call, for example by an out of band registration. The key name is a unique string to refer to each webhook, while the (optionally referenced) Path Item Object describes a request that may be initiated by the API provider and the expected responses. An `example `_ is available. """ components: Components = field(default_factory=Components) """An element to hold various schemas for the document.""" security: list[SecurityRequirement] | None = None """A declaration of which security mechanisms can be used across the API. The list of values includes alternative security requirement objects that can be used. Only one of the security requirement objects need to be satisfied to authorize a request. Individual operations can override this definition. To make security optional, an empty security requirement ( ``{}`` ) can be included in the array. """ tags: list[Tag] | None = None """A list of tags used by the document with additional metadata. The order of the tags can be used to reflect on their order by the parsing tools. Not all tags that are used by the `Operation Object `_ must be declared. The tags that are not declared MAY be organized randomly or based on the tools' logic. Each tag name in the list MUST be unique. """ external_docs: ExternalDocumentation | None = None """Additional external documentation.""" litestar-2.16.0/litestar/openapi/spec/operation.py000066400000000000000000000113521500564371300222230ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from litestar.openapi.spec.base import BaseSchemaObject if TYPE_CHECKING: from litestar.openapi.spec.callback import Callback from litestar.openapi.spec.external_documentation import ExternalDocumentation from litestar.openapi.spec.parameter import Parameter from litestar.openapi.spec.reference import Reference from litestar.openapi.spec.request_body import RequestBody from litestar.openapi.spec.responses import Responses from litestar.openapi.spec.security_requirement import SecurityRequirement from litestar.openapi.spec.server import Server __all__ = ("Operation",) @dataclass class Operation(BaseSchemaObject): """Describes a single API operation on a path.""" tags: list[str] | None = None """A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources or any other qualifier. """ summary: str | None = None """A short summary of what the operation does.""" description: str | None = None """A verbose explanation of the operation behavior. `CommonMark syntax `_ MAY be used for rich text representation. """ external_docs: ExternalDocumentation | None = None """Additional external documentation for this operation.""" operation_id: str | None = None """Unique string used to identify the operation. The id MUST be unique among all operations described in the API. The operationId value is **case-sensitive**. Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, it is RECOMMENDED to follow common programming naming conventions. """ parameters: list[Parameter | Reference] | None = None """A list of parameters that are applicable for this operation. If a parameter is already defined at the `Path Item `_, the new definition will override it but can never remove it. The list MUST NOT include duplicated parameters. A unique parameter is defined by a combination of a `name `_ and `location `_. The list can use the `Reference Object `_ to link to parameters that are defined at the `OpenAPI Object's components/parameters `_. """ request_body: RequestBody | Reference | None = None """The request body applicable for this operation. The ``requestBody`` is fully supported in HTTP methods where the HTTP 1.1 specification :rfc:`7231` has explicitly defined semantics for request bodies. In other cases where the HTTP spec is vague (such as `GET `_, `HEAD `_ and `DELETE `_, ``requestBody`` is permitted but does not have well-defined semantics and SHOULD be avoided if possible. """ responses: Responses | None = None """The list of possible responses as they are returned from executing this operation.""" callbacks: dict[str, Callback | Reference] | None = None """A map of possible out-of band callbacks related to the parent operation. The key is a unique identifier for the Callback Object. Each value in the map is a `Callback Object `_ that describes a request that may be initiated by the API provider and the expected responses. """ deprecated: bool = False """Declares this operation to be deprecated. Consumers SHOULD refrain from usage of the declared operation. Default value is ``False``. """ security: list[SecurityRequirement] | None = None """A declaration of which security mechanisms can be used for this operation. The list of values includes alternative security requirement objects that can be used. Only one of the security requirement objects need to be satisfied to authorize a request. To make security optional, an empty security requirement (``{}``) can be included in the array. This definition overrides any declared top-level `security `_. To remove a top-level security declaration, an empty array can be used. """ servers: list[Server] | None = None """An alternative ``server`` array to service this operation. If an alternative ``server`` object is specified at the Path Item Object or Root level, it will be overridden by this value. """ litestar-2.16.0/litestar/openapi/spec/parameter.py000066400000000000000000000145661500564371300222150ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Mapping from litestar.openapi.spec.base import BaseSchemaObject if TYPE_CHECKING: from litestar.openapi.spec.example import Example from litestar.openapi.spec.media_type import OpenAPIMediaType from litestar.openapi.spec.reference import Reference from litestar.openapi.spec.schema import Schema __all__ = ("Parameter",) @dataclass class Parameter(BaseSchemaObject): """Describes a single operation parameter. A unique parameter is defined by a combination of a `name `_ and `location `_. """ name: str """ **REQUIRED**. The name of the parameter. Parameter names are *case sensitive*. - If `in `_ is ``"path"``, the ``name`` field MUST correspond to a template expression occurring within the `path `_ field in the `Paths Object `_. See `Path Templating `_ for further information. - If `in `_ is ``"header"`` and the ``name`` field is ``"Accept"``, ``"Content-Type"`` or ``"Authorization"``, the parameter definition SHALL be ignored. - For all other cases, the ``name`` corresponds to the parameter name used by the `in `_ property. """ param_in: str """ **REQUIRED**. The location of the parameter. Possible values are ``"query"``, ``"header"``, ``"path"`` or ``"cookie"``. """ schema: Schema | Reference | None = None """The schema defining the type used for the parameter.""" description: str | None = None """A brief description of the parameter. This could contain examples of use. `CommonMark syntax `_ MAY be used for rich text representation. """ required: bool = False """Determines whether this parameter is mandatory. If the `parameter location `_ is ``"path"``, this property is **REQUIRED** and its value MUST be ``True``. Otherwise, the property MAY be included and its default value is ``False``. """ deprecated: bool = False """Specifies that a parameter is deprecated and SHOULD be transitioned out of usage. Default value is ``False``. """ allow_empty_value: bool = False """Sets the ability to pass empty-valued parameters. This is valid only for ``query`` parameters and allows sending a parameter with an empty value. Default value is ``False``. If `style `__ is used, and if behavior is ``n/a`` (cannot be serialized), the value of ``allowEmptyValue`` SHALL be ignored. Use of this property is NOT RECOMMENDED, as it is likely to be removed in a later revision. The rules for serialization of the parameter are specified in one of two ways. For simpler scenarios, a `schema `_ and `style `__ can describe the structure and syntax of the parameter. """ style: str | None = None """Describes how the parameter value will be serialized depending on the ype of the parameter value. Default values (based on value of ``in``): - for ``query`` - ``form`` - for ``path`` - ``simple`` - for ``header`` - ``simple`` - for ``cookie`` - ``form`` """ explode: bool | None = None """When this is true, parameter values of type ``array`` or ``object`` generate separate parameters for each value of the array or key-value pair of the map. For other types of parameters this property has no effect. When `style `__ is ``form``, the default value is ``True``. For all other styles, the default value is ``False``. """ allow_reserved: bool = False """Determines whether the parameter value SHOULD allow reserved characters, as defined by. :rfc:`3986` ``:/?#[]@!$&'()*+,;=`` to be included without percent-encoding. This property only applies to parameters with an ``in`` value of ``query``. The default value is ``False``. """ example: Any | None = None """Example of the parameter's potential value. The example SHOULD match the specified schema and encoding properties if present. The ``example`` field is mutually exclusive of the ``examples`` field. Furthermore, if referencing a ``schema`` that contains an example, the ``example`` value SHALL _override_ the example provided by the schema. To represent examples of media types that cannot naturally be represented in JSON or YAML, a string value can contain the example with escaping where necessary. """ examples: Mapping[str, Example | Reference] | None = None """Examples of the parameter's potential value. Each example SHOULD contain a value in the correct format as specified in the parameter encoding. The ``examples`` field is mutually exclusive of the ``example`` field. Furthermore, if referencing a ``schema`` that contains an example, the ``examples`` value SHALL _override_ the example provided by the schema. For more complex scenarios, the `content `_ property can define the media type and schema of the parameter. A parameter MUST contain either a ``schema`` property, or a ``content`` property, but not both. When ``example`` or ``examples`` are provided in conjunction with the ``schema`` object, the example MUST follow the prescribed serialization strategy for the parameter. """ content: dict[str, OpenAPIMediaType] | None = None """A map containing the representations for the parameter. The key is the media type and the value describes it. The map MUST only contain one entry. """ @property def _exclude_fields(self) -> set[str]: exclude = set() if self.param_in != "query": # these are only allowed in query params exclude.update({"allow_empty_value", "allow_reserved"}) return exclude litestar-2.16.0/litestar/openapi/spec/path_item.py000066400000000000000000000062551500564371300222030ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from litestar.openapi.spec.base import BaseSchemaObject if TYPE_CHECKING: from litestar.openapi.spec.operation import Operation from litestar.openapi.spec.parameter import Parameter from litestar.openapi.spec.reference import Reference from litestar.openapi.spec.server import Server __all__ = ("PathItem",) @dataclass class PathItem(BaseSchemaObject): """Describes the operations available on a single path. A Path Item MAY be empty, due to `ACL constraints `_. The path itself is still exposed to the documentation viewer, but they will not know which operations and parameters are available. """ ref: str | None = None """Allows for an external definition of this path item. The referenced structure MUST be in the format of a `Path Item Object `. In case a Path Item Object field appears both in the defined object and the referenced object, the behavior is undefined. See the rules for resolving `Relative References `_. """ summary: str | None = None """An optional, string summary, intended to apply to all operations in this path.""" description: str | None = None """An optional, string description, intended to apply to all operations in this path. `CommonMark syntax `_ MAY be used for rich text representation. """ get: Operation | None = None """A definition of a GET operation on this path.""" put: Operation | None = None """A definition of a PUT operation on this path.""" post: Operation | None = None """A definition of a POST operation on this path.""" delete: Operation | None = None """A definition of a DELETE operation on this path.""" options: Operation | None = None """A definition of a OPTIONS operation on this path.""" head: Operation | None = None """A definition of a HEAD operation on this path.""" patch: Operation | None = None """A definition of a PATCH operation on this path.""" trace: Operation | None = None """A definition of a TRACE operation on this path.""" servers: list[Server] | None = None """An alternative ``server`` array to service all operations in this path.""" parameters: list[Parameter | Reference] | None = None """A list of parameters that are applicable for all the operations described under this path. These parameters can be overridden at the operation level, but cannot be removed there. The list MUST NOT include duplicated parameters. A unique parameter is defined by a combination of a `name `_ and `location `_. The list can use the `Reference Object `_ to link to parameters that are defined at the `OpenAPI Object's components/parameters `_. """ litestar-2.16.0/litestar/openapi/spec/paths.py000066400000000000000000000023471500564371300213460ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Dict if TYPE_CHECKING: from litestar.openapi.spec import PathItem Paths = Dict[str, "PathItem"] """Holds the relative paths to the individual endpoints and their operations. The path is appended to the URL from the. `Server Object `_ in order to construct the full URL. The Paths MAY be empty, due to `Access Control List (ACL) constraints `_. Patterned Fields /{path}: PathItem A relative path to an individual endpoint. The field name MUST begin with a forward slash (``/``). The path is **appended** (no relative URL resolution) to the expanded URL from the `Server Object `_ 's ``url`` field in order to construct the full URL. `Path templating `_ is allowed. When matching URLs, concrete (non-templated) paths would be matched before their templated counterparts. Templated paths with the same hierarchy but different templated names MUST NOT exist as they are identical. In case of ambiguous matching, it's up to the tooling to decide which one to use. """ litestar-2.16.0/litestar/openapi/spec/reference.py000066400000000000000000000025071500564371300221630ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from litestar.openapi.spec.base import BaseSchemaObject __all__ = ("Reference",) @dataclass class Reference(BaseSchemaObject): """A simple object to allow referencing other components in the OpenAPI document, internally and externally. The ``$ref`` string value contains a URI `RFC3986 `_ , which identifies the location of the value being referenced. See the rules for resolving `Relative References `_. """ ref: str """**REQUIRED**. The reference identifier. This MUST be in the form of a URI.""" summary: str | None = None """A short summary which by default SHOULD override that of the referenced component. If the referenced object-type does not allow a ``summary`` field, then this field has no effect. """ description: str | None = None """A description which by default SHOULD override that of the referenced component. `CommonMark syntax `_ MAY be used for rich text representation. If the referenced object-type does not allow a ``description`` field, then this field has no effect. """ @property def value(self) -> str: return self.ref.split("/")[-1] litestar-2.16.0/litestar/openapi/spec/request_body.py000066400000000000000000000021071500564371300227260ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from litestar.openapi.spec.base import BaseSchemaObject if TYPE_CHECKING: from litestar.openapi.spec.media_type import OpenAPIMediaType __all__ = ("RequestBody",) @dataclass class RequestBody(BaseSchemaObject): """Describes a single request body.""" content: dict[str, OpenAPIMediaType] """ **REQUIRED**. The content of the request body. The key is a media type or `media type range `_ and the value describes it. For requests that match multiple keys, only the most specific key is applicable. e.g. ``text/plain`` overrides ``text/*`` """ description: str | None = None """A brief description of the request body. This could contain examples of use. `CommonMark syntax `_ MAY be used for rich text representation. """ required: bool = False """Determines if the request body is required in the request. Defaults to ``False``. """ litestar-2.16.0/litestar/openapi/spec/response.py000066400000000000000000000034761500564371300220710ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from litestar.openapi.spec.base import BaseSchemaObject if TYPE_CHECKING: from litestar.openapi.spec.header import OpenAPIHeader from litestar.openapi.spec.link import Link from litestar.openapi.spec.media_type import OpenAPIMediaType from litestar.openapi.spec.reference import Reference __all__ = ("OpenAPIResponse",) @dataclass class OpenAPIResponse(BaseSchemaObject): """Describes a single response from an API Operation, including design-time, static ``links`` to operations based on the response. """ description: str """**REQUIRED**. A short description of the response. `CommonMark syntax `_ MAY be used for rich text representation. """ headers: dict[str, OpenAPIHeader | Reference] | None = None """Maps a header name to its definition. `RFC7230 `_ states header names are case insensitive. If a response header is defined with the name ``Content-Type``, it SHALL be ignored. """ content: dict[str, OpenAPIMediaType] | None = None """A map containing descriptions of potential response payloads. The key is a media type or `media type range `_ and the value describes it. For responses that match multiple keys, only the most specific key is applicable. e.g. ``text/plain`` overrides ``text/*`` """ links: dict[str, Link | Reference] | None = None """A map of operations links that can be followed from the response. The key of the map is a short name for the link, following the naming constraints of the names for `Component Objects `_. """ litestar-2.16.0/litestar/openapi/spec/responses.py000066400000000000000000000044771500564371300222560ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Dict, Union if TYPE_CHECKING: from litestar.openapi.spec.reference import Reference from litestar.openapi.spec.response import OpenAPIResponse Responses = Dict[str, Union["OpenAPIResponse", "Reference"]] """A container for the expected responses of an operation. The container maps a HTTP response code to the expected response. The documentation is not necessarily expected to cover all possible HTTP response codes because they may not be known in advance. However, documentation is expected to cover a successful operation response and any known errors. The ``default`` MAY be used as a default response object for all HTTP codes hat are not covered individually by the specification. The ``Responses Object`` MUST contain at least one response code, and it SHOULD be the response for a successful operation call. Fixed Fields default: ``Optional[Union[Response, Reference]]`` The documentation of responses other than the ones declared for specific HTTP response codes. Use this field to cover undeclared responses. A `Reference Object `_ can link to a response that the `OpenAPI Object's components/responses `_ section defines. Patterned Fields {httpStatusCode}: ``Optional[Union[Response, Reference]]`` Any `HTTP status code `_ can be used as the property name, but only one property per code, to describe the expected response for that HTTP status code. A `Reference Object `_ can link to a response that is defined in the `OpenAPI Object's components/responses `_ section. This field MUST be enclosed in quotation marks (for example, ``200``) for compatibility between JSON and YAML. To define a range of response codes, this field MAY contain the uppercase wildcard character ``X``. For example, ``2XX`` represents all response codes between ``[200-299]``. Only the following range definitions are allowed: ``1XX``, ``2XX``, ``3XX``, ``4XX``, and ``5XX``. If a response is defined using an explicit code, the explicit code definition takes precedence over the range definition for that code. """ litestar-2.16.0/litestar/openapi/spec/schema.py000066400000000000000000001061571500564371300214730ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field, fields, is_dataclass from typing import TYPE_CHECKING, Any, Hashable, Mapping, Sequence, cast from litestar.openapi.spec.base import BaseSchemaObject from litestar.utils.predicates import is_non_string_sequence if TYPE_CHECKING: from litestar.openapi.spec.discriminator import Discriminator from litestar.openapi.spec.enums import OpenAPIFormat, OpenAPIType from litestar.openapi.spec.external_documentation import ExternalDocumentation from litestar.openapi.spec.reference import Reference from litestar.openapi.spec.xml import XML from litestar.types import DataclassProtocol __all__ = ("Schema", "SchemaDataContainer") def _recursive_hash(value: Hashable | Sequence | Mapping | DataclassProtocol | type[DataclassProtocol]) -> int: if isinstance(value, Mapping): hash_value = 0 for k, v in value.items(): if k != "examples": hash_value += hash(k) hash_value += _recursive_hash(v) return hash_value if is_dataclass(value): hash_value = hash(type(value).__name__) for field in fields(value): if field.name != "examples": hash_value += hash(field.name) hash_value += _recursive_hash(getattr(value, field.name, None)) return hash_value if is_non_string_sequence(value): return sum(_recursive_hash(v) for v in value) return hash(value) if isinstance(value, Hashable) else 0 @dataclass class Schema(BaseSchemaObject): """The Schema Object allows the definition of input and output data types. These types can be objects, but also primitives and arrays. This object is a superset of the `JSON Schema Specification Draft 2020-12 `_. For more information about the properties, see `JSON Schema Core `_ and `JSON Schema Validation `_. Unless stated otherwise, the property definitions follow those of JSON Schema and do not add any additional semantics. Where JSON Schema indicates that behavior is defined by the application (e.g. for annotations), OAS also defers the definition of semantics to the application consuming the OpenAPI document. The following properties are taken directly from the `JSON Schema Core `_ and follow the same specifications. """ all_of: Sequence[Reference | Schema] | None = field(default=None, metadata={"alias": "allOf"}) """This keyword's value MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. An instance validates successfully against this keyword if it validates successfully against all schemas defined by this keyword's value. """ any_of: Sequence[Reference | Schema] | None = field(default=None, metadata={"alias": "anyOf"}) """This keyword's value MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. An instance validates successfully against this keyword if it validates successfully against at least one schema defined by this keyword's value. Note that when annotations are being collected, all subschemas MUST be examined so that annotations are collected from each subschema that validates successfully. """ one_of: Sequence[Reference | Schema] | None = field(default=None, metadata={"alias": "oneOf"}) """This keyword's value MUST be a non-empty array. Each item of the array MUST be a valid JSON Schema. An instance validates successfully against this keyword if it validates successfully against exactly one schema defined by this keyword's value. """ schema_not: Reference | Schema | None = field(default=None, metadata={"alias": "not"}) """This keyword's value MUST be a valid JSON Schema. An instance is valid against this keyword if it fails to validate successfully against the schema defined by this keyword. """ schema_if: Reference | Schema | None = field(default=None, metadata={"alias": "if"}) """This keyword's value MUST be a valid JSON Schema. This validation outcome of this keyword's subschema has no direct effect on the overall validation result. Rather, it controls which of the "then" or "else" keywords are evaluated. Instances that successfully validate against this keyword's subschema MUST also be valid against the subschema value of the "then" keyword, if present. Instances that fail to validate against this keyword's subschema MUST also be valid against the subschema value of the "else" keyword, if present. If annotations (Section 7.7) are being collected, they are collected rom this keyword's subschema in the usual way, including when the keyword is present without either "then" or "else". """ then: Reference | Schema | None = None """This keyword's value MUST be a valid JSON Schema. When "if" is present, and the instance successfully validates against its subschema, then validation succeeds against this keyword if the instance also successfully validates against this keyword's subschema. This keyword has no effect when "if" is absent, or when the instance fails to validate against its subschema. Implementations MUST NOT evaluate the instance against this keyword, for either validation or annotation collection purposes, in such cases. """ schema_else: Reference | Schema | None = field(default=None, metadata={"alias": "else"}) """This keyword's value MUST be a valid JSON Schema. When "if" is present, and the instance fails to validate against its subschema, then validation succeeds against this keyword if the instance successfully validates against this keyword's subschema. This keyword has no effect when "if" is absent, or when the instance successfully validates against its subschema. Implementations MUST NOT evaluate the instance against this keyword, for either validation or annotation collection purposes, in such cases. """ dependent_schemas: dict[str, Reference | Schema] | None = field( default=None, metadata={"alias": "dependentSchemas"} ) """This keyword specifies subschemas that are evaluated if the instance is an object and contains a certain property. This keyword's value MUST be an object. Each value in the object MUST be a valid JSON Schema. If the object key is a property in the instance, the entire instance must validate against the subschema. Its use is dependent on the presence of the property. Omitting this keyword has the same behavior as an empty object. """ prefix_items: Sequence[Reference | Schema] | None = field(default=None, metadata={"alias": "prefixItems"}) """The value of "prefixItems" MUST be a non-empty array of valid JSON Schemas. Validation succeeds if each element of the instance validates against the schema at the same position, if any. This keyword does not constrain the length of the array. If the array is longer than this keyword's value, this keyword validates only the prefix of matching length. This keyword produces an annotation value which is the largest index to which this keyword applied a subschema. he value MAY be a boolean true if a subschema was applied to every index of the instance, such as is produced by the "items" keyword. This annotation affects the behavior of "items" and "unevaluatedItems". Omitting this keyword has the same assertion behavior as an empty array. """ items: Reference | Schema | None = None """The value of "items" MUST be a valid JSON Schema. This keyword applies its subschema to all instance elements at indexes greater than the length of the "prefixItems" array in the same schema object, as reported by the annotation result of that "prefixItems" keyword. If no such annotation result exists, "items" applies its subschema to all instance array elements. [[CREF11: Note that the behavior of "items" without "prefixItems" is identical to that of the schema form of "items" in prior drafts. When "prefixItems" is present, the behavior of "items" is identical to the former "additionalItems" keyword. ]] If the "items" subschema is applied to any positions within the instance array, it produces an annotation result of boolean true, indicating that all remaining array elements have been evaluated against this keyword's subschema. Omitting this keyword has the same assertion behavior as an empty schema. Implementations MAY choose to implement or optimize this keyword in another way that produces the same effect, such as by directly checking for the presence and size of a "prefixItems" array. Implementations that do not support annotation collection MUST do so. """ contains: Reference | Schema | None = None """The value of this keyword MUST be a valid JSON Schema. An array instance is valid against "contains" if at least one of its elements is valid against the given schema. The subschema MUST be applied to every array element even after the first match has been found, in order to collect annotations for use by other keywords. This is to ensure that all possible annotations are collected. Logically, the validation result of applying the value subschema to each item in the array MUST be ORed with "false", resulting in an overall validation result. This keyword produces an annotation value which is an array of the indexes to which this keyword validates successfully when applying its subschema, in ascending order. The value MAY be a boolean "true" if the subschema validates successfully when applied to every index of the instance. The annotation MUST be present if the instance array to which this keyword's schema applies is empty. """ properties: dict[str, Reference | Schema] | None = None """The value of "properties" MUST be an object. Each value of this object MUST be a valid JSON Schema. Validation succeeds if, for each name that appears in both the instance and as a name within this keyword's value, the child instance for that name successfully validates against the corresponding schema. The annotation result of this keyword is the set of instance property names matched by this keyword. Omitting this keyword has the same assertion behavior as an empty object. """ pattern_properties: dict[str, Reference | Schema] | None = field( default=None, metadata={"alias": "patternProperties"} ) """The value of "patternProperties" MUST be an object. Each property name of this object SHOULD be a valid regular expression, according to the ECMA-262 regular expression dialect. Each property value of this object MUST be a valid JSON Schema. Validation succeeds if, for each instance name that matches any regular expressions that appear as a property name in this keyword's value, the child instance for that name successfully validates against each schema that corresponds to a matching regular expression. The annotation result of this keyword is the set of instance property names matched by this keyword. Omitting this keyword has the same assertion behavior as an empty object. """ additional_properties: Reference | Schema | bool | None = field( default=None, metadata={"alias": "additionalProperties"} ) """The value of "additionalProperties" MUST be a valid JSON Schema. The behavior of this keyword depends on the presence and annotation results of "properties" and "patternProperties" within the same schema object. Validation with "additionalProperties" applies only to the child values of instance names that do not appear in the annotation results of either "properties" or "patternProperties". For all such properties, validation succeeds if the child instance validates against the "additionalProperties" schema. The annotation result of this keyword is the set of instance property names validated by this keyword's subschema. Omitting this keyword has the same assertion behavior as an empty schema. Implementations MAY choose to implement or optimize this keyword in another way that produces the same effect, such as by directly checking the names in "properties" and the patterns in "patternProperties" against the instance property set. Implementations that do not support annotation collection MUST do so. """ property_names: Reference | Schema | None = field(default=None, metadata={"alias": "propertyNames"}) """The value of "propertyNames" MUST be a valid JSON Schema. If the instance is an object, this keyword validates if every property name in the instance validates against the provided schema. Note the property name that the schema is testing will always be a string. Omitting this keyword has the same behavior as an empty schema. """ unevaluated_items: Reference | Schema | None = field(default=None, metadata={"alias": "unevaluatedItems"}) """The value of "unevaluatedItems" MUST be a valid JSON Schema. The behavior of this keyword depends on the annotation results of adjacent keywords that apply to the instance location being validated. Specifically, the annotations from "prefixItems" items", and "contains", which can come from those keywords when they are adjacent to the "unevaluatedItems" keyword. Those three annotations, as well as "unevaluatedItems", can also result from any and all adjacent in-place applicator (Section 10.2) keywords. This includes but is not limited to the in-place applicators defined in this document. If no relevant annotations are present, the "unevaluatedItems" subschema MUST be applied to all locations in the array. If a boolean true value is present from any of the relevant annotations, unevaluatedItems" MUST be ignored. Otherwise, the subschema MUST be applied to any index greater than the largest annotation value for "prefixItems", which does not appear in any annotation value for "contains". This means that "prefixItems", "items", "contains", and all in-place applicators MUST be evaluated before this keyword can be evaluated. Authors of extension keywords MUST NOT define an in-place applicator that would need to be evaluated after this keyword. If the "unevaluatedItems" subschema is applied to any positions within the instance array, it produces an annotation result of boolean true, analogous to the behavior of "items". Omitting this keyword has the same assertion behavior as an empty schema. """ unevaluated_properties: Reference | Schema | None = field(default=None, metadata={"alias": "unevaluatedProperties"}) """The value of "unevaluatedProperties" MUST be a valid JSON Schema. The behavior of this keyword depends on the annotation results of adjacent keywords that apply to the instance location being validated. Specifically, the annotations from "properties", "patternProperties", and "additionalProperties", which can come from those keywords when they are adjacent to the "unevaluatedProperties" keyword. Those three annotations, as well as "unevaluatedProperties", can also result from any and all adjacent in-place applicator (Section 10.2) keywords. This includes but is not limited to the in-place applicators defined in this document. Validation with "unevaluatedProperties" applies only to the child values of instance names that do not appear in the "properties", "patternProperties", "additionalProperties", or "unevaluatedProperties" annotation results that apply to the instance location being validated. For all such properties, validation succeeds if the child instance validates against the "unevaluatedProperties" schema. This means that "properties", "patternProperties", "additionalProperties", and all in-place applicators MUST be evaluated before this keyword can be evaluated. Authors of extension keywords MUST NOT define an in-place applicator that would need to be evaluated after this keyword. The annotation result of this keyword is the set of instance property names validated by this keyword's subschema. Omitting this keyword has the same assertion behavior as an empty schema. The following properties are taken directly from the `JSON Schema Validation `_ and follow the same specifications: """ type: OpenAPIType | Sequence[OpenAPIType] | None = None """The value of this keyword MUST be either a string or an array. If it is an array, elements of the array MUST be strings and MUST be unique. String values MUST be one of the six primitive types (``"null"``, ``"boolean"``, ``"object"``, ``"array"``, ``"number"``, and ``"string"``), or ``"integer"`` which matches any number with a zero fractional part. An instance validates if and only if the instance is in any of the sets listed for this keyword. """ enum: Sequence[Any] | None = None """The value of this keyword MUST be an array. This array SHOULD have at least one element. Elements in the array SHOULD be unique. An instance validates successfully against this keyword if its value is equal to one of the elements in this keyword's array value. Elements in the array might be of any type, including null. """ const: Any | None = None """The value of this keyword MAY be of any type, including null. Use of this keyword is functionally equivalent to an "enum" (Section 6.1.2) with a single value. An instance validates successfully against this keyword if its value is equal to the value of the keyword. """ multiple_of: float | None = field(default=None, metadata={"alias": "multipleOf"}) """The value of "multipleOf" MUST be a number, strictly greater than 0. A numeric instance is only valid if division by this keyword's value results in an integer. """ maximum: float | None = None """The value of "maximum" MUST be a number, representing an inclusive upper limit for a numeric instance. If the instance is a number, then this keyword validates only if the instance is less than or exactly equal to "maximum". """ exclusive_maximum: float | None = None """The value of "exclusiveMaximum" MUST be a number, representing an exclusive upper limit for a numeric instance. If the instance is a number, then the instance is valid only if it has a value strictly less than (not equal to) "exclusiveMaximum". """ minimum: float | None = None """The value of "minimum" MUST be a number, representing an inclusive lower limit for a numeric instance. If the instance is a number, then this keyword validates only if the instance is greater than or exactly equal to "minimum". """ exclusive_minimum: float | None = field(default=None, metadata={"alias": "exclusiveMinimum"}) """The value of "exclusiveMinimum" MUST be a number, representing an exclusive lower limit for a numeric instance. If the instance is a number, then the instance is valid only if it has a value strictly greater than (not equal to) "exclusiveMinimum". """ max_length: int | None = field(default=None, metadata={"alias": "maxLength"}) """The value of this keyword MUST be a non-negative integer. A string instance is valid against this keyword if its length is less than, or equal to, the value of this keyword. The length of a string instance is defined as the number of its characters as defined by :rfc:`8259`. """ min_length: int | None = field(default=None, metadata={"alias": "minLength"}) """The value of this keyword MUST be a non-negative integer. A string instance is valid against this keyword if its length is greater than, or equal to, the value of this keyword. The length of a string instance is defined as the number of its characters as defined by :rfc:`8259`. Omitting this keyword has the same behavior as a value of 0. """ pattern: str | None = None """The value of this keyword MUST be a string. This string SHOULD be a valid regular expression, according to the ECMA-262 regular expression dialect. A string instance is considered valid if the regular expression matches the instance successfully. Recall: regular expressions are not implicitly anchored. """ max_items: int | None = field(default=None, metadata={"alias": "maxItems"}) """The value of this keyword MUST be a non-negative integer. An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword. """ min_items: int | None = field(default=None, metadata={"alias": "minItems"}) """The value of this keyword MUST be a non-negative integer. An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. Omitting this keyword has the same behavior as a value of 0. """ unique_items: bool | None = field(default=None, metadata={"alias": "uniqueItems"}) """The value of this keyword MUST be a boolean. If this keyword has boolean value false, the instance validates successfully. If it has boolean value true, the instance validates successfully if all of its elements are unique. Omitting this keyword has the same behavior as a value of false. """ max_contains: int | None = field(default=None, metadata={"alias": "maxContains"}) """The value of this keyword MUST be a non-negative integer. If "contains" is not present within the same schema object, then this keyword has no effect. An instance array is valid against "maxContains" in two ways, depending on the form of the annotation result of an adjacent "contains" [json-schema] keyword. The first way is if the annotation result is an array and the length of that array is less than or equal to the "maxContains" value. The second way is if the annotation result is a boolean "true" and the instance array length is less than r equal to the "maxContains" value. """ min_contains: int | None = field(default=None, metadata={"alias": "minContains"}) """The value of this keyword MUST be a non-negative integer. If "contains" is not present within the same schema object, then this keyword has no effect. An instance array is valid against "minContains" in two ways, depending on the form of the annotation result of an adjacent "contains" [json-schema] keyword. The first way is if the annotation result is an array and the length of that array is greater than or equal to the "minContains" value. The second way is if the annotation result is a boolean "true" and the instance array length is greater than or equal to the "minContains" value. A value of 0 is allowed, but is only useful for setting a range of occurrences from 0 to the value of "maxContains". A value of 0 with no "maxContains" causes "contains" to always pass validation. Omitting this keyword has the same behavior as a value of 1. """ max_properties: int | None = field(default=None, metadata={"alias": "maxProperties"}) """The value of this keyword MUST be a non-negative integer. An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword. """ min_properties: int | None = field(default=None, metadata={"alias": "minProperties"}) """The value of this keyword MUST be a non-negative integer. An object instance is valid against "minProperties" if its number of properties is greater than, or equal to, the value of this keyword. Omitting this keyword has the same behavior as a value of 0. """ required: Sequence[str] | None = None """The value of this keyword MUST be an array. Elements of this array, if any, MUST be strings, and MUST be unique. An object instance is valid against this keyword if every item in the rray is the name of a property in the instance. Omitting this keyword has the same behavior as an empty array. """ dependent_required: dict[str, Sequence[str]] | None = field(default=None, metadata={"alias": "dependentRequired"}) """The value of this keyword MUST be an object. Properties in this object, f any, MUST be arrays. Elements in each array, if any, MUST be strings, and MUST be unique. This keyword specifies properties that are required if a specific other property is present. Their requirement is dependent on the presence of the other property. Validation succeeds if, for each name that appears in both the instance and as a name within this keyword's value, every item in the corresponding array is also the name of a property in the instance. Omitting this keyword has the same behavior as an empty object. """ format: OpenAPIFormat | None = None """From OpenAPI: See `Data Type Formats `_ for further details. While relying on JSON Schema's defined formats, the OAS offers a few additional predefined formats. From JSON Schema: Structural validation alone may be insufficient to allow an application to correctly utilize certain values. The "format" annotation keyword is defined to allow schema authors to convey semantic information for a fixed subset of values which are accurately described by authoritative resources, be they RFCs or other external specifications. The value of this keyword is called a format attribute. It MUST be a string. A format attribute can generally only validate a given set of instance types. If the type of the instance to validate is not in this set, validation for this format attribute and instance SHOULD succeed. All format attributes defined in this section apply to strings, but a format attribute can be specified to apply to any instance types defined in the data model defined in the core JSON Schema. [json-schema] [[CREF1: Note that the "type" keyword in this specification defines an "integer" type which is not part of the data model. Therefore a format attribute can be limited to numbers, but not specifically to integers. However, a numeric format can be used alongside the "type" keyword with a value of "integer", or could be explicitly defined to always pass if the number is not an integer, which produces essentially the same behavior as only applying to integers. ]] """ content_encoding: str | None = field(default=None, metadata={"alias": "contentEncoding"}) """If the instance value is a string, this property defines that the string SHOULD be interpreted as binary data and decoded using the encoding named by this property. Possible values indicating base 16, 32, and 64 encodings with several variations are listed in :rfc:`4648`. Additionally, sections 6.7 and 6.8 of :rfc:`2045` provide encodings used in MIME. As "base64" is defined in both RFCs, the definition from :rfc:`4648` SHOULD be assumed unless the string is specifically intended for use in a MIME context. Note that all of these encodings result in strings consisting only of 7-bit ASCII characters. therefore, this keyword has no meaning for strings containing characters outside of that range. If this keyword is absent, but "contentMediaType" is present, this indicates that the encoding is the identity encoding, meaning that no transformation was needed in order to represent the content in a UTF-8 string. """ content_media_type: str | None = field(default=None, metadata={"alias": "contentMediaType"}) """If the instance is a string, this property indicates the media type of the contents of the string. If "contentEncoding" is present, this property describes the decoded string. The value of this property MUST be a string, which MUST be a media type, as defined by :rfc:`2046` """ content_schema: Reference | Schema | None = field(default=None, metadata={"alias": "contentSchema"}) """If the instance is a string, and if "contentMediaType" is present, this property contains a schema which describes the structure of the string. This keyword MAY be used with any media type that can be mapped into JSON Schema's data model. The value of this property MUST be a valid JSON schema. It SHOULD be ignored if "contentMediaType" is not present. """ title: str | None = None """The value of "title" MUST be a string. The title can be used to decorate a user interface with information about the data produced by this user interface. A title will preferably be short. """ description: str | None = None """From OpenAPI: `CommonMark syntax `_ MAY be used for rich text representation. From JSON Schema: The value "description" MUST be a string. The description can be used to decorate a user interface with information about the data produced by this user interface. A description will provide explanation about the purpose of the instance described by this schema. """ default: Any | None = None """There are no restrictions placed on the value of this keyword. When multiple occurrences of this keyword are applicable to a single sub-instance, implementations SHOULD remove duplicates. This keyword can be used to supply a default JSON value associated with a particular schema. It is RECOMMENDED that a default value be valid against the associated schema. """ deprecated: bool | None = None """The value of this keyword MUST be a boolean. When multiple occurrences of this keyword are applicable to a single sub-instance, applications SHOULD consider the instance location to be deprecated if any occurrence specifies a true value. If "deprecated" has a value of boolean true, it indicates that applications SHOULD refrain from usage of the declared property. It MAY mean the property is going to be removed in the future. A root schema containing "deprecated" with a value of true indicates that the entire resource being described MAY be removed in the future. The "deprecated" keyword applies to each instance location to which the schema object containing the keyword successfully applies. This can result in scenarios where every array item or object property is deprecated even though the containing array or object is not. Omitting this keyword has the same behavior as a value of false. """ read_only: bool | None = field(default=None, metadata={"alias": "readOnly"}) """The value of "readOnly" MUST be a boolean. When multiple occurrences of this keyword are applicable to a single sub-instance, the resulting behavior SHOULD be as for a true value if any occurrence specifies a true value, and SHOULD be as for a false value otherwise. If "readOnly" has a value of boolean true, it indicates that the value of the instance is managed exclusively by the owning authority, and attempts by an application to modify the value of this property are expected to be ignored or rejected by that owning authority. An instance document that is marked as "readOnly" for the entire document MAY be ignored if sent to the owning authority, or MAY result in an error, at the authority's discretion. For example, "readOnly" would be used to mark a database-generated serial number as read-only, while "writeOnly" would be used to mark a password input field. This keyword can be used to assist in user interface instance generation. In particular, an application MAY choose to use a widget that hides input values as they are typed for write-only fields. Omitting these keywords has the same behavior as values of false. """ write_only: bool | None = field(default=None, metadata={"alias": "writeOnly"}) """The value of "writeOnly" MUST be a boolean. When multiple occurrences of this keyword are applicable to a single sub-instance, the resulting behavior SHOULD be as for a true value if any occurrence specifies a true value, and SHOULD be as for a false value otherwise. If "writeOnly" has a value of boolean true, it indicates that the value is never present when the instance is retrieved from the owning authority. It can be present when sent to the owning authority to update or create the document (or the resource it represents), but it will not be included in any updated or newly created version of the instance. An instance document that is marked as "writeOnly" for the entire document MAY be returned as a blank document of some sort, or MAY produce an error upon retrieval, or have the retrieval request ignored, at the authority's discretion. For example, "readOnly" would be used to mark a database-generated serial number as read-only, while "writeOnly" would be used to mark a password input field. This keyword can be used to assist in user interface instance generation. In particular, an application MAY choose to use a widget that hides input values as they are typed for write-only fields. Omitting these keywords has the same behavior as values of false. """ examples: list[Any] | None = None """The value of this must be an array containing the example values.""" discriminator: Discriminator | None = None """Adds support for polymorphism. The discriminator is an object name that is used to differentiate between other schemas which may satisfy the payload description. See `Composition and Inheritance `_ for more details. """ xml: XML | None = None """This MAY be used only on properties schemas. It has no effect on root schemas. Adds additional metadata to describe the XML representation of this property. """ external_docs: ExternalDocumentation | None = field(default=None, metadata={"alias": "externalDocs"}) """Additional external documentation for this schema.""" example: Any | None = None """A free-form property to include an example of an instance for this schema. To represent examples that cannot be naturally represented in JSON or YAML, a string value can be used to contain the example with escaping where necessary. Deprecated: The example property has been deprecated in favor of the JSON Schema examples keyword. Use of example is discouraged, and later versions of this specification may remove it. """ def __hash__(self) -> int: return _recursive_hash(self) @classmethod def field_aliases(cls) -> dict[str, str]: if hasattr(cls, "_field_aliases"): return cast("dict[str, str]", cls._field_aliases) retval = {} for field_def in fields(cls): if field_def.metadata is not None and (field_alias := field_def.metadata.get("alias")): retval[field_alias] = field_def.name cls._field_aliases = retval # type: ignore[attr-defined] return retval @dataclass class SchemaDataContainer(Schema): """Special class that allows using python data containers, e.g. dataclasses or pydantic models, to represent a schema """ data_container: Any = None """A data container instance that will be used to generate the schema.""" litestar-2.16.0/litestar/openapi/spec/security_requirement.py000066400000000000000000000032261500564371300245130ustar00rootroot00000000000000from __future__ import annotations from typing import Dict, List SecurityRequirement = Dict[str, List[str]] """Lists the required security schemes to execute this operation. The name used for each property MUST correspond to a security scheme declared in the. `Security Schemes `_ under the `Components Object `_. Security Requirement Objects that contain multiple schemes require that all schemes MUST be satisfied for a request to be authorized. This enables support for scenarios where multiple query parameters or HTTP headers are required to convey security information. When a list of Security Requirement Objects is defined on the `OpenAPI Object `_ or `Operation Object `_, only one of the Security Requirement Objects in the list needs to be satisfied to authorize the request. Patterned Fields {name}: ``List[str]`` Each name MUST correspond to a security scheme which is declared in the `Security Schemes `_ under the `Components Object `_. if the security scheme is of type ``"oauth2"`` or ``"openIdConnect"``, then the value is a list of scope names required for the execution, and the list MAY be empty if authorization does not require a specified scope. For other security scheme types, the array MAY contain a list of role names which are required for the execution,but are not otherwise defined or exchanged in-band. """ litestar-2.16.0/litestar/openapi/spec/security_scheme.py000066400000000000000000000052021500564371300234130ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Literal from litestar.openapi.spec.base import BaseSchemaObject if TYPE_CHECKING: from litestar.openapi.spec.oauth_flows import OAuthFlows __all__ = ("SecurityScheme",) @dataclass class SecurityScheme(BaseSchemaObject): """Defines a security scheme that can be used by the operations. Supported schemes are HTTP authentication, an API key (either as a header, a cookie parameter or as a query parameter), mutual TLS (use of a client certificate), OAuth2's common flows (implicit, password, client credentials and authorization code) as defined in :rfc`6749`, and `OpenID Connect Discovery `_. Please note that as of 2020, the implicit flow is about to be deprecated by `OAuth 2.0 Security Best Current Practice `_. Recommended for most use case is Authorization Code Grant flow with PKCE. """ type: Literal["apiKey", "http", "mutualTLS", "oauth2", "openIdConnect"] """**REQUIRED**. The type of the security scheme.""" description: str | None = None """A description for security scheme. `CommonMark syntax `_ MAY be used for rich text representation. """ name: str | None = None """ **REQUIRED** for ``apiKey``. The name of the header, query or cookie parameter to be used. """ security_scheme_in: Literal["query", "header", "cookie"] | None = None """ **REQUIRED** for ``apiKey``. The location of the API key. """ scheme: str | None = None """ **REQUIRED** for ``http``. The name of the HTTP Authorization scheme to be used in the authorization header as defined in :rfc:`7235`. The values used SHOULD be registered in the `IANA Authentication Scheme registry `_ """ bearer_format: str | None = None """A hint to the client to identify how the bearer token is formatted. Bearer tokens are usually generated by an authorization server, so this information is primarily for documentation purposes. """ flows: OAuthFlows | None = None """**REQUIRED** for ``oauth2``. An object containing configuration information for the flow types supported.""" open_id_connect_url: str | None = None """**REQUIRED** for ``openIdConnect``. OpenId Connect URL to discover OAuth2 configuration values. This MUST be in the form of a URL. The OpenID Connect standard requires the use of TLS. """ litestar-2.16.0/litestar/openapi/spec/server.py000066400000000000000000000020651500564371300215320ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from litestar.openapi.spec.base import BaseSchemaObject if TYPE_CHECKING: from litestar.openapi.spec.server_variable import ServerVariable __all__ = ("Server",) @dataclass class Server(BaseSchemaObject): """An object representing a Server.""" url: str """ **REQUIRED**. A URL to the target host. This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served. Variable substitutions will be made when a variable is named in ``{brackets}``. """ description: str | None = None """An optional string describing the host designated by the URL. `CommonMark syntax `_ MAY be used for rich text representation. """ variables: dict[str, ServerVariable] | None = None """A map between a variable name and its value. The value is used for substitution in the server's URL template.""" litestar-2.16.0/litestar/openapi/spec/server_variable.py000066400000000000000000000022231500564371300233730ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from litestar.openapi.spec.base import BaseSchemaObject __all__ = ("ServerVariable",) @dataclass class ServerVariable(BaseSchemaObject): """An object representing a Server Variable for server URL template substitution.""" default: str """**REQUIRED**. The default value to use for substitution, which SHALL be sent if an alternate value is _not_ supplied. Note this behavior is different than the `Schema Object's `_ treatment of default values, because in those cases parameter values are optional. If the `enum `_ is defined, the value MUST exist in the enum's values. """ enum: list[str] | None = None """An enumeration of string values to be used if the substitution options are from a limited set. The array SHOULD NOT be empty. """ description: str | None = None """An optional description for the server variable. `CommonMark syntax `_ MAY be used for rich text representation. """ litestar-2.16.0/litestar/openapi/spec/tag.py000066400000000000000000000016331500564371300207770ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING from litestar.openapi.spec.base import BaseSchemaObject if TYPE_CHECKING: from litestar.openapi.spec.external_documentation import ExternalDocumentation __all__ = ("Tag",) @dataclass class Tag(BaseSchemaObject): """Adds metadata to a single tag that is used by the `Operation Object `_. It is not mandatory to have a Tag Object per tag defined in the Operation Object instances. """ name: str """**REQUIRED**. The name of the tag.""" description: str | None = None """A short description for the tag. `CommonMark syntax `_ MAY be used for rich text representation. """ external_docs: ExternalDocumentation | None = None """Additional external documentation for this tag.""" litestar-2.16.0/litestar/openapi/spec/xml.py000066400000000000000000000032461500564371300210260ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from litestar.openapi.spec.base import BaseSchemaObject __all__ = ("XML",) @dataclass() class XML(BaseSchemaObject): """A metadata object that allows for more fine-tuned XML model definitions. When using arrays, XML element names are *not* inferred (for singular/plural forms) and the ``name`` property SHOULD be used to add that information. See examples for expected behavior. """ name: str | None = None """ Replaces the name of the element/attribute used for the described schema property. When defined within ``items``, it will affect the name of the individual XML elements within the list. When defined alongside ``type`` being ``array`` (outside the ``items``), it will affect the wrapping element and only if ``wrapped`` is ``True``. If ``wrapped`` is ``False``, it will be ignored. """ namespace: str | None = None """The URI of the namespace definition. Value MUST be in the form of an absolute URI.""" prefix: str | None = None """The prefix to be used for the `xmlName `_ """ attribute: bool = False """Declares whether the property definition translates to an attribute instead of an element. Default value is ``False``. """ wrapped: bool = False """ MAY be used only for an array definition. Signifies whether the array is wrapped (for example, ````) or unwrapped (````). Default value is ``False``. The definition takes effect only when defined alongside ``type`` being ``array`` (outside the ``items``). """ litestar-2.16.0/litestar/pagination.py000066400000000000000000000261651500564371300177770ustar00rootroot00000000000000# ruff: noqa: UP006,UP007 from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Generic, List, Optional, TypeVar from uuid import UUID __all__ = ( "AbstractAsyncClassicPaginator", "AbstractAsyncCursorPaginator", "AbstractAsyncOffsetPaginator", "AbstractSyncClassicPaginator", "AbstractSyncCursorPaginator", "AbstractSyncOffsetPaginator", "ClassicPagination", "CursorPagination", "OffsetPagination", ) T = TypeVar("T") C = TypeVar("C", int, str, UUID) @dataclass class ClassicPagination(Generic[T]): """Container for data returned using limit/offset pagination.""" __slots__ = ("current_page", "items", "page_size", "total_pages") items: List[T] """List of data being sent as part of the response.""" page_size: int """Number of items per page.""" current_page: int """Current page number.""" total_pages: int """Total number of pages.""" # AA requires it's own `OffsetPagination` class in versions greater that 0.9.0 # If we find it, use it. try: from advanced_alchemy.service import ( OffsetPagination, # pyright: ignore[reportMissingImports,reportGeneralTypeIssues] ) except ImportError: @dataclass class OffsetPagination(Generic[T]): # type: ignore[no-redef] """Container for data returned using limit/offset pagination.""" __slots__ = ("items", "limit", "offset", "total") items: List[T] """List of data being sent as part of the response.""" limit: int """Maximal number of items to send.""" offset: int """Offset from the beginning of the query. Identical to an index. """ total: int """Total number of items.""" @dataclass class CursorPagination(Generic[C, T]): """Container for data returned using cursor pagination.""" __slots__ = ("cursor", "items", "next_cursor", "results_per_page") items: List[T] """List of data being sent as part of the response.""" results_per_page: int """Maximal number of items to send.""" cursor: Optional[C] """Unique ID, designating the last identifier in the given data set. This value can be used to request the "next" batch of records. """ class AbstractSyncClassicPaginator(ABC, Generic[T]): """Base paginator class for sync classic pagination. Implement this class to return paginated result sets using the classic pagination scheme. """ @abstractmethod def get_total(self, page_size: int) -> int: """Return the total number of records. Args: page_size: Maximal number of records to return. Returns: An integer. """ raise NotImplementedError @abstractmethod def get_items(self, page_size: int, current_page: int) -> list[T]: """Return a list of items of the given size 'page_size' correlating with 'current_page'. Args: page_size: Maximal number of records to return. current_page: The current page of results to return. Returns: A list of items. """ raise NotImplementedError def __call__(self, page_size: int, current_page: int) -> ClassicPagination[T]: """Return a paginated result set. Args: page_size: Maximal number of records to return. current_page: The current page of results to return. Returns: A paginated result set. """ total_pages = self.get_total(page_size=page_size) items = self.get_items(page_size=page_size, current_page=current_page) return ClassicPagination[T]( items=items, total_pages=total_pages, page_size=page_size, current_page=current_page ) class AbstractAsyncClassicPaginator(ABC, Generic[T]): """Base paginator class for async classic pagination. Implement this class to return paginated result sets using the classic pagination scheme. """ @abstractmethod async def get_total(self, page_size: int) -> int: """Return the total number of records. Args: page_size: Maximal number of records to return. Returns: An integer. """ raise NotImplementedError @abstractmethod async def get_items(self, page_size: int, current_page: int) -> list[T]: """Return a list of items of the given size 'page_size' correlating with 'current_page'. Args: page_size: Maximal number of records to return. current_page: The current page of results to return. Returns: A list of items. """ raise NotImplementedError async def __call__(self, page_size: int, current_page: int) -> ClassicPagination[T]: """Return a paginated result set. Args: page_size: Maximal number of records to return. current_page: The current page of results to return. Returns: A paginated result set. """ total_pages = await self.get_total(page_size=page_size) items = await self.get_items(page_size=page_size, current_page=current_page) return ClassicPagination[T]( items=items, total_pages=total_pages, page_size=page_size, current_page=current_page ) class AbstractSyncOffsetPaginator(ABC, Generic[T]): """Base paginator class for limit / offset pagination. Implement this class to return paginated result sets using the limit / offset pagination scheme. """ @abstractmethod def get_total(self) -> int: """Return the total number of records. Returns: An integer. """ raise NotImplementedError @abstractmethod def get_items(self, limit: int, offset: int) -> list[T]: """Return a list of items of the given size 'limit' starting from position 'offset'. Args: limit: Maximal number of records to return. offset: Starting position within the result set (assume index 0 as starting position). Returns: A list of items. """ raise NotImplementedError def __call__(self, limit: int, offset: int) -> OffsetPagination[T]: """Return a paginated result set. Args: limit: Maximal number of records to return. offset: Starting position within the result set (assume index 0 as starting position). Returns: A paginated result set. """ total = self.get_total() items = self.get_items(limit=limit, offset=offset) return OffsetPagination[T](items=items, total=total, offset=offset, limit=limit) class AbstractAsyncOffsetPaginator(ABC, Generic[T]): """Base paginator class for limit / offset pagination. Implement this class to return paginated result sets using the limit / offset pagination scheme. """ @abstractmethod async def get_total(self) -> int: """Return the total number of records. Returns: An integer. """ raise NotImplementedError @abstractmethod async def get_items(self, limit: int, offset: int) -> list[T]: """Return a list of items of the given size 'limit' starting from position 'offset'. Args: limit: Maximal number of records to return. offset: Starting position within the result set (assume index 0 as starting position). Returns: A list of items. """ raise NotImplementedError async def __call__(self, limit: int, offset: int) -> OffsetPagination[T]: """Return a paginated result set. Args: limit: Maximal number of records to return. offset: Starting position within the result set (assume index 0 as starting position). Returns: A paginated result set. """ total = await self.get_total() items = await self.get_items(limit=limit, offset=offset) return OffsetPagination[T](items=items, total=total, offset=offset, limit=limit) class AbstractSyncCursorPaginator(ABC, Generic[C, T]): """Base paginator class for sync cursor pagination. Implement this class to return paginated result sets using the cursor pagination scheme. """ @abstractmethod def get_items(self, cursor: C | None, results_per_page: int) -> tuple[list[T], C | None]: """Return a list of items of the size 'results_per_page' following the given cursor, if any, Args: cursor: A unique identifier that acts as the 'cursor' after which results should be given. results_per_page: A maximal number of results to return. Returns: A tuple containing the result set and a new cursor that marks the last record retrieved. The new cursor can be used to ask for the 'next_cursor' batch of results. """ raise NotImplementedError def __call__(self, cursor: C | None, results_per_page: int) -> CursorPagination[C, T]: """Return a paginated result set given an optional cursor (unique ID) and a maximal number of results to return. Args: cursor: A unique identifier that acts as the 'cursor' after which results should be given. results_per_page: A maximal number of results to return. Returns: A paginated result set. """ items, new_cursor = self.get_items(cursor=cursor, results_per_page=results_per_page) return CursorPagination[C, T]( items=items, results_per_page=results_per_page, cursor=new_cursor, ) class AbstractAsyncCursorPaginator(ABC, Generic[C, T]): """Base paginator class for async cursor pagination. Implement this class to return paginated result sets using the cursor pagination scheme. """ @abstractmethod async def get_items(self, cursor: C | None, results_per_page: int) -> tuple[list[T], C | None]: """Return a list of items of the size 'results_per_page' following the given cursor, if any, Args: cursor: A unique identifier that acts as the 'cursor' after which results should be given. results_per_page: A maximal number of results to return. Returns: A tuple containing the result set and a new cursor that marks the last record retrieved. The new cursor can be used to ask for the 'next_cursor' batch of results. """ raise NotImplementedError async def __call__(self, cursor: C | None, results_per_page: int) -> CursorPagination[C, T]: """Return a paginated result set given an optional cursor (unique ID) and a maximal number of results to return. Args: cursor: A unique identifier that acts as the 'cursor' after which results should be given. results_per_page: A maximal number of results to return. Returns: A paginated result set. """ items, new_cursor = await self.get_items(cursor=cursor, results_per_page=results_per_page) return CursorPagination[C, T]( items=items, results_per_page=results_per_page, cursor=new_cursor, ) litestar-2.16.0/litestar/params.py000066400000000000000000000403451500564371300171250ustar00rootroot00000000000000from __future__ import annotations from dataclasses import asdict, dataclass, field from typing import TYPE_CHECKING, Any, Hashable, Sequence from litestar.enums import RequestEncodingType from litestar.types import Empty __all__ = ( "Body", "BodyKwarg", "Dependency", "DependencyKwarg", "KwargDefinition", "Parameter", "ParameterKwarg", ) if TYPE_CHECKING: from litestar.openapi.spec.example import Example from litestar.openapi.spec.external_documentation import ( ExternalDocumentation, ) @dataclass(frozen=True) class KwargDefinition: """Data container representing a constrained kwarg.""" examples: list[Example] | None = field(default=None) """A list of Example models.""" external_docs: ExternalDocumentation | None = field(default=None) """A url pointing at external documentation for the given parameter.""" content_encoding: str | None = field(default=None) """The content encoding of the value. Applicable on to string values. See OpenAPI 3.1 for details. """ default: Any = field(default=Empty) """A default value. If const is true, this value is required. """ title: str | None = field(default=None) """String value used in the title section of the OpenAPI schema for the given parameter.""" description: str | None = field(default=None) """String value used in the description section of the OpenAPI schema for the given parameter.""" const: bool | None = field(default=None) """A boolean flag dictating whether this parameter is a constant. If True, the value passed to the parameter must equal its default value. This also causes the OpenAPI const field to be populated with the default value. """ gt: float | None = field(default=None) """Constrict value to be greater than a given float or int. Equivalent to exclusiveMinimum in the OpenAPI specification. """ ge: float | None = field(default=None) """Constrict value to be greater or equal to a given float or int. Equivalent to minimum in the OpenAPI specification. """ lt: float | None = field(default=None) """Constrict value to be less than a given float or int. Equivalent to exclusiveMaximum in the OpenAPI specification. """ le: float | None = field(default=None) """Constrict value to be less or equal to a given float or int. Equivalent to maximum in the OpenAPI specification. """ multiple_of: float | None = field(default=None) """Constrict value to a multiple of a given float or int. Equivalent to multipleOf in the OpenAPI specification. """ min_items: int | None = field(default=None) """Constrict a set or a list to have a minimum number of items. Equivalent to minItems in the OpenAPI specification. """ max_items: int | None = field(default=None) """Constrict a set or a list to have a maximum number of items. Equivalent to maxItems in the OpenAPI specification. """ min_length: int | None = field(default=None) """Constrict a string or bytes value to have a minimum length. Equivalent to minLength in the OpenAPI specification. """ max_length: int | None = field(default=None) """Constrict a string or bytes value to have a maximum length. Equivalent to maxLength in the OpenAPI specification. """ pattern: str | None = field(default=None) """A string representing a regex against which the given string will be matched. Equivalent to pattern in the OpenAPI specification. """ lower_case: bool | None = field(default=None) """Constrict a string value to be lower case.""" upper_case: bool | None = field(default=None) """Constrict a string value to be upper case.""" format: str | None = field(default=None) """Specify the format to which a string value should be converted.""" enum: Sequence[Any] | None = field(default=None) """A sequence of valid values.""" read_only: bool | None = field(default=None) """A boolean flag dictating whether this parameter is read only.""" schema_extra: dict[str, Any] | None = field(default=None) """Extensions to the generated schema. If set, will overwrite the matching fields in the generated schema. .. versionadded:: 2.8.0 """ schema_component_key: str | None = None """ Use as the key for the reference when creating a component for this type .. versionadded:: 2.12.0 """ @property def is_constrained(self) -> bool: """Return True if any of the constraints are set.""" return any( attr if attr and attr is not Empty else False # type: ignore[comparison-overlap] for attr in ( self.gt, self.ge, self.lt, self.le, self.multiple_of, self.min_items, self.max_items, self.min_length, self.max_length, self.pattern, self.const, self.lower_case, self.upper_case, ) ) @dataclass(frozen=True) class ParameterKwarg(KwargDefinition): """Data container representing a parameter.""" annotation: Any = field(default=Empty) """The field value - `Empty` by default.""" header: str | None = field(default=None) """The header parameter key - required for header parameters.""" cookie: str | None = field(default=None) """The cookie parameter key - required for cookie parameters.""" query: str | None = field(default=None) """The query parameter key for this parameter.""" required: bool | None = field(default=None) """A boolean flag dictating whether this parameter is required. If set to False, None values will be allowed. Defaults to True. """ def __hash__(self) -> int: # pragma: no cover """Hash the dataclass in a safe way. Returns: A hash """ return sum(hash(v) for v in asdict(self) if isinstance(v, Hashable)) def Parameter( annotation: Any = Empty, *, const: bool | None = None, content_encoding: str | None = None, cookie: str | None = None, default: Any = Empty, description: str | None = None, examples: list[Example] | None = None, external_docs: ExternalDocumentation | None = None, ge: float | None = None, gt: float | None = None, header: str | None = None, le: float | None = None, lt: float | None = None, max_items: int | None = None, max_length: int | None = None, min_items: int | None = None, min_length: int | None = None, multiple_of: float | None = None, pattern: str | None = None, query: str | None = None, required: bool | None = None, title: str | None = None, schema_extra: dict[str, Any] | None = None, schema_component_key: str | None = None, ) -> Any: """Create an extended parameter kwarg definition. Args: annotation: `Empty` by default. const: A boolean flag dictating whether this parameter is a constant. If True, the value passed to the parameter must equal its default value. This also causes the OpenAPI const field to be populated with the default value. content_encoding: The content encoding of the value. Applicable on to string values. See OpenAPI 3.1 for details. cookie: The cookie parameter key - required for cookie parameters. default: A default value. If const is true, this value is required. description: String value used in the description section of the OpenAPI schema for the given parameter. examples: A list of Example models. external_docs: A url pointing at external documentation for the given parameter. ge: Constrict value to be greater or equal to a given float or int. Equivalent to minimum in the OpenAPI specification. gt: Constrict value to be greater than a given float or int. Equivalent to exclusiveMinimum in the OpenAPI specification. header: The header parameter key - required for header parameters. le: Constrict value to be less or equal to a given float or int. Equivalent to maximum in the OpenAPI specification. lt: Constrict value to be less than a given float or int. Equivalent to exclusiveMaximum in the OpenAPI specification. max_items: Constrict a set or a list to have a maximum number of items. Equivalent to maxItems in the OpenAPI specification. max_length: Constrict a string or bytes value to have a maximum length. Equivalent to maxLength in the OpenAPI specification. min_items: Constrict a set or a list to have a minimum number of items. ֿ Equivalent to minItems in the OpenAPI specification. min_length: Constrict a string or bytes value to have a minimum length. Equivalent to minLength in the OpenAPI specification. multiple_of: Constrict value to a multiple of a given float or int. Equivalent to multipleOf in the OpenAPI specification. pattern: A string representing a regex against which the given string will be matched. Equivalent to pattern in the OpenAPI specification. query: The query parameter key for this parameter. required: A boolean flag dictating whether this parameter is required. If set to False, None values will be allowed. Defaults to True. title: String value used in the title section of the OpenAPI schema for the given parameter. schema_extra: Extensions to the generated schema. If set, will overwrite the matching fields in the generated schema. .. versionadded:: 2.8.0 schema_component_key: Use this as the key for the reference when creating a component for this type .. versionadded:: 2.12.0 """ return ParameterKwarg( annotation=annotation, header=header, cookie=cookie, query=query, examples=examples, external_docs=external_docs, content_encoding=content_encoding, required=required, default=default, title=title, description=description, const=const, gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of, min_items=min_items, max_items=max_items, min_length=min_length, max_length=max_length, pattern=pattern, schema_extra=schema_extra, schema_component_key=schema_component_key, ) @dataclass(frozen=True) class BodyKwarg(KwargDefinition): """Data container representing a request body.""" media_type: str | RequestEncodingType = field(default=RequestEncodingType.JSON) """Media-Type of the body.""" multipart_form_part_limit: int | None = field(default=None) """The maximal number of allowed parts in a multipart/formdata request. This limit is intended to protect from DoS attacks.""" def __hash__(self) -> int: # pragma: no cover """Hash the dataclass in a safe way. Returns: A hash """ return sum(hash(v) for v in asdict(self) if isinstance(v, Hashable)) def Body( *, const: bool | None = None, content_encoding: str | None = None, default: Any = Empty, description: str | None = None, examples: list[Example] | None = None, external_docs: ExternalDocumentation | None = None, ge: float | None = None, gt: float | None = None, le: float | None = None, lt: float | None = None, max_items: int | None = None, max_length: int | None = None, media_type: str | RequestEncodingType = RequestEncodingType.JSON, min_items: int | None = None, min_length: int | None = None, multipart_form_part_limit: int | None = None, multiple_of: float | None = None, pattern: str | None = None, title: str | None = None, schema_extra: dict[str, Any] | None = None, schema_component_key: str | None = None, ) -> Any: """Create an extended request body kwarg definition. Args: const: A boolean flag dictating whether this parameter is a constant. If True, the value passed to the parameter must equal its default value. This also causes the OpenAPI const field to be populated with the default value. content_encoding: The content encoding of the value. Applicable on to string values. See OpenAPI 3.1 for details. default: A default value. If const is true, this value is required. description: String value used in the description section of the OpenAPI schema for the given parameter. examples: A list of Example models. external_docs: A url pointing at external documentation for the given parameter. ge: Constrict value to be greater or equal to a given float or int. Equivalent to minimum in the OpenAPI specification. gt: Constrict value to be greater than a given float or int. Equivalent to exclusiveMinimum in the OpenAPI specification. le: Constrict value to be less or equal to a given float or int. Equivalent to maximum in the OpenAPI specification. lt: Constrict value to be less than a given float or int. Equivalent to exclusiveMaximum in the OpenAPI specification. max_items: Constrict a set or a list to have a maximum number of items. Equivalent to maxItems in the OpenAPI specification. max_length: Constrict a string or bytes value to have a maximum length. Equivalent to maxLength in the OpenAPI specification. media_type: Defaults to RequestEncodingType.JSON. min_items: Constrict a set or a list to have a minimum number of items. Equivalent to minItems in the OpenAPI specification. min_length: Constrict a string or bytes value to have a minimum length. Equivalent to minLength in the OpenAPI specification. multipart_form_part_limit: The maximal number of allowed parts in a multipart/formdata request. This limit is intended to protect from DoS attacks. multiple_of: Constrict value to a multiple of a given float or int. Equivalent to multipleOf in the OpenAPI specification. pattern: A string representing a regex against which the given string will be matched. Equivalent to pattern in the OpenAPI specification. title: String value used in the title section of the OpenAPI schema for the given parameter. schema_extra: Extensions to the generated schema. If set, will overwrite the matching fields in the generated schema. .. versionadded:: 2.8.0 schema_component_key: Use this as the key for the reference when creating a component for this type .. versionadded:: 2.12.0 """ return BodyKwarg( media_type=media_type, examples=examples, external_docs=external_docs, content_encoding=content_encoding, default=default, title=title, description=description, const=const, gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of, min_items=min_items, max_items=max_items, min_length=min_length, max_length=max_length, pattern=pattern, multipart_form_part_limit=multipart_form_part_limit, schema_extra=schema_extra, schema_component_key=schema_component_key, ) @dataclass(frozen=True) class DependencyKwarg: """Data container representing a dependency.""" default: Any = field(default=Empty) """A default value.""" skip_validation: bool = field(default=False) """Flag dictating whether to skip validation.""" def __hash__(self) -> int: """Hash the dataclass in a safe way. Returns: A hash """ return sum(hash(v) for v in asdict(self) if isinstance(v, Hashable)) def Dependency(*, default: Any = Empty, skip_validation: bool = False) -> Any: """Create a dependency kwarg definition. Args: default: A default value to use in case a dependency is not provided. skip_validation: If `True` provided dependency values are not validated by signature model. """ return DependencyKwarg(default=default, skip_validation=skip_validation) litestar-2.16.0/litestar/plugins/000077500000000000000000000000001500564371300167435ustar00rootroot00000000000000litestar-2.16.0/litestar/plugins/__init__.py000066400000000000000000000010731500564371300210550ustar00rootroot00000000000000from litestar.plugins.base import ( CLIPlugin, CLIPluginProtocol, DIPlugin, InitPlugin, InitPluginProtocol, OpenAPISchemaPlugin, OpenAPISchemaPluginProtocol, PluginProtocol, PluginRegistry, SerializationPlugin, SerializationPluginProtocol, ) __all__ = ( "CLIPlugin", "CLIPluginProtocol", "DIPlugin", "InitPlugin", "InitPluginProtocol", "OpenAPISchemaPlugin", "OpenAPISchemaPluginProtocol", "PluginProtocol", "PluginRegistry", "SerializationPlugin", "SerializationPluginProtocol", ) litestar-2.16.0/litestar/plugins/attrs.py000066400000000000000000000045151500564371300204570ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from typing_extensions import TypeGuard from litestar.exceptions import MissingDependencyException from litestar.plugins import OpenAPISchemaPluginProtocol from litestar.types import Empty from litestar.typing import FieldDefinition from litestar.utils import is_optional_union try: import attr import attrs except ImportError as e: raise MissingDependencyException("attrs") from e if TYPE_CHECKING: from litestar._openapi.schema_generation import SchemaCreator from litestar.openapi.spec import Schema __all__ = ("AttrsSchemaPlugin", "is_attrs_class") class AttrsSchemaPlugin(OpenAPISchemaPluginProtocol): @staticmethod def is_plugin_supported_type(value: Any) -> bool: return is_attrs_class(value) or is_attrs_class(type(value)) def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: """Given a type annotation, transform it into an OpenAPI schema class. Args: field_definition: FieldDefinition instance. schema_creator: An instance of the schema creator class Returns: An :class:`OpenAPI ` instance. """ type_hints = field_definition.get_type_hints(include_extras=True, resolve_generics=True) attr_fields = attr.fields_dict(field_definition.type_) return schema_creator.create_component_schema( field_definition, required=sorted( field_name for field_name, attribute in attr_fields.items() if attribute.default is attrs.NOTHING and not is_optional_union(type_hints[field_name]) ), property_fields={ field_name: FieldDefinition.from_kwarg(type_hints[field_name], field_name) for field_name in attr_fields }, ) def is_attrs_class(annotation: Any) -> TypeGuard[type[attrs.AttrsInstance]]: # pyright: ignore """Given a type annotation determine if the annotation is a class that includes an attrs attribute. Args: annotation: A type. Returns: A typeguard determining whether the type is an attrs class. """ return attrs.has(annotation) if attrs is not Empty else False # type: ignore[comparison-overlap] litestar-2.16.0/litestar/plugins/base.py000066400000000000000000000320001500564371300202220ustar00rootroot00000000000000from __future__ import annotations import abc from contextlib import contextmanager from typing import TYPE_CHECKING, Any, Iterator, Protocol, TypeVar, Union, cast, runtime_checkable if TYPE_CHECKING: from inspect import Signature from click import Group from litestar._openapi.schema_generation import SchemaCreator from litestar.app import Litestar from litestar.config.app import AppConfig from litestar.dto import AbstractDTO from litestar.openapi.spec import Schema from litestar.routes import BaseRoute from litestar.typing import FieldDefinition __all__ = ( "CLIPlugin", "CLIPluginProtocol", "DIPlugin", "InitPlugin", "InitPluginProtocol", "OpenAPISchemaPlugin", "OpenAPISchemaPluginProtocol", "PluginProtocol", "PluginRegistry", "SerializationPlugin", "SerializationPluginProtocol", ) @runtime_checkable class InitPluginProtocol(Protocol): """Protocol used to define plugins that affect the application's init process. .. deprecated:: 2.15 Use 'InitPlugin' instead """ __slots__ = () def on_app_init(self, app_config: AppConfig) -> AppConfig: """Receive the :class:`AppConfig<.config.app.AppConfig>` instance after `on_app_init` hooks have been called. Examples: .. code-block:: python from litestar import Litestar, get from litestar.di import Provide from litestar.plugins import InitPluginProtocol def get_name() -> str: return "world" @get("/my-path") def my_route_handler(name: str) -> dict[str, str]: return {"hello": name} class MyPlugin(InitPluginProtocol): def on_app_init(self, app_config: AppConfig) -> AppConfig: app_config.dependencies["name"] = Provide(get_name) app_config.route_handlers.append(my_route_handler) return app_config app = Litestar(plugins=[MyPlugin()]) Args: app_config: The :class:`AppConfig ` instance. Returns: The app config object. """ return app_config # pragma: no cover class InitPlugin(InitPluginProtocol): """Protocol used to define plugins that affect the application's init process.""" __slots__ = () def on_app_init(self, app_config: AppConfig) -> AppConfig: """Receive the :class:`AppConfig<.config.app.AppConfig>` instance after `on_app_init` hooks have been called. Examples: .. code-block:: python from litestar import Litestar, get from litestar.di import Provide from litestar.plugins import InitPluginProtocol def get_name() -> str: return "world" @get("/my-path") def my_route_handler(name: str) -> dict[str, str]: return {"hello": name} class MyPlugin(InitPluginProtocol): def on_app_init(self, app_config: AppConfig) -> AppConfig: app_config.dependencies["name"] = Provide(get_name) app_config.route_handlers.append(my_route_handler) return app_config app = Litestar(plugins=[MyPlugin()]) Args: app_config: The :class:`AppConfig ` instance. Returns: The app config object. """ return app_config # pragma: no cover class ReceiveRoutePlugin: """Receive routes as they are added to the application.""" __slots__ = () def receive_route(self, route: BaseRoute) -> None: """Receive routes as they are registered on an application.""" @runtime_checkable class CLIPluginProtocol(Protocol): """Plugin protocol to extend the CLI.""" __slots__ = () def on_cli_init(self, cli: Group) -> None: """Called when the CLI is initialized. This can be used to extend or override existing commands. Args: cli: The root :class:`click.Group` of the Litestar CLI Examples: .. code-block:: python from litestar import Litestar from litestar.plugins import CLIPluginProtocol from click import Group class CLIPlugin(CLIPluginProtocol): def on_cli_init(self, cli: Group) -> None: @cli.command() def is_debug_mode(app: Litestar): print(app.debug) app = Litestar(plugins=[CLIPlugin()]) """ class CLIPlugin(CLIPluginProtocol): """Plugin protocol to extend the CLI Server Lifespan.""" __slots__ = () @contextmanager def server_lifespan(self, app: Litestar) -> Iterator[None]: yield @runtime_checkable class SerializationPluginProtocol(Protocol): """Protocol used to define a serialization plugin for DTOs. .. deprecated:: 2.15 Use 'litestar.plugins.SerializationPluginProtocol' instead """ __slots__ = () def supports_type(self, field_definition: FieldDefinition) -> bool: """Given a value of indeterminate type, determine if this value is supported by the plugin. Args: field_definition: A parsed type. Returns: Whether the type is supported by the plugin. """ raise NotImplementedError() def create_dto_for_type(self, field_definition: FieldDefinition) -> type[AbstractDTO]: """Given a parsed type, create a DTO class. Args: field_definition: A parsed type. Returns: A DTO class. """ raise NotImplementedError() class SerializationPlugin(SerializationPluginProtocol, abc.ABC): """Abstract base class for plugins that extend DTO functionality""" @abc.abstractmethod def supports_type(self, field_definition: FieldDefinition) -> bool: """Given a value of indeterminate type, determine if this value is supported by the plugin. Args: field_definition: A parsed type. Returns: Whether the type is supported by the plugin. """ raise NotImplementedError() @abc.abstractmethod def create_dto_for_type(self, field_definition: FieldDefinition) -> type[AbstractDTO]: """Given a parsed type, create a DTO class. Args: field_definition: A parsed type. Returns: A DTO class. """ raise NotImplementedError() class DIPlugin(abc.ABC): """Extend dependency injection""" @abc.abstractmethod def has_typed_init(self, type_: Any) -> bool: """Return ``True`` if ``type_`` has type information available for its :func:`__init__` method that cannot be extracted from this method's type annotations (e.g. a Pydantic BaseModel subclass), and :meth:`DIPlugin.get_typed_init` supports extraction of these annotations. """ ... @abc.abstractmethod def get_typed_init(self, type_: Any) -> tuple[Signature, dict[str, Any]]: r"""Return signature and type information about the ``type_``\ s :func:`__init__` method. """ ... @runtime_checkable class OpenAPISchemaPluginProtocol(Protocol): """Plugin protocol to extend the support of OpenAPI schema generation for non-library types.""" __slots__ = () @staticmethod def is_plugin_supported_type(value: Any) -> bool: """Given a value of indeterminate type, determine if this value is supported by the plugin. Args: value: An arbitrary value. Returns: A typeguard dictating whether the value is supported by the plugin. """ raise NotImplementedError() def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: """Given a type annotation, transform it into an OpenAPI schema class. Args: field_definition: An :class:`OpenAPI ` instance. schema_creator: An instance of the openapi SchemaCreator. Returns: An :class:`OpenAPI ` instance. """ raise NotImplementedError() class OpenAPISchemaPlugin(OpenAPISchemaPluginProtocol): """Plugin to extend the support of OpenAPI schema generation for non-library types.""" __slots__ = () @staticmethod def is_plugin_supported_type(value: Any) -> bool: """Given a value of indeterminate type, determine if this value is supported by the plugin. This is called by the default implementation of :meth:`is_plugin_supported_field` for backwards compatibility. User's should prefer to override that method instead. Args: value: An arbitrary value. Returns: A bool indicating whether the value is supported by the plugin. """ raise NotImplementedError( "One of either is_plugin_supported_type or is_plugin_supported_field should be defined. " "The default implementation of is_plugin_supported_field calls is_plugin_supported_type " "for backwards compatibility. Users should prefer to override is_plugin_supported_field " "as it receives a 'FieldDefinition' instance which is more useful than a raw type." ) def is_plugin_supported_field(self, field_definition: FieldDefinition) -> bool: """Given a :class:`FieldDefinition ` that represents an indeterminate type, determine if this value is supported by the plugin Args: field_definition: A parsed type. Returns: Whether the type is supported by the plugin. """ return self.is_plugin_supported_type(field_definition.annotation) @staticmethod def is_undefined_sentinel(value: Any) -> bool: """Return ``True`` if ``value`` should be treated as an undefined field""" return False @staticmethod def is_constrained_field(field_definition: FieldDefinition) -> bool: """Return ``True`` if the field should be treated as constrained. If returning ``True``, constraints should be defined in the field's extras """ return False PluginProtocol = Union[ CLIPlugin, CLIPluginProtocol, InitPluginProtocol, OpenAPISchemaPlugin, OpenAPISchemaPluginProtocol, ReceiveRoutePlugin, SerializationPluginProtocol, DIPlugin, ] PluginT = TypeVar("PluginT", bound=PluginProtocol) class PluginRegistry: __slots__ = { # noqa: RUF023 "init": "Plugins that implement the InitPluginProtocol", "openapi": "Plugins that implement the OpenAPISchemaPluginProtocol", "receive_route": "ReceiveRoutePlugin instances", "serialization": "Plugins that implement the SerializationPluginProtocol", "cli": "Plugins that implement the CLIPluginProtocol", "di": "DIPlugin instances", "_plugins_by_type": None, "_plugins": None, "_get_plugins_of_type": None, } def __init__(self, plugins: list[PluginProtocol]) -> None: self._plugins_by_type = {type(p): p for p in plugins} self._plugins = frozenset(plugins) self.init = tuple(p for p in plugins if isinstance(p, InitPluginProtocol)) self.openapi = tuple(p for p in plugins if isinstance(p, OpenAPISchemaPluginProtocol)) self.receive_route = tuple(p for p in plugins if isinstance(p, ReceiveRoutePlugin)) self.serialization = tuple(p for p in plugins if isinstance(p, SerializationPluginProtocol)) self.cli = tuple(p for p in plugins if isinstance(p, CLIPluginProtocol)) self.di = tuple(p for p in plugins if isinstance(p, DIPlugin)) def get(self, type_: type[PluginT] | str) -> PluginT: """Return the registered plugin of ``type_``. This should be used with subclasses of the plugin protocols. """ if isinstance(type_, str): for plugin in self._plugins: _name = plugin.__class__.__name__ _module = plugin.__class__.__module__ _qualname = ( f"{_module}.{plugin.__class__.__qualname__}" if _module is not None and _module != "__builtin__" else plugin.__class__.__qualname__ ) if type_ in {_name, _qualname}: return cast(PluginT, plugin) raise KeyError(f"No plugin of type {type_!r} registered") try: return cast(PluginT, self._plugins_by_type[type_]) # type: ignore[index] except KeyError as e: raise KeyError(f"No plugin of type {type_.__name__!r} registered") from e def __iter__(self) -> Iterator[PluginProtocol]: return iter(self._plugins) def __contains__(self, item: PluginProtocol) -> bool: return item in self._plugins litestar-2.16.0/litestar/plugins/core/000077500000000000000000000000001500564371300176735ustar00rootroot00000000000000litestar-2.16.0/litestar/plugins/core/__init__.py000066400000000000000000000001061500564371300220010ustar00rootroot00000000000000from ._msgspec import MsgspecDIPlugin __all__ = ("MsgspecDIPlugin",) litestar-2.16.0/litestar/plugins/core/_msgspec.py000066400000000000000000000056411500564371300220530ustar00rootroot00000000000000from __future__ import annotations import dataclasses import inspect from inspect import Signature from typing import Any import msgspec from litestar.openapi.spec import Example from litestar.params import ParameterKwarg from litestar.plugins import DIPlugin __all__ = ("MsgspecDIPlugin", "kwarg_definition_from_field") class MsgspecDIPlugin(DIPlugin): def has_typed_init(self, type_: Any) -> bool: return type(type_) is type(msgspec.Struct) def get_typed_init(self, type_: Any) -> tuple[Signature, dict[str, Any]]: parameters = [] type_hints = {} for field_info in msgspec.structs.fields(type_): type_hints[field_info.name] = field_info.type parameters.append( inspect.Parameter( name=field_info.name, kind=inspect.Parameter.KEYWORD_ONLY, annotation=field_info.type, default=field_info.default, ) ) return inspect.Signature(parameters), type_hints def kwarg_definition_from_field(field: msgspec.inspect.Field) -> tuple[ParameterKwarg | None, dict[str, Any]]: extra: dict[str, Any] = {} kwargs: dict[str, Any] = {} if isinstance(field.type, msgspec.inspect.Metadata): meta = field.type field_type = meta.type if extra_json_schema := meta.extra_json_schema: kwargs["title"] = extra_json_schema.get("title") kwargs["description"] = extra_json_schema.get("description") if examples := extra_json_schema.get("examples"): kwargs["examples"] = [Example(value=e) for e in examples] kwargs["schema_extra"] = extra_json_schema.get("extra") extra = meta.extra or {} else: field_type = field.type if isinstance( field_type, ( msgspec.inspect.IntType, msgspec.inspect.FloatType, ), ): kwargs["gt"] = field_type.gt kwargs["ge"] = field_type.ge kwargs["lt"] = field_type.lt kwargs["le"] = field_type.le kwargs["multiple_of"] = field_type.multiple_of elif isinstance( field_type, ( msgspec.inspect.StrType, msgspec.inspect.BytesType, msgspec.inspect.ByteArrayType, msgspec.inspect.MemoryViewType, ), ): kwargs["min_length"] = field_type.min_length kwargs["max_length"] = field_type.max_length if isinstance(field_type, msgspec.inspect.StrType): kwargs["pattern"] = field_type.pattern parameter_defaults = { f.name: default for f in dataclasses.fields(ParameterKwarg) if (default := f.default) is not dataclasses.MISSING } kwargs_without_defaults = {k: v for k, v in kwargs.items() if v != parameter_defaults[k]} if kwargs_without_defaults: return ParameterKwarg(**kwargs_without_defaults), extra return None, extra litestar-2.16.0/litestar/plugins/flash.py000066400000000000000000000054021500564371300204130ustar00rootroot00000000000000"""Plugin for creating and retrieving flash messages.""" from __future__ import annotations from contextlib import suppress from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Mapping import litestar.exceptions from litestar.exceptions import MissingDependencyException from litestar.middleware import DefineMiddleware from litestar.middleware.session import SessionMiddleware from litestar.plugins import InitPlugin from litestar.security.session_auth.middleware import MiddlewareWrapper from litestar.template.base import _get_request_from_context from litestar.utils.predicates import is_class_and_subclass if TYPE_CHECKING: from collections.abc import Callable from litestar.config.app import AppConfig from litestar.connection.base import ASGIConnection from litestar.template import TemplateConfig @dataclass class FlashConfig: """Configuration for Flash messages.""" template_config: TemplateConfig class FlashPlugin(InitPlugin): """Flash messages Plugin.""" def __init__(self, config: FlashConfig): """Initialize the plugin. Args: config: Configuration for flash messages, including the template engine instance. """ self.config = config def on_app_init(self, app_config: AppConfig) -> AppConfig: """Register the message callable on the template engine instance. Args: app_config: The application configuration. Returns: The application configuration with the message callable registered. """ for mw in app_config.middleware: if isinstance(mw, DefineMiddleware) and is_class_and_subclass( mw.middleware, (MiddlewareWrapper, SessionMiddleware) ): break else: raise litestar.exceptions.ImproperlyConfiguredException("Flash messages require a session middleware.") template_callable: Callable[[Any], Any] = get_flashes with suppress(MissingDependencyException): from litestar.contrib.minijinja import MiniJinjaTemplateEngine, _transform_state if isinstance(self.config.template_config.engine_instance, MiniJinjaTemplateEngine): template_callable = _transform_state(get_flashes) self.config.template_config.engine_instance.register_template_callable("get_flashes", template_callable) # pyright: ignore[reportGeneralTypeIssues] return app_config def flash( request: ASGIConnection[Any, Any, Any, Any], message: Any, category: str, ) -> None: request.session.setdefault("_messages", []).append({"message": message, "category": category}) def get_flashes(context: Mapping[str, Any]) -> Any: return _get_request_from_context(context).session.pop("_messages", []) litestar-2.16.0/litestar/plugins/htmx.py000066400000000000000000000016511500564371300203000ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # pyright: reportUnusedImport=false from __future__ import annotations from litestar_htmx import ( ClientRedirect, ClientRefresh, EventAfterType, HTMXConfig, HTMXDetails, HTMXHeaders, HtmxHeaderType, HTMXPlugin, HTMXRequest, HTMXTemplate, HXLocation, HXStopPolling, LocationType, PushUrl, PushUrlType, ReplaceUrl, Reswap, ReSwapMethod, Retarget, TriggerEvent, TriggerEventType, _utils, ) __all__ = ( "ClientRedirect", "ClientRefresh", "EventAfterType", "HTMXConfig", "HTMXDetails", "HTMXHeaders", "HTMXPlugin", "HTMXRequest", "HTMXTemplate", "HXLocation", "HXStopPolling", "HtmxHeaderType", "LocationType", "PushUrl", "PushUrlType", "ReSwapMethod", "ReplaceUrl", "Reswap", "Retarget", "TriggerEvent", "TriggerEventType", "_utils", ) litestar-2.16.0/litestar/plugins/problem_details.py000066400000000000000000000133141500564371300224640ustar00rootroot00000000000000"""Plugin for converting exceptions into a problem details response.""" from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable, Mapping, TypeVar from typing_extensions import TypeAlias from litestar.exceptions.http_exceptions import HTTPException from litestar.plugins.base import InitPlugin from litestar.response.base import Response if TYPE_CHECKING: from litestar.config.app import AppConfig from litestar.connection.request import Request from litestar.types.callable_types import ExceptionHandler, ExceptionT ProblemDetailsExceptionT = TypeVar("ProblemDetailsExceptionT", bound="ProblemDetailsException") ProblemDetailsExceptionHandlerType: TypeAlias = "Callable[[Request, ProblemDetailsExceptionT], Response]" ExceptionToProblemDetailMapType: TypeAlias = ( "Mapping[type[ExceptionT], Callable[[ExceptionT], ProblemDetailsExceptionT]]" ) def _problem_details_exception_handler(request: Request[Any, Any, Any], exc: ProblemDetailsException) -> Response[Any]: return exc.to_response(request) def _create_exception_handler( exc_to_problem_details_exc_fn: Callable[[ExceptionT], ProblemDetailsException], exc_type: type[ExceptionT] ) -> ExceptionHandler[ExceptionT]: def _exception_handler(req: Request, exc: exc_type) -> Response: # type: ignore[valid-type] problem_details_exc = exc_to_problem_details_exc_fn(exc) return problem_details_exc.to_response(req) return _exception_handler def _http_exception_to_problem_detail_exception(exc: HTTPException) -> ProblemDetailsException: return ProblemDetailsException( status_code=exc.status_code, title=exc.detail, extra=exc.extra, headers=exc.headers, ) class ProblemDetailsException(HTTPException): """A problem details exception as per RFC 9457.""" _PROBLEM_DETAILS_MEDIA_TYPE = "application/problem+json" def __init__( self, *args: Any, detail: str = "", status_code: int | None = None, headers: dict[str, str] | None = None, extra: dict[str, Any] | list[Any] | None = None, type_: str | None = None, title: str | None = None, instance: str | None = None, ) -> None: """Initialize ``ProblemDetailsException``. Args: *args: if ``detail`` kwarg not provided, first arg should be error detail. detail: Exception details or message. Will default to args[0] if not provided. status_code: Exception HTTP status code. headers: Headers to set on the response. extra: An extra mapping to attach to the exception. type_: The type field in the problem details. title: The title field in the problem details. instance: The instance field in the problem details. """ super().__init__( *args, detail=detail, status_code=status_code, headers=headers, extra=extra, ) self.type_ = type_ self.title = title self.instance = instance def to_response(self, request: Request[Any, Any, Any]) -> Response[dict[str, Any]]: """Convert the problem details exception into a ``Response.``""" problem_details: dict[str, Any] = {"status": self.status_code} if self.type_ is not None: problem_details["type"] = self.type_ if self.title is not None: problem_details["title"] = self.title if self.instance is not None: problem_details["instance"] = self.instance if self.detail is not None: problem_details["detail"] = self.detail if extra := self.extra: if isinstance(extra, Mapping): problem_details.update(extra) else: problem_details["extra"] = extra return Response( problem_details, headers=self.headers, media_type=self._PROBLEM_DETAILS_MEDIA_TYPE, status_code=self.status_code, ) @dataclass class ProblemDetailsConfig: """The configuration object for ``ProblemDetailsPlugin.``""" exception_handler: ProblemDetailsExceptionHandlerType = _problem_details_exception_handler """The exception handler used for ``ProblemdetailsException.``""" enable_for_all_http_exceptions: bool = False """Flag indicating whether to convert all :exc:`HTTPException` into ``ProblemDetailsException.``""" exception_to_problem_detail_map: ExceptionToProblemDetailMapType = field(default_factory=dict) """A mapping to convert exceptions into ``ProblemDetailsException.`` All exceptions provided in this will get a custom exception handler where these exceptions are converted into ``ProblemDetailException`` before handling them. This can be used to override the handler for ``HTTPException`` as well. """ class ProblemDetailsPlugin(InitPlugin): """A plugin to convert exceptions into problem details as per RFC 9457.""" def __init__(self, config: ProblemDetailsConfig | None = None): self.config = config or ProblemDetailsConfig() def on_app_init(self, app_config: AppConfig) -> AppConfig: app_config.exception_handlers[ProblemDetailsException] = self.config.exception_handler if self.config.enable_for_all_http_exceptions: app_config.exception_handlers[HTTPException] = _create_exception_handler( _http_exception_to_problem_detail_exception, HTTPException ) for exc_type, conversion_fn in self.config.exception_to_problem_detail_map.items(): app_config.exception_handlers[exc_type] = _create_exception_handler(conversion_fn, exc_type) return app_config litestar-2.16.0/litestar/plugins/prometheus/000077500000000000000000000000001500564371300211365ustar00rootroot00000000000000litestar-2.16.0/litestar/plugins/prometheus/__init__.py000066400000000000000000000003171500564371300232500ustar00rootroot00000000000000from .config import PrometheusConfig from .controller import PrometheusController from .middleware import PrometheusMiddleware __all__ = ("PrometheusConfig", "PrometheusController", "PrometheusMiddleware") litestar-2.16.0/litestar/plugins/prometheus/config.py000066400000000000000000000055211500564371300227600ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING, Callable, Mapping, Sequence from litestar.exceptions import MissingDependencyException from litestar.middleware.base import DefineMiddleware from litestar.plugins.prometheus.middleware import ( PrometheusMiddleware, ) __all__ = ("PrometheusConfig",) try: import prometheus_client # noqa: F401 except ImportError as e: raise MissingDependencyException("prometheus_client", "prometheus-client", "prometheus") from e if TYPE_CHECKING: from litestar.connection.request import Request from litestar.types import Method, Scopes @dataclass class PrometheusConfig: """Configuration class for the PrometheusConfig middleware.""" app_name: str = field(default="litestar") """The name of the application to use in the metrics.""" prefix: str = "litestar" """The prefix to use for the metrics.""" labels: Mapping[str, str | Callable] | None = field(default=None) """A mapping of labels to add to the metrics. The values can be either a string or a callable that returns a string.""" exemplars: Callable[[Request], dict] | None = field(default=None) """A callable that returns a list of exemplars to add to the metrics. Only supported in opementrics-text exposition format.""" buckets: list[str | float] | None = field(default=None) """A list of buckets to use for the histogram.""" excluded_http_methods: Method | Sequence[Method] | None = field(default=None) """A list of http methods to exclude from the metrics.""" exclude_unhandled_paths: bool = field(default=False) """Whether to ignore requests for unhandled paths from the metrics.""" exclude: str | list[str] | None = field(default=None) """A pattern or list of patterns for routes to exclude from the metrics.""" exclude_opt_key: str | None = field(default=None) """A key or list of keys in ``opt`` with which a route handler can "opt-out" of the middleware.""" scopes: Scopes | None = field(default=None) """ASGI scopes processed by the middleware, if None both ``http`` and ``websocket`` will be processed.""" middleware_class: type[PrometheusMiddleware] = field(default=PrometheusMiddleware) """The middleware class to use. """ group_path: bool = field(default=False) """Whether to group paths in the metrics to avoid cardinality explosion. """ @property def middleware(self) -> DefineMiddleware: """Create an instance of :class:`DefineMiddleware ` that wraps with. [PrometheusMiddleware][litestar.plugins.prometheus.PrometheusMiddleware]. or a subclass of this middleware. Returns: An instance of ``DefineMiddleware``. """ return DefineMiddleware(self.middleware_class, config=self) litestar-2.16.0/litestar/plugins/prometheus/controller.py000066400000000000000000000032631500564371300236770ustar00rootroot00000000000000from __future__ import annotations import os from litestar import Controller, get from litestar.exceptions import MissingDependencyException from litestar.response import Response try: import prometheus_client # noqa: F401 except ImportError as e: raise MissingDependencyException("prometheus_client", "prometheus-client", "prometheus") from e from prometheus_client import ( CONTENT_TYPE_LATEST, REGISTRY, CollectorRegistry, generate_latest, multiprocess, ) from prometheus_client.openmetrics.exposition import ( CONTENT_TYPE_LATEST as OPENMETRICS_CONTENT_TYPE_LATEST, ) from prometheus_client.openmetrics.exposition import ( generate_latest as openmetrics_generate_latest, ) __all__ = [ "PrometheusController", ] class PrometheusController(Controller): """Controller for Prometheus endpoints.""" path: str = "/metrics" """The path to expose the metrics on.""" openmetrics_format: bool = False """Whether to expose the metrics in OpenMetrics format.""" @get() async def get(self) -> Response: registry = REGISTRY if "prometheus_multiproc_dir" in os.environ or "PROMETHEUS_MULTIPROC_DIR" in os.environ: registry = CollectorRegistry() multiprocess.MultiProcessCollector(registry) # type: ignore[no-untyped-call] if self.openmetrics_format: headers = {"Content-Type": OPENMETRICS_CONTENT_TYPE_LATEST} return Response(openmetrics_generate_latest(registry), status_code=200, headers=headers) # type: ignore[no-untyped-call] headers = {"Content-Type": CONTENT_TYPE_LATEST} return Response(generate_latest(registry), status_code=200, headers=headers) litestar-2.16.0/litestar/plugins/prometheus/middleware.py000066400000000000000000000155461500564371300236400ustar00rootroot00000000000000from __future__ import annotations import time from functools import wraps from typing import TYPE_CHECKING, Any, Callable, ClassVar, cast from litestar.connection.request import Request from litestar.enums import ScopeType from litestar.exceptions import MissingDependencyException from litestar.middleware.base import AbstractMiddleware __all__ = ("PrometheusMiddleware",) from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR try: import prometheus_client # noqa: F401 except ImportError as e: raise MissingDependencyException("prometheus_client", "prometheus-client", "prometheus") from e from prometheus_client import Counter, Gauge, Histogram if TYPE_CHECKING: from prometheus_client.metrics import MetricWrapperBase from litestar.plugins.prometheus import PrometheusConfig from litestar.types import ASGIApp, Message, Receive, Scope, Send class PrometheusMiddleware(AbstractMiddleware): """Prometheus Middleware.""" _metrics: ClassVar[dict[str, MetricWrapperBase]] = {} def __init__(self, app: ASGIApp, config: PrometheusConfig) -> None: """Middleware that adds Prometheus instrumentation to the application. Args: app: The ``next`` ASGI app to call. config: An instance of :class:`PrometheusConfig <.plugins.prometheus.PrometheusConfig>` """ super().__init__(app=app, scopes=config.scopes, exclude=config.exclude, exclude_opt_key=config.exclude_opt_key) self._config = config self._kwargs: dict[str, Any] = {} if self._config.buckets is not None: self._kwargs["buckets"] = self._config.buckets def request_count(self, labels: dict[str, str | int | float]) -> Counter: metric_name = f"{self._config.prefix}_requests_total" if metric_name not in PrometheusMiddleware._metrics: PrometheusMiddleware._metrics[metric_name] = Counter( name=metric_name, documentation="Total requests", labelnames=[*labels.keys()], ) return cast("Counter", PrometheusMiddleware._metrics[metric_name]) def request_time(self, labels: dict[str, str | int | float]) -> Histogram: metric_name = f"{self._config.prefix}_request_duration_seconds" if metric_name not in PrometheusMiddleware._metrics: PrometheusMiddleware._metrics[metric_name] = Histogram( name=metric_name, documentation="Request duration, in seconds", labelnames=[*labels.keys()], **self._kwargs, ) return cast("Histogram", PrometheusMiddleware._metrics[metric_name]) def requests_in_progress(self, labels: dict[str, str | int | float]) -> Gauge: metric_name = f"{self._config.prefix}_requests_in_progress" if metric_name not in PrometheusMiddleware._metrics: PrometheusMiddleware._metrics[metric_name] = Gauge( name=metric_name, documentation="Total requests currently in progress", labelnames=[*labels.keys()], multiprocess_mode="livesum", ) return cast("Gauge", PrometheusMiddleware._metrics[metric_name]) def requests_error_count(self, labels: dict[str, str | int | float]) -> Counter: metric_name = f"{self._config.prefix}_requests_error_total" if metric_name not in PrometheusMiddleware._metrics: PrometheusMiddleware._metrics[metric_name] = Counter( name=metric_name, documentation="Total errors in requests", labelnames=[*labels.keys()], ) return cast("Counter", PrometheusMiddleware._metrics[metric_name]) def _get_extra_labels(self, request: Request[Any, Any, Any]) -> dict[str, str]: """Get extra labels provided by the config and if they are callable, parse them. Args: request: The request object. Returns: A dictionary of extra labels. """ return {k: str(v(request) if callable(v) else v) for k, v in (self._config.labels or {}).items()} def _get_default_labels(self, request: Request[Any, Any, Any]) -> dict[str, str | int | float]: """Get default label values from the request. Args: request: The request object. Returns: A dictionary of default labels. """ path = request.url.path if self._config.group_path: path = request.scope["path_template"] return { "method": request.method if request.scope["type"] == ScopeType.HTTP else request.scope["type"], "path": path, "status_code": 200, "app_name": self._config.app_name, } async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI callable. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ request = Request[Any, Any, Any](scope, receive) if self._config.excluded_http_methods and request.method in self._config.excluded_http_methods: await self.app(scope, receive, send) return labels = {**self._get_default_labels(request), **self._get_extra_labels(request)} request_span = {"start_time": time.perf_counter(), "end_time": 0, "duration": 0, "status_code": 200} wrapped_send = self._get_wrapped_send(send, request_span) self.requests_in_progress(labels).labels(*labels.values()).inc() try: await self.app(scope, receive, wrapped_send) finally: extra: dict[str, Any] = {} if self._config.exemplars: extra["exemplar"] = self._config.exemplars(request) self.requests_in_progress(labels).labels(*labels.values()).dec() labels["status_code"] = request_span["status_code"] label_values = [*labels.values()] if request_span["status_code"] >= HTTP_500_INTERNAL_SERVER_ERROR: self.requests_error_count(labels).labels(*label_values).inc(**extra) self.request_count(labels).labels(*label_values).inc(**extra) self.request_time(labels).labels(*label_values).observe(request_span["duration"], **extra) def _get_wrapped_send(self, send: Send, request_span: dict[str, float]) -> Callable: @wraps(send) async def wrapped_send(message: Message) -> None: if message["type"] == "http.response.start": request_span["status_code"] = message["status"] if message["type"] == "http.response.body": end = time.perf_counter() request_span["duration"] = end - request_span["start_time"] request_span["end_time"] = end await send(message) return wrapped_send litestar-2.16.0/litestar/plugins/pydantic/000077500000000000000000000000001500564371300205565ustar00rootroot00000000000000litestar-2.16.0/litestar/plugins/pydantic/__init__.py000066400000000000000000000075221500564371300226750ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from litestar.plugins import InitPlugin from litestar.plugins.pydantic.dto import PydanticDTO from litestar.plugins.pydantic.plugins.di import PydanticDIPlugin from litestar.plugins.pydantic.plugins.init import PydanticInitPlugin from litestar.plugins.pydantic.plugins.schema import PydanticSchemaPlugin if TYPE_CHECKING: from pydantic import BaseModel from pydantic.v1 import BaseModel as BaseModelV1 from litestar.config.app import AppConfig from litestar.types.serialization import PydanticV1FieldsListType, PydanticV2FieldsListType __all__ = ( "PydanticDIPlugin", "PydanticDTO", "PydanticInitPlugin", "PydanticPlugin", "PydanticSchemaPlugin", ) def _model_dump(model: BaseModel | BaseModelV1, *, by_alias: bool = False) -> dict[str, Any]: return ( model.model_dump(mode="json", by_alias=by_alias) # pyright: ignore if hasattr(model, "model_dump") else {k: v.decode() if isinstance(v, bytes) else v for k, v in model.dict(by_alias=by_alias).items()} ) def _model_dump_json(model: BaseModel | BaseModelV1, by_alias: bool = False) -> str: return ( model.model_dump_json(by_alias=by_alias) # pyright: ignore if hasattr(model, "model_dump_json") else model.json(by_alias=by_alias) # pyright: ignore ) class PydanticPlugin(InitPlugin): """A plugin that provides Pydantic integration.""" __slots__ = ( "exclude", "exclude_defaults", "exclude_none", "exclude_unset", "include", "prefer_alias", "validate_strict", ) def __init__( self, exclude: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, exclude_defaults: bool = False, exclude_none: bool = False, exclude_unset: bool = False, include: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, prefer_alias: bool = False, validate_strict: bool = False, ) -> None: """Pydantic Plugin to support serialization / validation of Pydantic types / models :param exclude: Fields to exclude during serialization :param exclude_defaults: Fields to exclude during serialization when they are set to their default value :param exclude_none: Fields to exclude during serialization when they are set to ``None`` :param exclude_unset: Fields to exclude during serialization when they arenot set :param include: Fields to exclude during serialization :param prefer_alias: Use the ``by_alias=True`` flag when dumping models :param validate_strict: Use ``strict=True`` when calling ``.model_validate`` on Pydantic 2.x models """ self.exclude = exclude self.exclude_defaults = exclude_defaults self.exclude_none = exclude_none self.exclude_unset = exclude_unset self.include = include self.prefer_alias = prefer_alias self.validate_strict = validate_strict def on_app_init(self, app_config: AppConfig) -> AppConfig: """Configure application for use with Pydantic. Args: app_config: The :class:`AppConfig <.config.app.AppConfig>` instance. """ app_config.plugins.extend( [ PydanticInitPlugin( exclude=self.exclude, exclude_defaults=self.exclude_defaults, exclude_none=self.exclude_none, exclude_unset=self.exclude_unset, include=self.include, prefer_alias=self.prefer_alias, validate_strict=self.validate_strict, ), PydanticSchemaPlugin(prefer_alias=self.prefer_alias), PydanticDIPlugin(), ] ) return app_config litestar-2.16.0/litestar/plugins/pydantic/dto.py000066400000000000000000000167651500564371300217350ustar00rootroot00000000000000from __future__ import annotations import dataclasses from dataclasses import replace from typing import TYPE_CHECKING, Any, Collection, Generic, TypeVar from warnings import warn from typing_extensions import Annotated, TypeAlias, override from litestar.dto.base_dto import AbstractDTO from litestar.dto.data_structures import DTOFieldDefinition from litestar.dto.field import DTO_FIELD_META_KEY, extract_dto_field from litestar.exceptions import MissingDependencyException, ValidationException from litestar.plugins.pydantic.utils import get_model_info, is_pydantic_2_model, is_pydantic_undefined, is_pydantic_v2 from litestar.types.empty import Empty from litestar.typing import FieldDefinition if TYPE_CHECKING: from typing import Generator from litestar.dto import DTOConfig try: import pydantic as _ # noqa: F401 except ImportError as e: raise MissingDependencyException("pydantic") from e try: import pydantic as pydantic_v2 if not is_pydantic_v2(pydantic_v2): raise ImportError from pydantic import ValidationError as ValidationErrorV2 from pydantic import v1 as pydantic_v1 from pydantic.v1 import ValidationError as ValidationErrorV1 ModelType: TypeAlias = "pydantic_v1.BaseModel | pydantic_v2.BaseModel" # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] except ImportError: import pydantic as pydantic_v1 # type: ignore[no-redef] pydantic_v2 = Empty # type: ignore[assignment] from pydantic import ValidationError as ValidationErrorV1 # type: ignore[assignment] ValidationErrorV2 = ValidationErrorV1 # type: ignore[assignment, misc] ModelType = "pydantic_v1.BaseModel" # type: ignore[misc] T = TypeVar("T", bound="ModelType | Collection[ModelType]") __all__ = ("PydanticDTO",) _down_types: dict[Any, Any] = { pydantic_v1.EmailStr: str, pydantic_v1.IPvAnyAddress: str, pydantic_v1.IPvAnyInterface: str, pydantic_v1.IPvAnyNetwork: str, } if pydantic_v2 is not Empty: # type: ignore[comparison-overlap] # pragma: no cover _down_types.update( { pydantic_v2.JsonValue: Any, pydantic_v2.EmailStr: str, pydantic_v2.IPvAnyAddress: str, pydantic_v2.IPvAnyInterface: str, pydantic_v2.IPvAnyNetwork: str, } ) def convert_validation_error(validation_error: ValidationErrorV1 | ValidationErrorV2) -> list[dict[str, Any]]: # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] error_list = validation_error.errors() for error in error_list: if isinstance(exception := error.get("ctx", {}).get("error"), Exception): error["ctx"]["error"] = type(exception).__name__ # pyright: ignore[reportTypedDictNotRequiredAccess] return error_list # type: ignore[return-value] def downtype_for_data_transfer(field_definition: FieldDefinition) -> FieldDefinition: if sub := _down_types.get(field_definition.annotation): return FieldDefinition.from_kwarg( annotation=Annotated[sub, field_definition.metadata], name=field_definition.name ) return field_definition class PydanticDTO(AbstractDTO[T], Generic[T]): """Support for domain modelling with Pydantic.""" @override def decode_builtins(self, value: dict[str, Any]) -> Any: try: return super().decode_builtins(value) except (ValidationErrorV2, ValidationErrorV1) as ex: raise ValidationException(extra=convert_validation_error(ex)) from ex @override def decode_bytes(self, value: bytes) -> Any: try: return super().decode_bytes(value) except (ValidationErrorV2, ValidationErrorV1) as ex: raise ValidationException(extra=convert_validation_error(ex)) from ex @classmethod def generate_field_definitions( cls, model_type: type[pydantic_v1.BaseModel | pydantic_v2.BaseModel], # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] ) -> Generator[DTOFieldDefinition, None, None]: model_info = get_model_info(model_type) model_fields = model_info.model_fields model_field_definitions = model_info.field_definitions for field_name, field_definition in model_field_definitions.items(): field_definition = downtype_for_data_transfer(field_definition) dto_field = extract_dto_field(field_definition, field_definition.extra) default: Any = Empty default_factory: Any = None if field_info := model_fields.get(field_name): # field_info might not exist, since FieldInfo isn't provided by pydantic # for computed fields, but we still generate a FieldDefinition for them try: extra = field_info.extra # type: ignore[union-attr] except AttributeError: extra = field_info.json_schema_extra # type: ignore[union-attr] if extra is not None and extra.pop(DTO_FIELD_META_KEY, None): warn( message="Declaring 'DTOField' via Pydantic's 'Field.extra' is deprecated. " "Use 'Annotated', e.g., 'Annotated[str, DTOField(mark='read-only')]' instead. " "Support for 'DTOField' in 'Field.extra' will be removed in v3.", category=DeprecationWarning, stacklevel=2, ) if not is_pydantic_undefined(field_info.default): default = field_info.default elif field_definition.is_optional: default = None else: default = Empty default_factory = ( field_info.default_factory if field_info.default_factory and not is_pydantic_undefined(field_info.default_factory) else None ) yield replace( DTOFieldDefinition.from_field_definition( field_definition=field_definition, dto_field=dto_field, model_name=model_type.__name__, default_factory=default_factory, # we don't want the constraints to be set on the DTO struct as # constraints, but as schema metadata only, so we can let pydantic # handle all the constraining passthrough_constraints=False, ), default=default, name=field_name, ) @classmethod def detect_nested_field(cls, field_definition: FieldDefinition) -> bool: if pydantic_v2 is not Empty: # type: ignore[comparison-overlap] return field_definition.is_subclass_of((pydantic_v1.BaseModel, pydantic_v2.BaseModel)) return field_definition.is_subclass_of(pydantic_v1.BaseModel) # type: ignore[unreachable] @classmethod def get_config_for_model_type(cls, config: DTOConfig, model_type: type[Any]) -> DTOConfig: if is_pydantic_2_model(model_type) and (model_config := getattr(model_type, "model_config", None)): if model_config.get("extra") == "forbid": config = dataclasses.replace(config, forbid_unknown_fields=True) elif issubclass(model_type, pydantic_v1.BaseModel) and (model_config := getattr(model_type, "Config", None)): # noqa: SIM102 if getattr(model_config, "extra", None) == "forbid": config = dataclasses.replace(config, forbid_unknown_fields=True) return config litestar-2.16.0/litestar/plugins/pydantic/plugins/000077500000000000000000000000001500564371300222375ustar00rootroot00000000000000litestar-2.16.0/litestar/plugins/pydantic/plugins/__init__.py000066400000000000000000000000001500564371300243360ustar00rootroot00000000000000litestar-2.16.0/litestar/plugins/pydantic/plugins/di.py000066400000000000000000000016241500564371300232100ustar00rootroot00000000000000from __future__ import annotations import inspect from inspect import Signature from typing import Any from litestar.plugins import DIPlugin from litestar.plugins.pydantic.utils import is_pydantic_model_class class PydanticDIPlugin(DIPlugin): def has_typed_init(self, type_: Any) -> bool: return is_pydantic_model_class(type_) def get_typed_init(self, type_: Any) -> tuple[Signature, dict[str, Any]]: try: model_fields = dict(type_.model_fields) except AttributeError: model_fields = {k: model_field.field_info for k, model_field in type_.__fields__.items()} parameters = [ inspect.Parameter(name=field_name, kind=inspect.Parameter.KEYWORD_ONLY, annotation=Any) for field_name in model_fields ] type_hints = {field_name: Any for field_name in model_fields} return Signature(parameters), type_hints litestar-2.16.0/litestar/plugins/pydantic/plugins/init.py000066400000000000000000000253101500564371300235550ustar00rootroot00000000000000from __future__ import annotations from contextlib import suppress from functools import partial from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast from uuid import UUID from msgspec import ValidationError from typing_extensions import Buffer, TypeGuard from litestar._signature.types import ExtendedMsgSpecValidationError from litestar.exceptions import MissingDependencyException from litestar.plugins import InitPlugin from litestar.plugins.pydantic.utils import is_pydantic_v2 from litestar.utils import is_class_and_subclass try: import pydantic as _ # noqa: F401 except ImportError as e: raise MissingDependencyException("pydantic") from e try: import pydantic as pydantic_v2 if not is_pydantic_v2(pydantic_v2): raise ImportError from pydantic import v1 as pydantic_v1 except ImportError: import pydantic as pydantic_v1 # type: ignore[no-redef] pydantic_v2 = None # type: ignore[assignment] if TYPE_CHECKING: import pydantic as pydantic_v2_mandatory from litestar.config.app import AppConfig from litestar.types.serialization import PydanticV1FieldsListType, PydanticV2FieldsListType else: pydantic_v2_mandatory = pydantic_v2 T = TypeVar("T") def _dec_pydantic_v1(model_type: type[pydantic_v1.BaseModel], value: Any) -> pydantic_v1.BaseModel: try: return model_type.parse_obj(value) except pydantic_v1.ValidationError as e: raise ExtendedMsgSpecValidationError(errors=cast("list[dict[str, Any]]", e.errors())) from e def _dec_pydantic_v2( model_type: type[pydantic_v2_mandatory.BaseModel], value: Any, strict: bool ) -> pydantic_v2_mandatory.BaseModel: try: return model_type.model_validate(value, strict=strict) except pydantic_v2_mandatory.ValidationError as e: hide_input = model_type.model_config.get("hide_input_in_errors", False) raise ExtendedMsgSpecValidationError( errors=cast("list[dict[str, Any]]", e.errors(include_input=not hide_input)) ) from e def _dec_pydantic_uuid( uuid_type: type[pydantic_v1.UUID1] | type[pydantic_v1.UUID3] | type[pydantic_v1.UUID4] | type[pydantic_v1.UUID5], value: Any, ) -> ( type[pydantic_v1.UUID1] | type[pydantic_v1.UUID3] | type[pydantic_v1.UUID4] | type[pydantic_v1.UUID5] ): # pragma: no cover if isinstance(value, str): value = uuid_type(value) elif isinstance(value, Buffer): value = bytes(value) try: value = uuid_type(value.decode()) except ValueError: # 16 bytes in big-endian order as the bytes argument fail # the above check value = uuid_type(bytes=value) elif isinstance(value, UUID): value = uuid_type(str(value)) if not isinstance(value, uuid_type): raise ValidationError(f"Invalid UUID: {value!r}") if value._required_version != value.version: # pyright: ignore[reportAttributeAccessIssue] raise ValidationError(f"Invalid UUID version: {value!r}") return cast( "type[pydantic_v1.UUID1] | type[pydantic_v1.UUID3] | type[pydantic_v1.UUID4] | type[pydantic_v1.UUID5]", value ) def _is_pydantic_v1_uuid(value: Any) -> bool: # pragma: no cover return is_class_and_subclass(value, (pydantic_v1.UUID1, pydantic_v1.UUID3, pydantic_v1.UUID4, pydantic_v1.UUID5)) _base_encoders: dict[Any, Callable[[Any], Any]] = { pydantic_v1.EmailStr: str, pydantic_v1.NameEmail: str, pydantic_v1.ByteSize: lambda val: val.real, } if pydantic_v2 is not None: # pragma: no cover _base_encoders.update( { pydantic_v2.EmailStr: str, pydantic_v2.NameEmail: str, pydantic_v2.ByteSize: lambda val: val.real, } ) def is_pydantic_v1_model_class(annotation: Any) -> TypeGuard[type[pydantic_v1.BaseModel]]: return is_class_and_subclass(annotation, pydantic_v1.BaseModel) def is_pydantic_v2_model_class(annotation: Any) -> TypeGuard[type[pydantic_v2.BaseModel]]: # pyright: ignore[reportInvalidTypeForm] return is_class_and_subclass(annotation, pydantic_v2.BaseModel) # pyright: ignore[reportOptionalMemberAccess] class PydanticInitPlugin(InitPlugin): __slots__ = ( "exclude", "exclude_defaults", "exclude_none", "exclude_unset", "include", "prefer_alias", "validate_strict", ) def __init__( self, exclude: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, exclude_defaults: bool = False, exclude_none: bool = False, exclude_unset: bool = False, include: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, prefer_alias: bool = False, validate_strict: bool = False, ) -> None: """Pydantic Plugin to support serialization / validation of Pydantic types / models :param exclude: Fields to exclude during serialization :param exclude_defaults: Fields to exclude during serialization when they are set to their default value :param exclude_none: Fields to exclude during serialization when they are set to ``None`` :param exclude_unset: Fields to exclude during serialization when they arenot set :param include: Fields to exclude during serialization :param prefer_alias: Use the ``by_alias=True`` flag when dumping models :param validate_strict: Use ``strict=True`` when calling ``.model_validate`` on Pydantic 2.x models """ self.exclude = exclude self.exclude_defaults = exclude_defaults self.exclude_none = exclude_none self.exclude_unset = exclude_unset self.include = include self.prefer_alias = prefer_alias self.validate_strict = validate_strict @classmethod def encoders( cls, exclude: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, exclude_defaults: bool = False, exclude_none: bool = False, exclude_unset: bool = False, include: PydanticV1FieldsListType | PydanticV2FieldsListType | None = None, prefer_alias: bool = False, ) -> dict[Any, Callable[[Any], Any]]: encoders = { **_base_encoders, **cls._create_pydantic_v1_encoders( prefer_alias=prefer_alias, exclude=exclude, exclude_defaults=exclude_defaults, exclude_none=exclude_none, exclude_unset=exclude_unset, include=include, ), } if pydantic_v2 is not None: # pragma: no cover encoders.update( cls._create_pydantic_v2_encoders( prefer_alias=prefer_alias, exclude=exclude, # type: ignore[arg-type] exclude_defaults=exclude_defaults, exclude_none=exclude_none, exclude_unset=exclude_unset, include=include, # type: ignore[arg-type] ) ) return encoders @classmethod def decoders(cls, validate_strict: bool = False) -> list[tuple[Callable[[Any], bool], Callable[[Any, Any], Any]]]: decoders: list[tuple[Callable[[Any], bool], Callable[[Any, Any], Any]]] = [ (is_pydantic_v1_model_class, _dec_pydantic_v1) ] if pydantic_v2 is not None: # pragma: no cover decoders.append( ( is_pydantic_v2_model_class, partial(_dec_pydantic_v2, strict=validate_strict), ) ) decoders.append((_is_pydantic_v1_uuid, _dec_pydantic_uuid)) return decoders @staticmethod def _create_pydantic_v1_encoders( exclude: PydanticV1FieldsListType | None = None, exclude_defaults: bool = False, exclude_none: bool = False, exclude_unset: bool = False, include: PydanticV1FieldsListType | None = None, prefer_alias: bool = False, ) -> dict[Any, Callable[[Any], Any]]: # pragma: no cover return { pydantic_v1.BaseModel: lambda model: { k: v.decode() if isinstance(v, bytes) else v for k, v in model.dict( by_alias=prefer_alias, exclude=exclude, exclude_defaults=exclude_defaults, exclude_none=exclude_none, exclude_unset=exclude_unset, include=include, ).items() }, pydantic_v1.SecretField: str, pydantic_v1.StrictBool: int, pydantic_v1.color.Color: str, # pyright: ignore[reportAttributeAccessIssue] pydantic_v1.ConstrainedBytes: lambda val: val.decode("utf-8"), pydantic_v1.ConstrainedDate: lambda val: val.isoformat(), pydantic_v1.AnyUrl: str, } @staticmethod def _create_pydantic_v2_encoders( exclude: PydanticV2FieldsListType | None = None, exclude_defaults: bool = False, exclude_none: bool = False, exclude_unset: bool = False, include: PydanticV2FieldsListType | None = None, prefer_alias: bool = False, ) -> dict[Any, Callable[[Any], Any]]: encoders: dict[Any, Callable[[Any], Any]] = { pydantic_v2.BaseModel: lambda model: model.model_dump( # pyright: ignore[reportOptionalMemberAccess] by_alias=prefer_alias, exclude=exclude, exclude_defaults=exclude_defaults, exclude_none=exclude_none, exclude_unset=exclude_unset, include=include, mode="json", ), pydantic_v2.types.SecretStr: lambda val: "**********" if val else "", # pyright: ignore[reportOptionalMemberAccess] pydantic_v2.types.SecretBytes: lambda val: "**********" if val else "", # pyright: ignore[reportOptionalMemberAccess] pydantic_v2.AnyUrl: str, # pyright: ignore[reportOptionalMemberAccess] } with suppress(ImportError): from pydantic_extra_types import color encoders[color.Color] = str return encoders def on_app_init(self, app_config: AppConfig) -> AppConfig: app_config.type_encoders = { **self.encoders( prefer_alias=self.prefer_alias, exclude=self.exclude, exclude_defaults=self.exclude_defaults, exclude_none=self.exclude_none, exclude_unset=self.exclude_unset, include=self.include, ), **(app_config.type_encoders or {}), } app_config.type_decoders = [ *self.decoders(validate_strict=self.validate_strict), *(app_config.type_decoders or []), ] return app_config litestar-2.16.0/litestar/plugins/pydantic/plugins/schema.py000066400000000000000000000247511500564371300240620ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from litestar.exceptions import MissingDependencyException from litestar.openapi.spec import OpenAPIFormat, OpenAPIType, Schema from litestar.plugins import OpenAPISchemaPlugin from litestar.plugins.pydantic.utils import ( get_model_info, is_pydantic_constrained_field, is_pydantic_model_class, is_pydantic_undefined, is_pydantic_v2, ) from litestar.utils import is_class_and_subclass try: import pydantic as _ # noqa: F401 except ImportError as e: raise MissingDependencyException("pydantic") from e try: import pydantic as pydantic_v2 if not is_pydantic_v2(pydantic_v2): raise ImportError from pydantic import v1 as pydantic_v1 except ImportError: import pydantic as pydantic_v1 # type: ignore[no-redef] pydantic_v2 = None # type: ignore[assignment] if TYPE_CHECKING: from litestar._openapi.schema_generation.schema import SchemaCreator from litestar.typing import FieldDefinition PYDANTIC_TYPE_MAP: dict[type[Any] | None | Any, Schema] = { pydantic_v1.ByteSize: Schema(type=OpenAPIType.INTEGER), pydantic_v1.EmailStr: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.EMAIL), pydantic_v1.IPvAnyAddress: Schema( one_of=[ Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.IPV4, description="IPv4 address", ), Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.IPV6, description="IPv6 address", ), ] ), pydantic_v1.IPvAnyInterface: Schema( one_of=[ Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.IPV4, description="IPv4 interface", ), Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.IPV6, description="IPv6 interface", ), ] ), pydantic_v1.IPvAnyNetwork: Schema( one_of=[ Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.IPV4, description="IPv4 network", ), Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.IPV6, description="IPv6 network", ), ] ), pydantic_v1.Json: Schema(type=OpenAPIType.OBJECT, format=OpenAPIFormat.JSON_POINTER), pydantic_v1.NameEmail: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.EMAIL, description="Name and email"), # removed in v2 pydantic_v1.PyObject: Schema( type=OpenAPIType.STRING, description="dot separated path identifying a python object, e.g. 'decimal.Decimal'", ), # annotated in v2 pydantic_v1.UUID1: Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.UUID, description="UUID1 string", ), pydantic_v1.UUID3: Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.UUID, description="UUID3 string", ), pydantic_v1.UUID4: Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.UUID, description="UUID4 string", ), pydantic_v1.UUID5: Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.UUID, description="UUID5 string", ), pydantic_v1.DirectoryPath: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI_REFERENCE), pydantic_v1.AnyUrl: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URL), pydantic_v1.AnyHttpUrl: Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.URL, description="must be a valid HTTP based URL" ), pydantic_v1.FilePath: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI_REFERENCE), pydantic_v1.HttpUrl: Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.URL, description="must be a valid HTTP based URL", max_length=2083, ), pydantic_v1.RedisDsn: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI, description="redis DSN"), pydantic_v1.PostgresDsn: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI, description="postgres DSN"), pydantic_v1.SecretBytes: Schema(type=OpenAPIType.STRING), pydantic_v1.SecretStr: Schema(type=OpenAPIType.STRING), pydantic_v1.StrictBool: Schema(type=OpenAPIType.BOOLEAN), pydantic_v1.StrictBytes: Schema(type=OpenAPIType.STRING), pydantic_v1.StrictFloat: Schema(type=OpenAPIType.NUMBER), pydantic_v1.StrictInt: Schema(type=OpenAPIType.INTEGER), pydantic_v1.StrictStr: Schema(type=OpenAPIType.STRING), pydantic_v1.NegativeFloat: Schema(type=OpenAPIType.NUMBER, exclusive_maximum=0.0), pydantic_v1.NegativeInt: Schema(type=OpenAPIType.INTEGER, exclusive_maximum=0), pydantic_v1.NonNegativeInt: Schema(type=OpenAPIType.INTEGER, minimum=0), pydantic_v1.NonPositiveFloat: Schema(type=OpenAPIType.NUMBER, maximum=0.0), pydantic_v1.PaymentCardNumber: Schema(type=OpenAPIType.STRING, min_length=12, max_length=19), pydantic_v1.PositiveFloat: Schema(type=OpenAPIType.NUMBER, exclusive_minimum=0.0), pydantic_v1.PositiveInt: Schema(type=OpenAPIType.INTEGER, exclusive_minimum=0), } if pydantic_v2 is not None: # pragma: no cover from pydantic import networks PYDANTIC_TYPE_MAP.update( { pydantic_v2.SecretStr: Schema(type=OpenAPIType.STRING), pydantic_v2.SecretBytes: Schema(type=OpenAPIType.STRING), pydantic_v2.ByteSize: Schema(type=OpenAPIType.INTEGER), pydantic_v2.EmailStr: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.EMAIL), pydantic_v2.IPvAnyAddress: Schema( one_of=[ Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.IPV4, description="IPv4 address", ), Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.IPV6, description="IPv6 address", ), ] ), pydantic_v2.IPvAnyInterface: Schema( one_of=[ Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.IPV4, description="IPv4 interface", ), Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.IPV6, description="IPv6 interface", ), ] ), pydantic_v2.IPvAnyNetwork: Schema( one_of=[ Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.IPV4, description="IPv4 network", ), Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.IPV6, description="IPv6 network", ), ] ), pydantic_v2.Json: Schema(type=OpenAPIType.OBJECT, format=OpenAPIFormat.JSON_POINTER), pydantic_v2.NameEmail: Schema( type=OpenAPIType.STRING, format=OpenAPIFormat.EMAIL, description="Name and email" ), pydantic_v2.AnyUrl: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URL), } ) if int(pydantic_v2.version.version_short().split(".")[1]) >= 10: # These were 'Annotated' type aliases before Pydantic 2.10, where they were # changed to proper classes. Using subscripted generics type in an 'isinstance' # check would raise a 'TypeError' on Python <3.12 PYDANTIC_TYPE_MAP.update( { networks.HttpUrl: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URL), networks.AnyHttpUrl: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URL), } ) _supported_types = (pydantic_v1.BaseModel, *PYDANTIC_TYPE_MAP.keys()) if pydantic_v2 is not None: # pragma: no cover _supported_types = (pydantic_v2.BaseModel, *_supported_types) class PydanticSchemaPlugin(OpenAPISchemaPlugin): __slots__ = ("prefer_alias",) def __init__(self, prefer_alias: bool = False) -> None: self.prefer_alias = prefer_alias @staticmethod def is_plugin_supported_type(value: Any) -> bool: return isinstance(value, _supported_types) or is_class_and_subclass(value, _supported_types) # type: ignore[arg-type] @staticmethod def is_undefined_sentinel(value: Any) -> bool: return is_pydantic_undefined(value) @staticmethod def is_constrained_field(field_definition: FieldDefinition) -> bool: return is_pydantic_constrained_field(field_definition.annotation) def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: """Given a type annotation, transform it into an OpenAPI schema class. Args: field_definition: FieldDefinition instance. schema_creator: An instance of the schema creator class Returns: An :class:`OpenAPI ` instance. """ if schema_creator.prefer_alias != self.prefer_alias: schema_creator.prefer_alias = True if is_pydantic_model_class(field_definition.annotation): return self.for_pydantic_model(field_definition=field_definition, schema_creator=schema_creator) return PYDANTIC_TYPE_MAP[field_definition.annotation] # pragma: no cover @classmethod def for_pydantic_model(cls, field_definition: FieldDefinition, schema_creator: SchemaCreator) -> Schema: # pyright: ignore """Create a schema object for a given pydantic model class. Args: field_definition: FieldDefinition instance. schema_creator: An instance of the schema creator class Returns: A schema instance. """ model_info = get_model_info(field_definition.annotation, prefer_alias=schema_creator.prefer_alias) return schema_creator.create_component_schema( field_definition, required=sorted(f.name for f in model_info.field_definitions.values() if f.is_required), property_fields=model_info.field_definitions, title=model_info.title, examples=None if model_info.example is None else [model_info.example], ) litestar-2.16.0/litestar/plugins/pydantic/utils.py000066400000000000000000000461031500564371300222740ustar00rootroot00000000000000# mypy: strict-equality=False # pyright: reportGeneralTypeIssues=false from __future__ import annotations import datetime import re from dataclasses import dataclass from inspect import isclass from typing import TYPE_CHECKING, Any, Callable, Literal, Optional from typing_extensions import Annotated, get_type_hints from litestar.openapi.spec import Example from litestar.params import KwargDefinition, ParameterKwarg from litestar.types import Empty from litestar.typing import FieldDefinition from litestar.utils import deprecated, is_class_and_subclass, is_generic, is_undefined_sentinel from litestar.utils.typing import ( _substitute_typevars, get_origin_or_inner_type, get_safe_generic_origin, get_type_hints_with_generics_resolved, normalize_type_annotation, ) # isort: off try: from pydantic import v1 as pydantic_v1 import pydantic as pydantic_v2 from pydantic.fields import PydanticUndefined as Pydantic2Undefined # type: ignore[attr-defined] from pydantic.v1.fields import Undefined as Pydantic1Undefined PYDANTIC_UNDEFINED_SENTINELS = {Pydantic1Undefined, Pydantic2Undefined} except ImportError: try: import pydantic as pydantic_v1 # type: ignore[no-redef] from pydantic.fields import Undefined as Pydantic1Undefined # type: ignore[attr-defined, no-redef] pydantic_v2 = Empty # type: ignore[assignment] PYDANTIC_UNDEFINED_SENTINELS = {Pydantic1Undefined} except ImportError: # pyright: ignore pydantic_v1 = Empty # type: ignore[assignment] pydantic_v2 = Empty # type: ignore[assignment] PYDANTIC_UNDEFINED_SENTINELS = set() # isort: on if TYPE_CHECKING: from types import ModuleType from typing_extensions import TypeGuard def is_pydantic_model_class( annotation: Any, ) -> TypeGuard[type[pydantic_v1.BaseModel | pydantic_v2.BaseModel]]: # pyright: ignore """Given a type annotation determine if the annotation is a subclass of pydantic's BaseModel. Args: annotation: A type. Returns: A typeguard determining whether the type is :data:`BaseModel pydantic.BaseModel>`. """ tests: list[bool] = [] if pydantic_v1 is not Empty: # pragma: no cover tests.append(is_class_and_subclass(annotation, pydantic_v1.BaseModel)) if pydantic_v2 is not Empty: # pragma: no cover tests.append(is_class_and_subclass(annotation, pydantic_v2.BaseModel)) return any(tests) def is_pydantic_model_instance( annotation: Any, ) -> TypeGuard[pydantic_v1.BaseModel | pydantic_v2.BaseModel]: # pyright: ignore """Given a type annotation determine if the annotation is an instance of pydantic's BaseModel. Args: annotation: A type. Returns: A typeguard determining whether the type is :data:`BaseModel pydantic.BaseModel>`. """ tests: list[bool] = [] if pydantic_v1 is not Empty: # pragma: no cover tests.append(isinstance(annotation, pydantic_v1.BaseModel)) if pydantic_v2 is not Empty: # pragma: no cover tests.append(isinstance(annotation, pydantic_v2.BaseModel)) return any(tests) def is_pydantic_constrained_field(annotation: Any) -> bool: """Check if the given annotation is a constrained pydantic type. Args: annotation: A type annotation Returns: True if pydantic is installed and the type is a constrained type, otherwise False. """ if pydantic_v1 is Empty: # pragma: no cover return False # type: ignore[unreachable] return any( is_class_and_subclass(annotation, constrained_type) # pyright: ignore for constrained_type in ( pydantic_v1.ConstrainedBytes, pydantic_v1.ConstrainedDate, pydantic_v1.ConstrainedDecimal, pydantic_v1.ConstrainedFloat, pydantic_v1.ConstrainedFrozenSet, pydantic_v1.ConstrainedInt, pydantic_v1.ConstrainedList, pydantic_v1.ConstrainedSet, pydantic_v1.ConstrainedStr, ) ) def pydantic_unwrap_and_get_origin(annotation: Any) -> Any | None: if pydantic_v2 is Empty or (pydantic_v1 is not Empty and is_class_and_subclass(annotation, pydantic_v1.BaseModel)): return get_origin_or_inner_type(annotation) origin = annotation.__pydantic_generic_metadata__["origin"] return normalize_type_annotation(origin) def pydantic_get_type_hints_with_generics_resolved( annotation: Any, globalns: dict[str, Any] | None = None, localns: dict[str, Any] | None = None, include_extras: bool = False, model_annotations: dict[str, Any] | None = None, ) -> dict[str, Any]: if pydantic_v2 is Empty or (pydantic_v1 is not Empty and is_class_and_subclass(annotation, pydantic_v1.BaseModel)): return get_type_hints_with_generics_resolved(annotation, type_hints=model_annotations) origin = pydantic_unwrap_and_get_origin(annotation) if origin is None: if model_annotations is None: # pragma: no cover model_annotations = get_type_hints( annotation, globalns=globalns, localns=localns, include_extras=include_extras ) typevar_map = {p: p for p in annotation.__pydantic_generic_metadata__["parameters"]} else: if model_annotations is None: model_annotations = get_type_hints( origin, globalns=globalns, localns=localns, include_extras=include_extras ) args = annotation.__pydantic_generic_metadata__["args"] parameters = origin.__pydantic_generic_metadata__["parameters"] typevar_map = dict(zip(parameters, args)) return {n: _substitute_typevars(type_, typevar_map) for n, type_ in model_annotations.items()} @deprecated(version="2.6.2") def pydantic_get_unwrapped_annotation_and_type_hints(annotation: Any) -> tuple[Any, dict[str, Any]]: # pragma: no cover """Get the unwrapped annotation and the type hints after resolving generics. Args: annotation: A type annotation. Returns: A tuple containing the unwrapped annotation and the type hints. """ if is_generic(annotation): origin = pydantic_unwrap_and_get_origin(annotation) return origin or annotation, pydantic_get_type_hints_with_generics_resolved(annotation, include_extras=True) return annotation, get_type_hints(annotation, include_extras=True) def is_pydantic_2_model( obj: type[pydantic_v1.BaseModel | pydantic_v2.BaseModel], # pyright: ignore ) -> TypeGuard[pydantic_v2.BaseModel]: # pyright: ignore return pydantic_v2 is not Empty and issubclass(obj, pydantic_v2.BaseModel) def is_pydantic_undefined(value: Any) -> bool: return any(v is value for v in PYDANTIC_UNDEFINED_SENTINELS) def create_field_definitions_for_computed_fields( model: type[pydantic_v1.BaseModel | pydantic_v2.BaseModel], # pyright: ignore prefer_alias: bool, ) -> dict[str, FieldDefinition]: """Create field definitions for computed fields. Args: model: A pydantic model. prefer_alias: Whether to prefer the alias or the name of the field. Returns: A dictionary containing the field definitions for the computed fields. """ pydantic_decorators = getattr(model, "__pydantic_decorators__", None) if pydantic_decorators is None: return {} def get_name(k: str, dec: Any) -> str: if not dec.info.alias: return k return dec.info.alias if prefer_alias else k # type: ignore[no-any-return] return { (name := get_name(k, dec)): FieldDefinition.from_annotation( Annotated[ dec.info.return_type, KwargDefinition( title=dec.info.title, description=dec.info.description, read_only=True, examples=[Example(value=v) for v in examples] if (examples := dec.info.examples) else None, schema_extra=dec.info.json_schema_extra, ), ], name=name, ) for k, dec in pydantic_decorators.computed_fields.items() } def is_pydantic_v2(module: ModuleType) -> bool: """Determine if the given module is pydantic v2. Given a module we expect to be a pydantic version, determine if it is pydantic v2. Args: module: A module. Returns: True if the module is pydantic v2, otherwise False. """ return bool(module.__version__.startswith("2.")) @dataclass(frozen=True) class PydanticModelInfo: pydantic_version: Literal["1", "2"] field_definitions: dict[str, FieldDefinition] model_fields: dict[str, pydantic_v1.fields.FieldInfo | pydantic_v2.fields.FieldInfo] # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] title: str | None = None example: Any | None = None is_generic: bool = False _CreateFieldDefinition = Callable[..., FieldDefinition] def _create_field_definition_v1( # noqa: C901 field_annotation: Any, *, field_info: pydantic_v1.fields.FieldInfo, # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] **field_definition_kwargs: Any, ) -> FieldDefinition: kwargs: dict[str, Any] = {} examples: list[Any] = [] if example := field_info.extra.get("example"): examples.append(example) if extra_examples := field_info.extra.get("examples"): examples.extend(extra_examples) if examples: kwargs["examples"] = [Example(value=e) for e in examples] if title := field_info.title: kwargs["title"] = title if description := field_info.description: kwargs["description"] = description kwarg_definition: KwargDefinition | None = None if isclass(field_annotation): if issubclass(field_annotation, pydantic_v1.ConstrainedBytes): # pyright: ignore[reportArgumentType,reportAttributeAccessIssue] kwarg_definition = ParameterKwarg( min_length=field_annotation.min_length, max_length=field_annotation.max_length, lower_case=field_annotation.to_lower, upper_case=field_annotation.to_upper, **kwargs, ) field_definition_kwargs["raw"] = field_annotation field_annotation = bytes elif issubclass(field_annotation, pydantic_v1.ConstrainedStr): # pyright: ignore[reportArgumentType,reportAttributeAccessIssue] kwarg_definition = ParameterKwarg( min_length=field_annotation.min_length, max_length=field_annotation.max_length, lower_case=field_annotation.to_lower, upper_case=field_annotation.to_upper, pattern=field_annotation.regex.pattern if isinstance(field_annotation.regex, re.Pattern) else field_annotation.regex, **kwargs, ) field_definition_kwargs["raw"] = field_annotation field_annotation = str elif issubclass(field_annotation, pydantic_v1.ConstrainedDate): # pyright: ignore[reportArgumentType,reportAttributeAccessIssue] # TODO: The typings of ParameterKwarg need fixing. Specifically, the # gt/ge/lt/le fields need to be typed with protocols, such that they may # accept any type that implements the respective comparisons kwarg_definition = ParameterKwarg( gt=field_annotation.gt, # type: ignore[arg-type] ge=field_annotation.ge, # type: ignore[arg-type] lt=field_annotation.lt, # type: ignore[arg-type] le=field_annotation.le, # type: ignore[arg-type] **kwargs, ) field_definition_kwargs["raw"] = field_annotation field_annotation = datetime.date elif issubclass( field_annotation, (pydantic_v1.ConstrainedInt, pydantic_v1.ConstrainedFloat, pydantic_v1.ConstrainedDecimal), # pyright: ignore[reportArgumentType,reportAttributeAccessIssue] ): kwarg_definition = ParameterKwarg( gt=field_annotation.gt, # type: ignore[arg-type] ge=field_annotation.ge, # type: ignore[arg-type] lt=field_annotation.lt, # type: ignore[arg-type] le=field_annotation.le, # type: ignore[arg-type] multiple_of=field_annotation.multiple_of, # type: ignore[arg-type] **kwargs, ) field_definition_kwargs["raw"] = field_annotation field_annotation = field_annotation.mro()[2] elif issubclass( field_annotation, (pydantic_v1.ConstrainedList, pydantic_v1.ConstrainedSet, pydantic_v1.ConstrainedFrozenSet), # pyright: ignore[reportArgumentType,reportAttributeAccessIssue] ): kwarg_definition = ParameterKwarg( max_items=field_annotation.max_items, min_items=field_annotation.min_items, **kwargs ) field_definition_kwargs["raw"] = field_annotation # on < 3.9, these builtins are not generic origin = get_safe_generic_origin(None, field_annotation.__origin__) field_annotation = origin[field_annotation.item_type] if kwarg_definition is None and kwargs: kwarg_definition = ParameterKwarg(**kwargs) if kwarg_definition: field_definition_kwargs["raw"] = field_annotation field_annotation = Annotated[field_annotation, kwarg_definition] return FieldDefinition.from_annotation( annotation=field_annotation, **field_definition_kwargs, ) def _create_field_definition_v2( # noqa: C901 field_annotation: Any, *, field_info: pydantic_v2.fields.FieldInfo, # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] **field_definition_kwargs: Any, ) -> FieldDefinition: kwargs: dict[str, Any] = {} examples: list[Any] = [] field_meta: list[Any] = [] if json_schema_extra := field_info.json_schema_extra: if callable(json_schema_extra): raise ValueError("Callables not supported for json_schema_extra") if json_schema_example := json_schema_extra.get("example"): del json_schema_extra["example"] examples.append(json_schema_example) if json_schema_examples := json_schema_extra.get("examples"): del json_schema_extra["examples"] examples.extend(json_schema_examples) # type: ignore[arg-type] if field_examples := field_info.examples: examples.extend(field_examples) if examples: if not json_schema_extra: json_schema_extra = {} json_schema_extra["examples"] = examples if description := field_info.description: kwargs["description"] = description if title := field_info.title: kwargs["title"] = title for meta in field_info.metadata: if isinstance(meta, pydantic_v2.types.StringConstraints): # pyright: ignore[reportAttributeAccessIssue] kwargs["min_length"] = meta.min_length kwargs["max_length"] = meta.max_length kwargs["pattern"] = meta.pattern kwargs["lower_case"] = meta.to_lower kwargs["upper_case"] = meta.to_upper # forward other metadata else: field_meta.append(meta) if json_schema_extra: kwargs["schema_extra"] = json_schema_extra kwargs = {k: v for k, v in kwargs.items() if v is not None} if kwargs: kwarg_definition = ParameterKwarg(**kwargs) field_meta.append(kwarg_definition) if field_meta: field_definition_kwargs["raw"] = field_annotation for meta in field_meta: field_annotation = Annotated[field_annotation, meta] return FieldDefinition.from_annotation( annotation=field_annotation, **field_definition_kwargs, ) def get_model_info( annotation: Any, prefer_alias: bool = False, ) -> PydanticModelInfo: model: type[pydantic_v1.BaseModel | pydantic_v2.BaseModel] # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] if is_generic(annotation): is_generic_model = True model = pydantic_unwrap_and_get_origin(annotation) or annotation else: is_generic_model = False model = annotation if is_pydantic_2_model(model): model_config = model.model_config model_field_info = model.model_fields title = model_config.get("title") example = model_config.get("example") is_v2_model = True else: model_config = model.__config__ # type: ignore[assignment, union-attr] model_field_info = model.__fields__ # type: ignore[assignment] title = getattr(model_config, "title", None) example = getattr(model_config, "example", None) is_v2_model = False model_fields: dict[str, pydantic_v1.fields.FieldInfo | pydantic_v2.fields.FieldInfo] = { # pyright: ignore[reportInvalidTypeForm,reportGeneralTypeIssues] k: getattr(f, "field_info", f) for k, f in model_field_info.items() } if is_v2_model: # extract the annotations from the FieldInfo. This allows us to skip fields # which have been marked as private # if there's a default factory, we wrap the field in 'Optional', to signal # that it is not required model_annotations = { k: Optional[field_info.annotation] if field_info.default_factory else field_info.annotation # type: ignore[union-attr] for k, field_info in model_fields.items() } else: # pydantic v1 requires some workarounds here model_annotations = { k: f.outer_type_ if f.required or f.default else Optional[f.outer_type_] for k, f in model.__fields__.items() # type: ignore[union-attr] } if is_generic_model: # if the model is generic, resolve the type variables. We pass in the # already extracted annotations, to keep the logic of respecting private # fields consistent with the above model_annotations = pydantic_get_type_hints_with_generics_resolved( annotation, model_annotations=model_annotations, include_extras=True ) create_field_definition: _CreateFieldDefinition = ( _create_field_definition_v2 if is_v2_model else _create_field_definition_v1 # type: ignore[assignment] ) property_fields = { field_info.alias if field_info.alias and prefer_alias else k: create_field_definition( field_annotation=model_annotations[k], name=field_info.alias if field_info.alias and prefer_alias else k, default=Empty if is_undefined_sentinel(field_info.default) or is_pydantic_undefined(field_info.default) else field_info.default, field_info=field_info, ) for k, field_info in model_fields.items() } computed_field_definitions = create_field_definitions_for_computed_fields( model, prefer_alias=prefer_alias, ) property_fields.update(computed_field_definitions) return PydanticModelInfo( pydantic_version="2" if is_v2_model else "1", title=title, example=example, field_definitions=property_fields, is_generic=is_generic_model, model_fields=model_fields, ) litestar-2.16.0/litestar/plugins/sqlalchemy.py000066400000000000000000000113371500564371300214640ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # pyright: reportUnusedImport=false from __future__ import annotations from typing import TYPE_CHECKING from litestar.utils import warn_deprecation __all__ = ( "AlembicAsyncConfig", "AlembicCommands", "AlembicSyncConfig", "AsyncSessionConfig", # deprecated "AuditColumns", "BigIntAuditBase", "BigIntBase", "BigIntPrimaryKey", "CommonTableAttributes", "EngineConfig", "SQLAlchemyAsyncConfig", "SQLAlchemyDTO", "SQLAlchemyDTOConfig", "SQLAlchemyInitPlugin", "SQLAlchemyPlugin", "SQLAlchemySerializationPlugin", "SQLAlchemySyncConfig", "SyncSessionConfig", "UUIDAuditBase", "UUIDBase", "UUIDPrimaryKey", "async_autocommit_before_send_handler", "async_autocommit_handler_maker", "async_default_before_send_handler", "async_default_handler_maker", "base", "exceptions", "filters", "mixins", "operations", "orm_registry", "repository", "service", "sync_autocommit_before_send_handler", "sync_autocommit_handler_maker", "sync_default_before_send_handler", "sync_default_handler_maker", "types", "utils", ) def __getattr__(attr_name: str) -> object: _deprecated_attrs = { "AuditColumns", "BigIntAuditBase", "BigIntBase", "BigIntPrimaryKey", "CommonTableAttributes", "UUIDAuditBase", "UUIDBase", "UUIDPrimaryKey", "orm_registry", } if attr_name in _deprecated_attrs: from advanced_alchemy.base import ( AuditColumns, BigIntAuditBase, BigIntBase, BigIntPrimaryKey, CommonTableAttributes, UUIDAuditBase, UUIDBase, UUIDPrimaryKey, orm_registry, ) warn_deprecation( deprecated_name=f"litestar.plugins.sqlalchemy.{attr_name}", version="2.9.0", kind="import", removal_in="3.0", info=f"importing {attr_name} from 'litestar.plugins.sqlalchemy' is deprecated, please" f"import it from 'litestar.plugins.sqlalchemy.base.{attr_name}' instead", ) value = globals()[attr_name] = locals()[attr_name] return value if attr_name in set(__all__).difference(_deprecated_attrs): from advanced_alchemy import ( base, exceptions, filters, mixins, operations, repository, service, types, utils, ) from advanced_alchemy.extensions.litestar import ( AlembicAsyncConfig, AlembicCommands, AlembicSyncConfig, AsyncSessionConfig, EngineConfig, SQLAlchemyAsyncConfig, SQLAlchemyDTO, SQLAlchemyDTOConfig, SQLAlchemyInitPlugin, SQLAlchemyPlugin, SQLAlchemySerializationPlugin, SQLAlchemySyncConfig, SyncSessionConfig, async_autocommit_before_send_handler, async_autocommit_handler_maker, async_default_before_send_handler, async_default_handler_maker, sync_autocommit_before_send_handler, sync_autocommit_handler_maker, sync_default_before_send_handler, sync_default_handler_maker, ) value = globals()[attr_name] = locals()[attr_name] return value raise AttributeError(f"module {__name__!r} has no attribute {attr_name!r}") if TYPE_CHECKING: from advanced_alchemy import ( base, exceptions, filters, mixins, operations, repository, service, types, utils, ) from advanced_alchemy.base import ( AuditColumns, BigIntAuditBase, BigIntBase, BigIntPrimaryKey, CommonTableAttributes, UUIDAuditBase, UUIDBase, UUIDPrimaryKey, orm_registry, ) from advanced_alchemy.extensions.litestar import ( AlembicAsyncConfig, AlembicCommands, AlembicSyncConfig, AsyncSessionConfig, EngineConfig, SQLAlchemyAsyncConfig, SQLAlchemyDTO, SQLAlchemyDTOConfig, SQLAlchemyInitPlugin, SQLAlchemyPlugin, SQLAlchemySerializationPlugin, SQLAlchemySyncConfig, SyncSessionConfig, async_autocommit_before_send_handler, async_autocommit_handler_maker, async_default_before_send_handler, async_default_handler_maker, sync_autocommit_before_send_handler, sync_autocommit_handler_maker, sync_default_before_send_handler, sync_default_handler_maker, ) litestar-2.16.0/litestar/plugins/structlog.py000066400000000000000000000042601500564371300213450ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING from litestar.cli._utils import console from litestar.logging.config import StructLoggingConfig from litestar.middleware.logging import LoggingMiddlewareConfig from litestar.plugins import InitPlugin if TYPE_CHECKING: from litestar.config.app import AppConfig @dataclass class StructlogConfig: structlog_logging_config: StructLoggingConfig = field(default_factory=StructLoggingConfig) """Structlog Logging configuration for Litestar. See ``litestar.logging.config.StructLoggingConfig``` for details.""" middleware_logging_config: LoggingMiddlewareConfig = field(default_factory=LoggingMiddlewareConfig) """Middleware logging config.""" enable_middleware_logging: bool = True """Enable request logging.""" class StructlogPlugin(InitPlugin): """Structlog Plugin.""" __slots__ = ("_config",) def __init__(self, config: StructlogConfig | None = None) -> None: if config is None: config = StructlogConfig() self._config = config super().__init__() def on_app_init(self, app_config: AppConfig) -> AppConfig: """Structlog Plugin Args: app_config: The :class:`AppConfig ` instance. Returns: The app config object. """ if app_config.logging_config is not None and isinstance(app_config.logging_config, StructLoggingConfig): console.print( "[red dim]* Found pre-configured `StructLoggingConfig` on the `app` instance. Skipping configuration.[/]", ) else: app_config.logging_config = self._config.structlog_logging_config app_config.logging_config.configure() if self._config.structlog_logging_config.standard_lib_logging_config is not None: # pragma: no cover self._config.structlog_logging_config.standard_lib_logging_config.configure() # pragma: no cover if self._config.enable_middleware_logging: app_config.middleware.append(self._config.middleware_logging_config.middleware) return app_config # pragma: no cover litestar-2.16.0/litestar/py.typed000066400000000000000000000000001500564371300167470ustar00rootroot00000000000000litestar-2.16.0/litestar/repository/000077500000000000000000000000001500564371300175015ustar00rootroot00000000000000litestar-2.16.0/litestar/repository/__init__.py000066400000000000000000000005541500564371300216160ustar00rootroot00000000000000from __future__ import annotations from .abc import AbstractAsyncRepository, AbstractSyncRepository from .exceptions import ConflictError, NotFoundError, RepositoryError from .filters import FilterTypes __all__ = ( "AbstractAsyncRepository", "AbstractSyncRepository", "ConflictError", "FilterTypes", "NotFoundError", "RepositoryError", ) litestar-2.16.0/litestar/repository/_exceptions.py000066400000000000000000000006541500564371300224000ustar00rootroot00000000000000from __future__ import annotations # pragma: no cover __all__ = ("ConflictError", "NotFoundError", "RepositoryError") # pragma: no cover class RepositoryError(Exception): # pragma: no cover """Base repository exception type.""" class ConflictError(RepositoryError): # pragma: no cover """Data integrity error.""" class NotFoundError(RepositoryError): # pragma: no cover """An identity does not exist.""" litestar-2.16.0/litestar/repository/_filters.py000066400000000000000000000061711500564371300216670ustar00rootroot00000000000000"""Collection filter datastructures.""" from __future__ import annotations from collections import abc # noqa: TC003 from dataclasses import dataclass from datetime import datetime # noqa: TC003 from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar if TYPE_CHECKING: from typing_extensions import TypeAlias T = TypeVar("T") __all__ = ( "BeforeAfter", "CollectionFilter", "FilterTypes", "LimitOffset", "NotInCollectionFilter", "NotInSearchFilter", "OnBeforeAfter", "OrderBy", "SearchFilter", ) FilterTypes: TypeAlias = "BeforeAfter | OnBeforeAfter | CollectionFilter[Any] | LimitOffset | OrderBy | SearchFilter | NotInCollectionFilter[Any] | NotInSearchFilter" """Aggregate type alias of the types supported for collection filtering.""" @dataclass class BeforeAfter: """Data required to filter a query on a ``datetime`` column.""" field_name: str """Name of the model attribute to filter on.""" before: datetime | None """Filter results where field earlier than this.""" after: datetime | None """Filter results where field later than this.""" @dataclass class OnBeforeAfter: """Data required to filter a query on a ``datetime`` column.""" field_name: str """Name of the model attribute to filter on.""" on_or_before: datetime | None """Filter results where field is on or earlier than this.""" on_or_after: datetime | None """Filter results where field on or later than this.""" @dataclass class CollectionFilter(Generic[T]): """Data required to construct a ``WHERE ... IN (...)`` clause.""" field_name: str """Name of the model attribute to filter on.""" values: abc.Collection[T] """Values for ``IN`` clause.""" @dataclass class NotInCollectionFilter(Generic[T]): """Data required to construct a ``WHERE ... NOT IN (...)`` clause.""" field_name: str """Name of the model attribute to filter on.""" values: abc.Collection[T] """Values for ``NOT IN`` clause.""" @dataclass class LimitOffset: """Data required to add limit/offset filtering to a query.""" limit: int """Value for ``LIMIT`` clause of query.""" offset: int """Value for ``OFFSET`` clause of query.""" @dataclass class OrderBy: """Data required to construct a ``ORDER BY ...`` clause.""" field_name: str """Name of the model attribute to sort on.""" sort_order: Literal["asc", "desc"] = "asc" """Sort ascending or descending""" @dataclass class SearchFilter: """Data required to construct a ``WHERE field_name LIKE '%' || :value || '%'`` clause.""" field_name: str """Name of the model attribute to sort on.""" value: str """Values for ``LIKE`` clause.""" ignore_case: bool | None = False """Should the search be case insensitive.""" @dataclass class NotInSearchFilter: """Data required to construct a ``WHERE field_name NOT LIKE '%' || :value || '%'`` clause.""" field_name: str """Name of the model attribute to search on.""" value: str """Values for ``NOT LIKE`` clause.""" ignore_case: bool | None = False """Should the search be case insensitive.""" litestar-2.16.0/litestar/repository/abc/000077500000000000000000000000001500564371300202265ustar00rootroot00000000000000litestar-2.16.0/litestar/repository/abc/__init__.py000066400000000000000000000002421500564371300223350ustar00rootroot00000000000000from ._async import AbstractAsyncRepository from ._sync import AbstractSyncRepository __all__ = ( "AbstractAsyncRepository", "AbstractSyncRepository", ) litestar-2.16.0/litestar/repository/abc/_async.py000066400000000000000000000237641500564371300220700ustar00rootroot00000000000000from __future__ import annotations from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING, Any, Generic, TypeVar from litestar.repository.exceptions import NotFoundError if TYPE_CHECKING: from litestar.repository.filters import FilterTypes T = TypeVar("T") CollectionT = TypeVar("CollectionT") class AbstractAsyncRepository(Generic[T], metaclass=ABCMeta): """Interface for persistent data interaction.""" model_type: type[T] """Type of object represented by the repository.""" id_attribute: Any = "id" """Name of the primary identifying attribute on :attr:`model_type`.""" def __init__(self, **kwargs: Any) -> None: """Repository constructors accept arbitrary kwargs.""" super().__init__(**kwargs) @abstractmethod async def add(self, data: T) -> T: """Add ``data`` to the collection. Args: data: Instance to be added to the collection. Returns: The added instance. """ @abstractmethod async def add_many(self, data: list[T]) -> list[T]: """Add multiple ``data`` to the collection. Args: data: Instances to be added to the collection. Returns: The added instances. """ @abstractmethod async def count(self, *filters: FilterTypes, **kwargs: Any) -> int: """Get the count of records returned by a query. Args: *filters: Types for specific filtering operations. **kwargs: Instance attribute value filters. Returns: The count of instances """ @abstractmethod async def delete(self, item_id: Any) -> T: """Delete instance identified by ``item_id``. Args: item_id: Identifier of instance to be deleted. Returns: The deleted instance. Raises: NotFoundError: If no instance found identified by ``item_id``. """ @abstractmethod async def delete_many(self, item_ids: list[Any]) -> list[T]: """Delete multiple instances identified by list of IDs ``item_ids``. Args: item_ids: list of Identifiers to be deleted. Returns: The deleted instances. """ @abstractmethod async def exists(self, *filters: FilterTypes, **kwargs: Any) -> bool: """Return true if the object specified by ``kwargs`` exists. Args: *filters: Types for specific filtering operations. **kwargs: Identifier of the instance to be retrieved. Returns: True if the instance was found. False if not found. """ @abstractmethod async def get(self, item_id: Any, **kwargs: Any) -> T: """Get instance identified by ``item_id``. Args: item_id: Identifier of the instance to be retrieved. **kwargs: Additional arguments Returns: The retrieved instance. Raises: NotFoundError: If no instance found identified by ``item_id``. """ @abstractmethod async def get_one(self, **kwargs: Any) -> T: """Get an instance specified by the ``kwargs`` filters if it exists. Args: **kwargs: Instance attribute value filters. Returns: The retrieved instance. Raises: NotFoundError: If no instance found identified by ``kwargs``. """ @abstractmethod async def get_or_create(self, **kwargs: Any) -> tuple[T, bool]: """Get an instance specified by the ``kwargs`` filters if it exists or create it. Args: **kwargs: Instance attribute value filters. Returns: A tuple that includes the retrieved or created instance, and a boolean on whether the record was created or not """ @abstractmethod async def get_one_or_none(self, **kwargs: Any) -> T | None: """Get an instance if it exists or None. Args: **kwargs: Instance attribute value filters. Returns: The retrieved instance or None. """ @abstractmethod async def update(self, data: T) -> T: """Update instance with the attribute values present on ``data``. Args: data: An instance that should have a value for :attr:`id_attribute ` that exists in the collection. Returns: The updated instance. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ @abstractmethod async def update_many(self, data: list[T]) -> list[T]: """Update multiple instances with the attribute values present on instances in ``data``. Args: data: A list of instance that should have a value for :attr:`id_attribute ` that exists in the collection. Returns: a list of the updated instances. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ @abstractmethod async def upsert(self, data: T) -> T: """Update or create instance. Updates instance with the attribute values present on ``data``, or creates a new instance if one doesn't exist. Args: data: Instance to update existing, or be created. Identifier used to determine if an existing instance exists is the value of an attribute on ``data`` named as value of :attr:`id_attribute `. Returns: The updated or created instance. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ @abstractmethod async def upsert_many(self, data: list[T]) -> list[T]: """Update or create multiple instances. Update instances with the attribute values present on ``data``, or create a new instance if one doesn't exist. Args: data: Instances to update or created. Identifier used to determine if an existing instance exists is the value of an attribute on ``data`` named as value of :attr:`id_attribute `. Returns: The updated or created instances. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ @abstractmethod async def list_and_count(self, *filters: FilterTypes, **kwargs: Any) -> tuple[list[T], int]: """List records with total count. Args: *filters: Types for specific filtering operations. **kwargs: Instance attribute value filters. Returns: a tuple containing The list of instances, after filtering applied, and a count of records returned by query, ignoring pagination. """ @abstractmethod async def list(self, *filters: FilterTypes, **kwargs: Any) -> list[T]: """Get a list of instances, optionally filtered. Args: *filters: filters for specific filtering operations **kwargs: Instance attribute value filters. Returns: The list of instances, after filtering applied """ @abstractmethod def filter_collection_by_kwargs(self, collection: CollectionT, /, **kwargs: Any) -> CollectionT: """Filter the collection by kwargs. Has ``AND`` semantics where multiple kwargs name/value pairs are provided. Args: collection: the objects to be filtered **kwargs: key/value pairs such that objects remaining in the collection after filtering have the property that their attribute named ``key`` has value equal to ``value``. Returns: The filtered objects Raises: RepositoryError: if a named attribute doesn't exist on :attr:`model_type `. """ @staticmethod def check_not_found(item_or_none: T | None) -> T: """Raise :class:`NotFoundError` if ``item_or_none`` is ``None``. Args: item_or_none: Item (:class:`T `) to be tested for existence. Returns: The item, if it exists. """ if item_or_none is None: raise NotFoundError("No item found when one was expected") return item_or_none @classmethod def get_id_attribute_value(cls, item: T | type[T], id_attribute: str | None = None) -> Any: """Get value of attribute named as :attr:`id_attribute ` on ``item``. Args: item: Anything that should have an attribute named as :attr:`id_attribute ` value. id_attribute: Allows customization of the unique identifier to use for model fetching. Defaults to `None`, but can reference any surrogate or candidate key for the table. Returns: The value of attribute on ``item`` named as :attr:`id_attribute `. """ return getattr(item, id_attribute if id_attribute is not None else cls.id_attribute) @classmethod def set_id_attribute_value(cls, item_id: Any, item: T, id_attribute: str | None = None) -> T: """Return the ``item`` after the ID is set to the appropriate attribute. Args: item_id: Value of ID to be set on instance item: Anything that should have an attribute named as :attr:`id_attribute ` value. id_attribute: Allows customization of the unique identifier to use for model fetching. Defaults to `None`, but can reference any surrogate or candidate key for the table. Returns: Item with ``item_id`` set to :attr:`id_attribute ` """ setattr(item, id_attribute if id_attribute is not None else cls.id_attribute, item_id) return item litestar-2.16.0/litestar/repository/abc/_sync.py000066400000000000000000000237701500564371300217240ustar00rootroot00000000000000# Do not edit this file directly. It has been autogenerated from # litestar/repository/abc/_async.py from __future__ import annotations from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING, Any, Generic, TypeVar from litestar.repository.exceptions import NotFoundError if TYPE_CHECKING: from litestar.repository.filters import FilterTypes T = TypeVar("T") CollectionT = TypeVar("CollectionT") class AbstractSyncRepository(Generic[T], metaclass=ABCMeta): """Interface for persistent data interaction.""" model_type: type[T] """Type of object represented by the repository.""" id_attribute: Any = "id" """Name of the primary identifying attribute on :attr:`model_type`.""" def __init__(self, **kwargs: Any) -> None: """Repository constructors accept arbitrary kwargs.""" super().__init__(**kwargs) @abstractmethod def add(self, data: T) -> T: """Add ``data`` to the collection. Args: data: Instance to be added to the collection. Returns: The added instance. """ @abstractmethod def add_many(self, data: list[T]) -> list[T]: """Add multiple ``data`` to the collection. Args: data: Instances to be added to the collection. Returns: The added instances. """ @abstractmethod def count(self, *filters: FilterTypes, **kwargs: Any) -> int: """Get the count of records returned by a query. Args: *filters: Types for specific filtering operations. **kwargs: Instance attribute value filters. Returns: The count of instances """ @abstractmethod def delete(self, item_id: Any) -> T: """Delete instance identified by ``item_id``. Args: item_id: Identifier of instance to be deleted. Returns: The deleted instance. Raises: NotFoundError: If no instance found identified by ``item_id``. """ @abstractmethod def delete_many(self, item_ids: list[Any]) -> list[T]: """Delete multiple instances identified by list of IDs ``item_ids``. Args: item_ids: list of Identifiers to be deleted. Returns: The deleted instances. """ @abstractmethod def exists(self, *filters: FilterTypes, **kwargs: Any) -> bool: """Return true if the object specified by ``kwargs`` exists. Args: *filters: Types for specific filtering operations. **kwargs: Identifier of the instance to be retrieved. Returns: True if the instance was found. False if not found. """ @abstractmethod def get(self, item_id: Any, **kwargs: Any) -> T: """Get instance identified by ``item_id``. Args: item_id: Identifier of the instance to be retrieved. **kwargs: Additional arguments Returns: The retrieved instance. Raises: NotFoundError: If no instance found identified by ``item_id``. """ @abstractmethod def get_one(self, **kwargs: Any) -> T: """Get an instance specified by the ``kwargs`` filters if it exists. Args: **kwargs: Instance attribute value filters. Returns: The retrieved instance. Raises: NotFoundError: If no instance found identified by ``kwargs``. """ @abstractmethod def get_or_create(self, **kwargs: Any) -> tuple[T, bool]: """Get an instance specified by the ``kwargs`` filters if it exists or create it. Args: **kwargs: Instance attribute value filters. Returns: A tuple that includes the retrieved or created instance, and a boolean on whether the record was created or not """ @abstractmethod def get_one_or_none(self, **kwargs: Any) -> T | None: """Get an instance if it exists or None. Args: **kwargs: Instance attribute value filters. Returns: The retrieved instance or None. """ @abstractmethod def update(self, data: T) -> T: """Update instance with the attribute values present on ``data``. Args: data: An instance that should have a value for :attr:`id_attribute ` that exists in the collection. Returns: The updated instance. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ @abstractmethod def update_many(self, data: list[T]) -> list[T]: """Update multiple instances with the attribute values present on instances in ``data``. Args: data: A list of instance that should have a value for :attr:`id_attribute ` that exists in the collection. Returns: a list of the updated instances. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ @abstractmethod def upsert(self, data: T) -> T: """Update or create instance. Updates instance with the attribute values present on ``data``, or creates a new instance if one doesn't exist. Args: data: Instance to update existing, or be created. Identifier used to determine if an existing instance exists is the value of an attribute on ``data`` named as value of :attr:`id_attribute `. Returns: The updated or created instance. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ @abstractmethod def upsert_many(self, data: list[T]) -> list[T]: """Update or create multiple instances. Update instances with the attribute values present on ``data``, or create a new instance if one doesn't exist. Args: data: Instances to update or created. Identifier used to determine if an existing instance exists is the value of an attribute on ``data`` named as value of :attr:`id_attribute `. Returns: The updated or created instances. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ @abstractmethod def list_and_count(self, *filters: FilterTypes, **kwargs: Any) -> tuple[list[T], int]: """List records with total count. Args: *filters: Types for specific filtering operations. **kwargs: Instance attribute value filters. Returns: a tuple containing The list of instances, after filtering applied, and a count of records returned by query, ignoring pagination. """ @abstractmethod def list(self, *filters: FilterTypes, **kwargs: Any) -> list[T]: """Get a list of instances, optionally filtered. Args: *filters: filters for specific filtering operations **kwargs: Instance attribute value filters. Returns: The list of instances, after filtering applied """ @abstractmethod def filter_collection_by_kwargs(self, collection: CollectionT, /, **kwargs: Any) -> CollectionT: """Filter the collection by kwargs. Has ``AND`` semantics where multiple kwargs name/value pairs are provided. Args: collection: the objects to be filtered **kwargs: key/value pairs such that objects remaining in the collection after filtering have the property that their attribute named ``key`` has value equal to ``value``. Returns: The filtered objects Raises: RepositoryError: if a named attribute doesn't exist on :attr:`model_type `. """ @staticmethod def check_not_found(item_or_none: T | None) -> T: """Raise :class:`NotFoundError` if ``item_or_none`` is ``None``. Args: item_or_none: Item (:class:`T `) to be tested for existence. Returns: The item, if it exists. """ if item_or_none is None: raise NotFoundError("No item found when one was expected") return item_or_none @classmethod def get_id_attribute_value(cls, item: T | type[T], id_attribute: str | None = None) -> Any: """Get value of attribute named as :attr:`id_attribute ` on ``item``. Args: item: Anything that should have an attribute named as :attr:`id_attribute ` value. id_attribute: Allows customization of the unique identifier to use for model fetching. Defaults to `None`, but can reference any surrogate or candidate key for the table. Returns: The value of attribute on ``item`` named as :attr:`id_attribute `. """ return getattr(item, id_attribute if id_attribute is not None else cls.id_attribute) @classmethod def set_id_attribute_value(cls, item_id: Any, item: T, id_attribute: str | None = None) -> T: """Return the ``item`` after the ID is set to the appropriate attribute. Args: item_id: Value of ID to be set on instance item: Anything that should have an attribute named as :attr:`id_attribute ` value. id_attribute: Allows customization of the unique identifier to use for model fetching. Defaults to `None`, but can reference any surrogate or candidate key for the table. Returns: Item with ``item_id`` set to :attr:`id_attribute ` """ setattr(item, id_attribute if id_attribute is not None else cls.id_attribute, item_id) return item litestar-2.16.0/litestar/repository/exceptions.py000066400000000000000000000005541500564371300222400ustar00rootroot00000000000000try: from advanced_alchemy.exceptions import IntegrityError as ConflictError from advanced_alchemy.exceptions import NotFoundError, RepositoryError except ImportError: # pragma: no cover from ._exceptions import ConflictError, NotFoundError, RepositoryError # type: ignore[assignment] __all__ = ("ConflictError", "NotFoundError", "RepositoryError") litestar-2.16.0/litestar/repository/filters.py000066400000000000000000000013711500564371300215250ustar00rootroot00000000000000try: from advanced_alchemy.filters import ( BeforeAfter, CollectionFilter, FilterTypes, LimitOffset, NotInCollectionFilter, NotInSearchFilter, OnBeforeAfter, OrderBy, SearchFilter, ) except ImportError: from ._filters import ( # type: ignore[assignment] BeforeAfter, CollectionFilter, FilterTypes, LimitOffset, NotInCollectionFilter, NotInSearchFilter, OnBeforeAfter, OrderBy, SearchFilter, ) __all__ = ( "BeforeAfter", "CollectionFilter", "FilterTypes", "LimitOffset", "NotInCollectionFilter", "NotInSearchFilter", "OnBeforeAfter", "OrderBy", "SearchFilter", ) litestar-2.16.0/litestar/repository/handlers.py000066400000000000000000000017041500564371300216550ustar00rootroot00000000000000from typing import TYPE_CHECKING from litestar.repository.filters import ( BeforeAfter, CollectionFilter, FilterTypes, LimitOffset, NotInCollectionFilter, NotInSearchFilter, OnBeforeAfter, OrderBy, SearchFilter, ) if TYPE_CHECKING: from litestar.config.app import AppConfig __all__ = ("on_app_init", "signature_namespace_values") signature_namespace_values = { "BeforeAfter": BeforeAfter, "OnBeforeAfter": OnBeforeAfter, "CollectionFilter": CollectionFilter, "LimitOffset": LimitOffset, "OrderBy": OrderBy, "SearchFilter": SearchFilter, "NotInCollectionFilter": NotInCollectionFilter, "NotInSearchFilter": NotInSearchFilter, "FilterTypes": FilterTypes, } def on_app_init(app_config: "AppConfig") -> "AppConfig": """Add custom filters for the application during signature modelling.""" app_config.signature_namespace.update(signature_namespace_values) return app_config litestar-2.16.0/litestar/repository/testing/000077500000000000000000000000001500564371300211565ustar00rootroot00000000000000litestar-2.16.0/litestar/repository/testing/__init__.py000066400000000000000000000000001500564371300232550ustar00rootroot00000000000000litestar-2.16.0/litestar/repository/testing/generic_mock_repository.py000066400000000000000000000672721500564371300264720ustar00rootroot00000000000000"""A repository implementation for tests. Uses a `dict` for storage. """ from __future__ import annotations from datetime import datetime, timezone, tzinfo from typing import TYPE_CHECKING, Generic, Protocol, TypeVar from uuid import uuid4 from litestar.repository import AbstractAsyncRepository, AbstractSyncRepository, FilterTypes from litestar.repository.exceptions import ConflictError, RepositoryError if TYPE_CHECKING: from collections.abc import Callable, Hashable, Iterable, MutableMapping from typing import Any __all__ = ("GenericAsyncMockRepository", "GenericSyncMockRepository") class HasID(Protocol): id: Any ModelT = TypeVar("ModelT", bound="HasID") AsyncMockRepoT = TypeVar("AsyncMockRepoT", bound="GenericAsyncMockRepository") SyncMockRepoT = TypeVar("SyncMockRepoT", bound="GenericSyncMockRepository") class GenericAsyncMockRepository(AbstractAsyncRepository[ModelT], Generic[ModelT]): """A repository implementation for tests. Uses a :class:`dict` for storage. """ collection: MutableMapping[Hashable, ModelT] model_type: type[ModelT] match_fields: list[str] | str | None = None _model_has_created_at: bool _model_has_updated_at: bool def __init__( self, id_factory: Callable[[], Any] = uuid4, tz: tzinfo = timezone.utc, allow_ids_on_add: bool = False, **_: Any ) -> None: super().__init__() self._id_factory = id_factory self.tz = tz self.allow_ids_on_add = allow_ids_on_add @classmethod def __class_getitem__(cls: type[AsyncMockRepoT], item: type[ModelT]) -> type[AsyncMockRepoT]: """Add collection to ``_collections`` for the type. Args: item: The type that the class has been parametrized with. """ return type( # pyright:ignore f"{cls.__name__}[{item.__name__}]", (cls,), { "collection": {}, "model_type": item, "_model_has_created_at": hasattr(item, "created_at"), "_model_has_updated_at": hasattr(item, "updated_at"), }, ) def _find_or_raise_not_found(self, item_id: Any) -> ModelT: return self.check_not_found(self.collection.get(item_id)) def _find_or_none(self, item_id: Any) -> ModelT | None: return self.collection.get(item_id) def _now(self) -> datetime: return datetime.now(tz=self.tz).replace(tzinfo=None) def _update_audit_attributes(self, data: ModelT, now: datetime | None = None, do_created: bool = False) -> ModelT: now = now or self._now() if self._model_has_updated_at: data.updated_at = now # type:ignore[attr-defined] if do_created: data.created_at = now # type:ignore[attr-defined] return data async def add(self, data: ModelT) -> ModelT: """Add ``data`` to the collection. Args: data: Instance to be added to the collection. Returns: The added instance. """ if self.allow_ids_on_add is False and self.get_id_attribute_value(data) is not None: raise ConflictError("`add()` received identified item.") self._update_audit_attributes(data, do_created=True) if self.allow_ids_on_add is False: id_ = self._id_factory() self.set_id_attribute_value(id_, data) self.collection[data.id] = data return data async def add_many(self, data: Iterable[ModelT]) -> list[ModelT]: """Add multiple ``data`` to the collection. Args: data: Instance to be added to the collection. Returns: The added instance. """ now = self._now() for data_row in data: if self.allow_ids_on_add is False and self.get_id_attribute_value(data_row) is not None: raise ConflictError("`add()` received identified item.") self._update_audit_attributes(data_row, do_created=True, now=now) if self.allow_ids_on_add is False: id_ = self._id_factory() self.set_id_attribute_value(id_, data_row) self.collection[data_row.id] = data_row return list(data) async def delete(self, item_id: Any) -> ModelT: """Delete instance identified by ``item_id``. Args: item_id: Identifier of instance to be deleted. Returns: The deleted instance. Raises: NotFoundError: If no instance found identified by ``item_id``. """ try: return self._find_or_raise_not_found(item_id) finally: del self.collection[item_id] async def delete_many(self, item_ids: list[Any]) -> list[ModelT]: """Delete instances identified by list of identifiers ``item_ids``. Args: item_ids: list of identifiers of instances to be deleted. Returns: The deleted instances. """ instances: list[ModelT] = [] for item_id in item_ids: obj = await self.get_one_or_none(**{self.id_attribute: item_id}) if obj: obj = await self.delete(obj.id) instances.append(obj) return instances async def exists(self, *filters: FilterTypes, **kwargs: Any) -> bool: """Return true if the object specified by ``kwargs`` exists. Args: *filters: Types for specific filtering operations. **kwargs: Identifier of the instance to be retrieved. Returns: True if the instance was found. False if not found.. """ existing = await self.count(*filters, **kwargs) return bool(existing) async def get(self, item_id: Any, **kwargs: Any) -> ModelT: """Get instance identified by ``item_id``. Args: item_id: Identifier of the instance to be retrieved. **kwargs: additional arguments Returns: The retrieved instance. Raises: NotFoundError: If no instance found identified by ``item_id``. """ return self._find_or_raise_not_found(item_id) async def get_or_create(self, match_fields: list[str] | str | None = None, **kwargs: Any) -> tuple[ModelT, bool]: """Get instance identified by ``kwargs`` or create if it doesn't exist. Args: match_fields: a list of keys to use to match the existing model. When empty, all fields are matched. **kwargs: Identifier of the instance to be retrieved. Returns: a tuple that includes the instance and whether it needed to be created. """ match_fields = match_fields or self.match_fields if isinstance(match_fields, str): match_fields = [match_fields] if match_fields: match_filter = { field_name: field_value for field_name in match_fields if (field_value := kwargs.get(field_name)) is not None } else: match_filter = kwargs existing = await self.get_one_or_none(**match_filter) if existing: for field_name, new_field_value in kwargs.items(): field = getattr(existing, field_name, None) if field and field != new_field_value: setattr(existing, field_name, new_field_value) return existing, False return await self.add(self.model_type(**kwargs)), True # pyright: ignore[reportGeneralTypeIssues] async def get_one(self, **kwargs: Any) -> ModelT: """Get instance identified by query filters. Args: **kwargs: Instance attribute value filters. Returns: The retrieved instance or None Raises: NotFoundError: If no instance found identified by ``kwargs``. """ data = await self.list(**kwargs) return self.check_not_found(data[0] if data else None) async def get_one_or_none(self, **kwargs: Any) -> ModelT | None: """Get instance identified by query filters or None if not found. Args: **kwargs: Instance attribute value filters. Returns: The retrieved instance or None """ data = await self.list(**kwargs) return data[0] if data else None async def count(self, *filters: FilterTypes, **kwargs: Any) -> int: """Count of rows returned by query. Args: *filters: Types for specific filtering operations. **kwargs: Instance attribute value filters. Returns: Count of instances in collection, ignoring pagination. """ return len(await self.list(*filters, **kwargs)) async def update(self, data: ModelT) -> ModelT: """Update instance with the attribute values present on ``data``. Args: data: An instance that should have a value for :attr:`id_attribute ` that exists in the collection. Returns: The updated instance. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ item = self._find_or_raise_not_found(self.get_id_attribute_value(data)) self._update_audit_attributes(data, do_created=False) for key, val in model_items(data): setattr(item, key, val) return item async def update_many(self, data: list[ModelT]) -> list[ModelT]: """Update instances with the attribute values present on ``data``. Args: data: A list of instances that should have a value for :attr:`id_attribute ` that exists in the collection. Returns: The updated instances. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ items = [self._find_or_raise_not_found(self.get_id_attribute_value(row)) for row in data] now = self._now() for item in items: self._update_audit_attributes(item, do_created=False, now=now) for key, val in model_items(item): setattr(item, key, val) return items async def upsert(self, data: ModelT) -> ModelT: """Update or create instance. Updates instance with the attribute values present on ``data``, or creates a new instance if one doesn't exist. Args: data: Instance to update existing, or be created. Identifier used to determine if an existing instance exists is the value of an attribute on `data` named as value of :attr:`id_attribute `. Returns: The updated or created instance. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ item_id = self.get_id_attribute_value(data) if item_id in self.collection: return await self.update(data) return await self.add(data) async def upsert_many(self, data: list[ModelT]) -> list[ModelT]: """Update or create multiple instance. Update instance with the attribute values present on ``data``, or create a new instance if one doesn't exist. Args: data: List of instances to update existing, or be created. Identifier used to determine if an existing instance exists is the value of an attribute on `data` named as value of :attr:`id_attribute `. Returns: The updated or created instances. """ data_to_update = [row for row in data if self._find_or_none(self.get_id_attribute_value(row)) is not None] data_to_add = [row for row in data if self._find_or_none(self.get_id_attribute_value(row)) is None] updated_items = await self.update_many(data_to_update) added_items = await self.add_many(data_to_add) return updated_items + added_items async def list_and_count( self, *filters: FilterTypes, **kwargs: Any, ) -> tuple[list[ModelT], int]: """Get a list of instances, optionally filtered with a total row count. Args: *filters: Types for specific filtering operations. **kwargs: Instance attribute value filters. Returns: List of instances, and count of records returned by query, ignoring pagination. """ return await self.list(*filters, **kwargs), await self.count(*filters, **kwargs) async def list(self, *filters: FilterTypes, **kwargs: Any) -> list[ModelT]: """Get a list of instances, optionally filtered. Args: *filters: Types for specific filtering operations. **kwargs: Instance attribute value filters. Returns: The list of instances, after filtering applied. """ return list(self.filter_collection_by_kwargs(self.collection, **kwargs).values()) def filter_collection_by_kwargs( # type:ignore[override] self, collection: MutableMapping[Hashable, ModelT], /, **kwargs: Any ) -> MutableMapping[Hashable, ModelT]: """Filter the collection by kwargs. Args: collection: set of objects to filter **kwargs: key/value pairs such that objects remaining in the collection after filtering have the property that their attribute named ``key`` has value equal to ``value``. """ new_collection: dict[Hashable, ModelT] = {} for item in self.collection.values(): try: if all(getattr(item, name) == value for name, value in kwargs.items()): new_collection[item.id] = item except AttributeError as orig: raise RepositoryError from orig return new_collection @classmethod def seed_collection(cls, instances: Iterable[ModelT]) -> None: """Seed the collection for repository type. Args: instances: the instances to be added to the collection. """ for instance in instances: cls.collection[cls.get_id_attribute_value(instance)] = instance @classmethod def clear_collection(cls) -> None: """Empty the collection for repository type.""" cls.collection = {} class GenericSyncMockRepository(AbstractSyncRepository[ModelT], Generic[ModelT]): """A repository implementation for tests. Uses a :class:`dict` for storage. """ collection: MutableMapping[Hashable, ModelT] model_type: type[ModelT] match_fields: list[str] | str | None = None _model_has_created_at: bool _model_has_updated_at: bool def __init__( self, id_factory: Callable[[], Any] = uuid4, tz: tzinfo = timezone.utc, allow_ids_on_add: bool = False, **_: Any, ) -> None: super().__init__() self._id_factory = id_factory self.tz = tz self.allow_ids_on_add = allow_ids_on_add @classmethod def __class_getitem__(cls: type[SyncMockRepoT], item: type[ModelT]) -> type[SyncMockRepoT]: """Add collection to ``_collections`` for the type. Args: item: The type that the class has been parametrized with. """ return type( # pyright:ignore f"{cls.__name__}[{item.__name__}]", (cls,), { "collection": {}, "model_type": item, "_model_has_created_at": hasattr(item, "created_at"), "_model_has_updated_at": hasattr(item, "updated_at"), }, ) def _find_or_raise_not_found(self, item_id: Any) -> ModelT: return self.check_not_found(self.collection.get(item_id)) def _find_or_none(self, item_id: Any) -> ModelT | None: return self.collection.get(item_id) def _now(self) -> datetime: return datetime.now(tz=self.tz).replace(tzinfo=None) def _update_audit_attributes(self, data: ModelT, now: datetime | None = None, do_created: bool = False) -> ModelT: now = now or self._now() if self._model_has_updated_at: data.updated_at = now # type:ignore[attr-defined] if do_created: data.created_at = now # type:ignore[attr-defined] return data def add(self, data: ModelT) -> ModelT: """Add ``data`` to the collection. Args: data: Instance to be added to the collection. Returns: The added instance. """ if self.allow_ids_on_add is False and self.get_id_attribute_value(data) is not None: raise ConflictError("`add()` received identified item.") self._update_audit_attributes(data, do_created=True) if self.allow_ids_on_add is False: id_ = self._id_factory() self.set_id_attribute_value(id_, data) self.collection[data.id] = data return data def add_many(self, data: Iterable[ModelT]) -> list[ModelT]: """Add multiple ``data`` to the collection. Args: data: Instance to be added to the collection. Returns: The added instance. """ now = self._now() for data_row in data: if self.allow_ids_on_add is False and self.get_id_attribute_value(data_row) is not None: raise ConflictError("`add()` received identified item.") self._update_audit_attributes(data_row, do_created=True, now=now) if self.allow_ids_on_add is False: id_ = self._id_factory() self.set_id_attribute_value(id_, data_row) self.collection[data_row.id] = data_row return list(data) def delete(self, item_id: Any) -> ModelT: """Delete instance identified by ``item_id``. Args: item_id: Identifier of instance to be deleted. Returns: The deleted instance. Raises: NotFoundError: If no instance found identified by ``item_id``. """ try: return self._find_or_raise_not_found(item_id) finally: del self.collection[item_id] def delete_many(self, item_ids: list[Any]) -> list[ModelT]: """Delete instances identified by list of identifiers ``item_ids``. Args: item_ids: list of identifiers of instances to be deleted. Returns: The deleted instances. """ instances: list[ModelT] = [] for item_id in item_ids: if obj := self.get_one_or_none(**{self.id_attribute: item_id}): obj = self.delete(obj.id) instances.append(obj) return instances def exists(self, *filters: FilterTypes, **kwargs: Any) -> bool: """Return true if the object specified by ``kwargs`` exists. Args: *filters: Types for specific filtering operations. **kwargs: Identifier of the instance to be retrieved. Returns: True if the instance was found. False if not found.. """ existing = self.count(*filters, **kwargs) return bool(existing) def get(self, item_id: Any, **kwargs: Any) -> ModelT: """Get instance identified by ``item_id``. Args: item_id: Identifier of the instance to be retrieved. **kwargs: additional arguments Returns: The retrieved instance. Raises: NotFoundError: If no instance found identified by ``item_id``. """ return self._find_or_raise_not_found(item_id) def get_or_create(self, match_fields: list[str] | str | None = None, **kwargs: Any) -> tuple[ModelT, bool]: """Get instance identified by ``kwargs`` or create if it doesn't exist. Args: match_fields: a list of keys to use to match the existing model. When empty, all fields are matched. **kwargs: Identifier of the instance to be retrieved. Returns: a tuple that includes the instance and whether it needed to be created. """ match_fields = match_fields or self.match_fields if isinstance(match_fields, str): match_fields = [match_fields] if match_fields: match_filter = { field_name: field_value for field_name in match_fields if (field_value := kwargs.get(field_name)) is not None } else: match_filter = kwargs if existing := self.get_one_or_none(**match_filter): for field_name, new_field_value in kwargs.items(): field = getattr(existing, field_name, None) if field and field != new_field_value: setattr(existing, field_name, new_field_value) return existing, False return self.add(self.model_type(**kwargs)), True # pyright: ignore[reportGeneralTypeIssues] def get_one(self, **kwargs: Any) -> ModelT: """Get instance identified by query filters. Args: **kwargs: Instance attribute value filters. Returns: The retrieved instance or None Raises: NotFoundError: If no instance found identified by ``kwargs``. """ data = self.list(**kwargs) return self.check_not_found(data[0] if data else None) def get_one_or_none(self, **kwargs: Any) -> ModelT | None: """Get instance identified by query filters or None if not found. Args: **kwargs: Instance attribute value filters. Returns: The retrieved instance or None """ data = self.list(**kwargs) return data[0] if data else None def count(self, *filters: FilterTypes, **kwargs: Any) -> int: """Count of rows returned by query. Args: *filters: Types for specific filtering operations. **kwargs: Instance attribute value filters. Returns: Count of instances in collection, ignoring pagination. """ return len(self.list(*filters, **kwargs)) def update(self, data: ModelT) -> ModelT: """Update instance with the attribute values present on ``data``. Args: data: An instance that should have a value for :attr:`id_attribute ` that exists in the collection. Returns: The updated instance. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ item = self._find_or_raise_not_found(self.get_id_attribute_value(data)) self._update_audit_attributes(data, do_created=False) for key, val in model_items(data): setattr(item, key, val) return item def update_many(self, data: list[ModelT]) -> list[ModelT]: """Update instances with the attribute values present on ``data``. Args: data: A list of instances that should have a value for :attr:`id_attribute ` that exists in the collection. Returns: The updated instances. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ items = [self._find_or_raise_not_found(self.get_id_attribute_value(row)) for row in data] now = self._now() for item in items: self._update_audit_attributes(item, do_created=False, now=now) for key, val in model_items(item): setattr(item, key, val) return items def upsert(self, data: ModelT) -> ModelT: """Update or create instance. Updates instance with the attribute values present on ``data``, or creates a new instance if one doesn't exist. Args: data: Instance to update existing, or be created. Identifier used to determine if an existing instance exists is the value of an attribute on `data` named as value of :attr:`id_attribute `. Returns: The updated or created instance. Raises: NotFoundError: If no instance found with same identifier as ``data``. """ item_id = self.get_id_attribute_value(data) return self.update(data) if item_id in self.collection else self.add(data) def upsert_many(self, data: list[ModelT]) -> list[ModelT]: """Update or create multiple instance. Update instance with the attribute values present on ``data``, or create a new instance if one doesn't exist. Args: data: List of instances to update existing, or be created. Identifier used to determine if an existing instance exists is the value of an attribute on `data` named as value of :attr:`id_attribute `. Returns: The updated or created instances. """ data_to_update = [row for row in data if self._find_or_none(self.get_id_attribute_value(row)) is not None] data_to_add = [row for row in data if self._find_or_none(self.get_id_attribute_value(row)) is None] updated_items = self.update_many(data_to_update) added_items = self.add_many(data_to_add) return updated_items + added_items def list_and_count( self, *filters: FilterTypes, **kwargs: Any, ) -> tuple[list[ModelT], int]: """Get a list of instances, optionally filtered with a total row count. Args: *filters: Types for specific filtering operations. **kwargs: Instance attribute value filters. Returns: List of instances, and count of records returned by query, ignoring pagination. """ return self.list(*filters, **kwargs), self.count(*filters, **kwargs) def list(self, *filters: FilterTypes, **kwargs: Any) -> list[ModelT]: """Get a list of instances, optionally filtered. Args: *filters: Types for specific filtering operations. **kwargs: Instance attribute value filters. Returns: The list of instances, after filtering applied. """ return list(self.filter_collection_by_kwargs(self.collection, **kwargs).values()) def filter_collection_by_kwargs( # type:ignore[override] self, collection: MutableMapping[Hashable, ModelT], /, **kwargs: Any ) -> MutableMapping[Hashable, ModelT]: """Filter the collection by kwargs. Args: collection: set of objects to filter **kwargs: key/value pairs such that objects remaining in the collection after filtering have the property that their attribute named ``key`` has value equal to ``value``. """ new_collection: dict[Hashable, ModelT] = {} for item in self.collection.values(): try: if all(getattr(item, name) == value for name, value in kwargs.items()): new_collection[item.id] = item except AttributeError as orig: raise RepositoryError from orig return new_collection @classmethod def seed_collection(cls, instances: Iterable[ModelT]) -> None: """Seed the collection for repository type. Args: instances: the instances to be added to the collection. """ for instance in instances: cls.collection[cls.get_id_attribute_value(instance)] = instance @classmethod def clear_collection(cls) -> None: """Empty the collection for repository type.""" cls.collection = {} def model_items(model: Any) -> list[tuple[str, Any]]: return [(k, v) for k, v in model.__dict__.items() if not k.startswith("_")] litestar-2.16.0/litestar/response/000077500000000000000000000000001500564371300171205ustar00rootroot00000000000000litestar-2.16.0/litestar/response/__init__.py000066400000000000000000000005251500564371300212330ustar00rootroot00000000000000from .base import Response from .file import File from .redirect import Redirect from .sse import ServerSentEvent, ServerSentEventMessage from .streaming import Stream from .template import Template __all__ = ( "File", "Redirect", "Response", "ServerSentEvent", "ServerSentEventMessage", "Stream", "Template", ) litestar-2.16.0/litestar/response/base.py000066400000000000000000000405261500564371300204130ustar00rootroot00000000000000from __future__ import annotations import itertools import re from typing import TYPE_CHECKING, Any, ClassVar, Generic, Iterable, Literal, Mapping, TypeVar, overload from litestar.datastructures.cookie import Cookie from litestar.datastructures.headers import ETag, MutableScopeHeaders from litestar.enums import MediaType, OpenAPIMediaType from litestar.exceptions import ImproperlyConfiguredException from litestar.serialization import default_serializer, encode_json, encode_msgpack, get_serializer from litestar.status_codes import HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_304_NOT_MODIFIED from litestar.types.empty import Empty from litestar.utils.deprecation import deprecated, warn_deprecation from litestar.utils.helpers import get_enum_string_value if TYPE_CHECKING: from typing import Optional from litestar.app import Litestar from litestar.background_tasks import BackgroundTask, BackgroundTasks from litestar.connection import Request from litestar.types import ( HTTPResponseBodyEvent, HTTPResponseStartEvent, Receive, ResponseCookies, ResponseHeaders, Scope, Send, Serializer, TypeEncodersMap, ) __all__ = ("ASGIResponse", "Response") T = TypeVar("T") MEDIA_TYPE_APPLICATION_JSON_PATTERN = re.compile(r"^application/(?:.+\+)?json") class ASGIResponse: """A low-level ASGI response class.""" __slots__ = ( "_encoded_cookies", "background", "body", "content_length", "encoding", "headers", "is_head_response", "status_code", ) _should_set_content_length: ClassVar[bool] = True """A flag to indicate whether the content-length header should be set by default or not.""" def __init__( self, *, background: BackgroundTask | BackgroundTasks | None = None, body: bytes | str = b"", content_length: int | None = None, cookies: Iterable[Cookie] | None = None, encoded_headers: Iterable[tuple[bytes, bytes]] | None = None, encoding: str = "utf-8", headers: dict[str, Any] | Iterable[tuple[str, str]] | None = None, is_head_response: bool = False, media_type: MediaType | str | None = None, status_code: int | None = None, ) -> None: """A low-level ASGI response class. Args: background: A background task or a list of background tasks to be executed after the response is sent. body: encoded content to send in the response body. content_length: The response content length. cookies: The response cookies. encoded_headers: The response headers. encoding: The response encoding. headers: The response headers. is_head_response: A boolean indicating if the response is a HEAD response. media_type: The response media type. status_code: The response status code. """ body = body.encode() if isinstance(body, str) else body status_code = status_code or HTTP_200_OK self.headers = MutableScopeHeaders() if encoded_headers is not None: warn_deprecation("3.0", kind="parameter", deprecated_name="encoded_headers", alternative="headers") for header_name, header_value in encoded_headers: self.headers.add(header_name.decode("latin-1"), header_value.decode("latin-1")) if headers is not None: for k, v in headers.items() if isinstance(headers, dict) else headers: self.headers.add(k, v) # pyright: ignore media_type = get_enum_string_value(media_type or MediaType.JSON) status_allows_body = ( status_code not in {HTTP_204_NO_CONTENT, HTTP_304_NOT_MODIFIED} and status_code >= HTTP_200_OK ) if content_length is None: content_length = len(body) if not status_allows_body or is_head_response: if body and body != b"null": raise ImproperlyConfiguredException( "response content is not supported for HEAD responses and responses with a status code " "that does not allow content (304, 204, < 200)" ) body = b"" else: self.headers.setdefault( "content-type", (f"{media_type}; charset={encoding}" if media_type.startswith("text/") else media_type) ) if self._should_set_content_length: self.headers.setdefault("content-length", str(content_length)) self.background = background self.body = body self.content_length = content_length self._encoded_cookies = tuple( cookie.to_encoded_header() for cookie in (cookies or ()) if not cookie.documentation_only ) self.encoding = encoding self.is_head_response = is_head_response self.status_code = status_code @property @deprecated("3.0", kind="property", alternative="encode_headers()") def encoded_headers(self) -> list[tuple[bytes, bytes]]: return self.encode_headers() def encode_headers(self) -> list[tuple[bytes, bytes]]: return [*self.headers.headers, *self._encoded_cookies] async def after_response(self) -> None: """Execute after the response is sent. Returns: None """ if self.background is not None: await self.background() async def start_response(self, send: Send) -> None: """Emit the start event of the response. This event includes the headers and status codes. Args: send: The ASGI send function. Returns: None """ event: HTTPResponseStartEvent = { "type": "http.response.start", "status": self.status_code, "headers": self.encode_headers(), } await send(event) async def send_body(self, send: Send, receive: Receive) -> None: """Emit the response body. Args: send: The ASGI send function. receive: The ASGI receive function. Notes: - Response subclasses should customize this method if there is a need to customize sending data. Returns: None """ event: HTTPResponseBodyEvent = {"type": "http.response.body", "body": self.body, "more_body": False} await send(event) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI callable of the ``Response``. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ await self.start_response(send=send) if self.is_head_response: event: HTTPResponseBodyEvent = {"type": "http.response.body", "body": b"", "more_body": False} await send(event) else: await self.send_body(send=send, receive=receive) await self.after_response() class Response(Generic[T]): """Base Litestar HTTP response class, used as the basis for all other response classes.""" __slots__ = ( "background", "content", "cookies", "encoding", "headers", "media_type", "response_type_encoders", "status_code", ) content: T type_encoders: Optional[TypeEncodersMap] = None # noqa: UP007 def __init__( self, content: T, *, background: BackgroundTask | BackgroundTasks | None = None, cookies: ResponseCookies | None = None, encoding: str = "utf-8", headers: ResponseHeaders | None = None, media_type: MediaType | OpenAPIMediaType | str | None = None, status_code: int | None = None, type_encoders: TypeEncodersMap | None = None, ) -> None: """Initialize the response. Args: content: A value for the response body that will be rendered into bytes string. status_code: An HTTP status code. media_type: A value for the response ``Content-Type`` header. background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. Defaults to ``None``. headers: A string keyed dictionary of response headers. Header keys are insensitive. cookies: A list of :class:`Cookie <.datastructures.Cookie>` instances to be set under the response ``Set-Cookie`` header. encoding: The encoding to be used for the response headers. type_encoders: A mapping of types to callables that transform them into types supported for serialization. """ self.content = content self.background = background self.cookies: list[Cookie] = ( [Cookie(key=key, value=value) for key, value in cookies.items()] if isinstance(cookies, Mapping) else list(cookies or []) ) self.encoding = encoding self.headers: dict[str, Any] = ( dict(headers) if isinstance(headers, Mapping) else {h.name: h.value for h in headers or {}} ) self.media_type = media_type self.status_code = status_code self.response_type_encoders = {**(self.type_encoders or {}), **(type_encoders or {})} @overload def set_cookie(self, /, cookie: Cookie) -> None: ... @overload def set_cookie( self, key: str, value: str | None = None, max_age: int | None = None, expires: int | None = None, path: str = "/", domain: str | None = None, secure: bool = False, httponly: bool = False, samesite: Literal["lax", "strict", "none"] = "lax", ) -> None: ... def set_cookie( # type: ignore[misc] self, key: str | Cookie, value: str | None = None, max_age: int | None = None, expires: int | None = None, path: str = "/", domain: str | None = None, secure: bool = False, httponly: bool = False, samesite: Literal["lax", "strict", "none"] = "lax", ) -> None: """Set a cookie on the response. If passed a :class:`Cookie <.datastructures.Cookie>` instance, keyword arguments will be ignored. Args: key: Key for the cookie or a :class:`Cookie <.datastructures.Cookie>` instance. value: Value for the cookie, if none given defaults to empty string. max_age: Maximal age of the cookie before its invalidated. expires: Seconds from now until the cookie expires. path: Path fragment that must exist in the request url for the cookie to be valid. Defaults to ``/``. domain: Domain for which the cookie is valid. secure: Https is required for the cookie. httponly: Forbids javascript to access the cookie via ``document.cookie``. samesite: Controls whether a cookie is sent with cross-site requests. Defaults to ``lax``. Returns: None. """ if not isinstance(key, Cookie): key = Cookie( domain=domain, expires=expires, httponly=httponly, key=key, max_age=max_age, path=path, samesite=samesite, secure=secure, value=value, ) self.cookies.append(key) def set_header(self, key: str, value: Any) -> None: """Set a header on the response. Args: key: Header key. value: Header value. Returns: None. """ self.headers[key] = value def set_etag(self, etag: str | ETag) -> None: """Set an etag header. Args: etag: An etag value. Returns: None """ self.headers["etag"] = etag.to_header() if isinstance(etag, ETag) else etag def delete_cookie( self, key: str, path: str = "/", domain: str | None = None, ) -> None: """Delete a cookie. Args: key: Key of the cookie. path: Path of the cookie. domain: Domain of the cookie. Returns: None. """ cookie = Cookie(key=key, path=path, domain=domain, expires=0, max_age=0) self.cookies = [c for c in self.cookies if c != cookie] self.cookies.append(cookie) def render(self, content: Any, media_type: str, enc_hook: Serializer = default_serializer) -> bytes: """Handle the rendering of content into a bytes string. Returns: An encoded bytes string """ if isinstance(content, bytes): return content if content is Empty: raise RuntimeError("The `Empty` sentinel cannot be used as response content") try: if media_type.startswith("text/") and not content: return b"" if isinstance(content, str): return content.encode(self.encoding) if media_type == MediaType.MESSAGEPACK: return encode_msgpack(content, enc_hook) if MEDIA_TYPE_APPLICATION_JSON_PATTERN.match( media_type, ): return encode_json(content, enc_hook) raise ImproperlyConfiguredException(f"unsupported media_type {media_type} for content {content!r}") except (AttributeError, ValueError, TypeError) as e: raise ImproperlyConfiguredException("Unable to serialize response content") from e def to_asgi_response( self, app: Litestar | None, request: Request, *, background: BackgroundTask | BackgroundTasks | None = None, cookies: Iterable[Cookie] | None = None, encoded_headers: Iterable[tuple[bytes, bytes]] | None = None, headers: dict[str, str] | None = None, is_head_response: bool = False, media_type: MediaType | str | None = None, status_code: int | None = None, type_encoders: TypeEncodersMap | None = None, ) -> ASGIResponse: """Create an ASGIResponse from a Response instance. Args: app: The :class:`Litestar <.app.Litestar>` application instance. background: Background task(s) to be executed after the response is sent. cookies: A list of cookies to be set on the response. encoded_headers: A list of already encoded headers. headers: Additional headers to be merged with the response headers. Response headers take precedence. is_head_response: Whether the response is a HEAD response. media_type: Media type for the response. If ``media_type`` is already set on the response, this is ignored. request: The :class:`Request <.connection.Request>` instance. status_code: Status code for the response. If ``status_code`` is already set on the response, this is type_encoders: A dictionary of type encoders to use for encoding the response content. Returns: An ASGIResponse instance. """ if app is not None: warn_deprecation( version="2.1", deprecated_name="app", kind="parameter", removal_in="3.0.0", alternative="request.app", ) headers = {**headers, **self.headers} if headers is not None else self.headers cookies = self.cookies if cookies is None else itertools.chain(self.cookies, cookies) if type_encoders: type_encoders = {**type_encoders, **(self.response_type_encoders or {})} else: type_encoders = self.response_type_encoders media_type = get_enum_string_value(self.media_type or media_type or MediaType.JSON) return ASGIResponse( background=self.background or background, body=self.render(self.content, media_type, get_serializer(type_encoders)), cookies=cookies, encoded_headers=encoded_headers, encoding=self.encoding, headers=headers, is_head_response=is_head_response, media_type=media_type, status_code=self.status_code or status_code, ) litestar-2.16.0/litestar/response/file.py000066400000000000000000000401771500564371300204220ustar00rootroot00000000000000from __future__ import annotations import itertools from datetime import datetime from email.utils import formatdate from inspect import iscoroutine from mimetypes import encodings_map, guess_type from typing import TYPE_CHECKING, Any, AsyncGenerator, Coroutine, Final, Iterable, Literal, cast from urllib.parse import quote from zlib import adler32 from litestar.constants import ONE_MEGABYTE from litestar.exceptions import ImproperlyConfiguredException from litestar.file_system import BaseLocalFileSystem, FileSystemAdapter from litestar.response.base import Response from litestar.response.streaming import ASGIStreamingResponse from litestar.utils.deprecation import warn_deprecation from litestar.utils.helpers import get_enum_string_value if TYPE_CHECKING: from os import PathLike from os import stat_result as stat_result_type from anyio import Path from litestar.app import Litestar from litestar.background_tasks import BackgroundTask, BackgroundTasks from litestar.connection import Request from litestar.datastructures.cookie import Cookie from litestar.datastructures.headers import ETag from litestar.enums import MediaType from litestar.types import ( HTTPResponseBodyEvent, PathType, Receive, ResponseCookies, ResponseHeaders, Send, TypeEncodersMap, ) from litestar.types.file_types import FileInfo, FileSystemProtocol __all__ = ( "ASGIFileResponse", "File", "async_file_iterator", "create_etag_for_file", ) # brotli not supported in 'mimetypes.encodings_map' until py 3.9. encodings_map[".br"] = "br" async def async_file_iterator( file_path: PathType, chunk_size: int, adapter: FileSystemAdapter ) -> AsyncGenerator[bytes, None]: """Return an async that asynchronously reads a file and yields its chunks. Args: file_path: A path to a file. chunk_size: The chunk file to use. adapter: File system adapter class. adapter: File system adapter class. Returns: An async generator. """ async with await adapter.open(file_path) as file: while chunk := await file.read(chunk_size): yield chunk def create_etag_for_file(path: PathType, modified_time: float | None, file_size: int) -> str: """Create an etag. Notes: - Function is derived from flask. Returns: An etag. """ check = adler32(str(path).encode("utf-8")) & 0xFFFFFFFF parts = [str(file_size), str(check)] if modified_time: parts.insert(0, str(modified_time)) return f'"{"-".join(parts)}"' _MTIME_KEYS: Final = ( "mtime", "ctime", "Last-Modified", "updated_at", "modification_time", "last_changed", "change_time", "last_modified", "last_updated", "timestamp", ) def get_fsspec_mtime_equivalent(info: dict[str, Any]) -> float | None: """Return the 'mtime' or equivalent for different fsspec implementations, since they are not standardized. See https://github.com/fsspec/filesystem_spec/issues/526. """ # inspired by https://github.com/mdshw5/pyfaidx/blob/cac82f24e9c4e334cf87a92e477b92d4615d260f/pyfaidx/__init__.py#L1318-L1345 mtime: Any | None = next((info[key] for key in _MTIME_KEYS if key in info), None) if mtime is None or isinstance(mtime, float): return mtime if isinstance(mtime, datetime): return mtime.timestamp() if isinstance(mtime, str): return datetime.fromisoformat(mtime.replace("Z", "+00:00")).timestamp() raise ValueError(f"Unsupported mtime-type value type {type(mtime)!r}") class ASGIFileResponse(ASGIStreamingResponse): """A low-level ASGI response, streaming a file as response body.""" def __init__( self, *, background: BackgroundTask | BackgroundTasks | None = None, body: bytes | str = b"", chunk_size: int = ONE_MEGABYTE, content_disposition_type: Literal["attachment", "inline"] = "attachment", content_length: int | None = None, cookies: Iterable[Cookie] | None = None, encoded_headers: Iterable[tuple[bytes, bytes]] | None = None, encoding: str = "utf-8", etag: ETag | None = None, file_info: FileInfo | Coroutine[None, None, FileInfo] | None = None, file_path: str | PathLike | Path, file_system: FileSystemProtocol | None = None, filename: str = "", headers: dict[str, str] | None = None, is_head_response: bool = False, media_type: MediaType | str | None = None, stat_result: stat_result_type | None = None, status_code: int | None = None, ) -> None: """A low-level ASGI response, streaming a file as response body. Args: background: A background task or a list of background tasks to be executed after the response is sent. body: encoded content to send in the response body. chunk_size: The chunk size to use. content_disposition_type: The type of the ``Content-Disposition``. Either ``inline`` or ``attachment``. content_length: The response content length. cookies: The response cookies. encoded_headers: A list of encoded headers. encoding: The response encoding. etag: An etag. file_info: A file info. file_path: A path to a file. file_system: A file system adapter. filename: The name of the file. headers: A dictionary of headers. headers: The response headers. is_head_response: A boolean indicating if the response is a HEAD response. media_type: The media type of the file. stat_result: A stat result. status_code: The response status code. """ headers = headers or {} if not media_type: mimetype, content_encoding = guess_type(filename) if filename else (None, None) media_type = mimetype or "application/octet-stream" if content_encoding is not None: headers.update({"content-encoding": content_encoding}) self.adapter = FileSystemAdapter(file_system or BaseLocalFileSystem()) super().__init__( iterator=async_file_iterator(file_path=file_path, chunk_size=chunk_size, adapter=self.adapter), headers=headers, media_type=media_type, cookies=cookies, background=background, status_code=status_code, body=body, content_length=content_length, encoding=encoding, is_head_response=is_head_response, encoded_headers=encoded_headers, ) quoted_filename = quote(filename) is_utf8 = quoted_filename == filename if is_utf8: content_disposition = f'{content_disposition_type}; filename="{filename}"' else: content_disposition = f"{content_disposition_type}; filename*=utf-8''{quoted_filename}" self.headers.setdefault("content-disposition", content_disposition) self.chunk_size = chunk_size self.etag = etag self.file_path = file_path if file_info: self.file_info: FileInfo | Coroutine[Any, Any, FileInfo] = file_info elif stat_result: self.file_info = self.adapter.parse_stat_result(result=stat_result, path=file_path) else: self.file_info = self.adapter.info(self.file_path) async def send_body(self, send: Send, receive: Receive) -> None: """Emit a stream of events correlating with the response body. Args: send: The ASGI send function. receive: The ASGI receive function. Returns: None """ if self.chunk_size < self.content_length: await super().send_body(send=send, receive=receive) return async with await self.adapter.open(self.file_path) as file: body_event: HTTPResponseBodyEvent = { "type": "http.response.body", "body": await file.read(), "more_body": False, } await send(body_event) async def start_response(self, send: Send) -> None: """Emit the start event of the response. This event includes the headers and status codes. Args: send: The ASGI send function. Returns: None """ try: fs_info = self.file_info = cast( "FileInfo", (await self.file_info if iscoroutine(self.file_info) else self.file_info) ) except FileNotFoundError as e: raise ImproperlyConfiguredException(f"{self.file_path} does not exist") from e if fs_info["type"] != "file": raise ImproperlyConfiguredException(f"{self.file_path} is not a file") self.content_length = fs_info["size"] self.headers.setdefault("content-length", str(self.content_length)) mtime = get_fsspec_mtime_equivalent(fs_info) # type: ignore[arg-type] if mtime is not None: self.headers.setdefault("last-modified", formatdate(mtime, usegmt=True)) if self.etag: self.headers.setdefault("etag", self.etag.to_header()) else: self.headers.setdefault( "etag", create_etag_for_file(path=self.file_path, modified_time=mtime, file_size=fs_info["size"]), ) await super().start_response(send=send) class File(Response): """A response, streaming a file as response body.""" __slots__ = ( "chunk_size", "content_disposition_type", "etag", "file_info", "file_path", "file_system", "filename", "stat_result", ) def __init__( self, path: str | PathLike | Path, *, background: BackgroundTask | BackgroundTasks | None = None, chunk_size: int = ONE_MEGABYTE, content_disposition_type: Literal["attachment", "inline"] = "attachment", cookies: ResponseCookies | None = None, encoding: str = "utf-8", etag: ETag | None = None, file_info: FileInfo | Coroutine[Any, Any, FileInfo] | None = None, file_system: FileSystemProtocol | None = None, filename: str | None = None, headers: ResponseHeaders | None = None, media_type: Literal[MediaType.TEXT] | str | None = None, stat_result: stat_result_type | None = None, status_code: int | None = None, ) -> None: """Initialize ``File`` Notes: - This class extends the :class:`Stream <.response.Stream>` class. Args: path: A file path in one of the supported formats. background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. Defaults to None. chunk_size: The chunk sizes to use when streaming the file. Defaults to 1MB. content_disposition_type: The type of the ``Content-Disposition``. Either ``inline`` or ``attachment``. cookies: A list of :class:`Cookie <.datastructures.Cookie>` instances to be set under the response ``Set-Cookie`` header. encoding: The encoding to be used for the response headers. etag: An optional :class:`ETag <.datastructures.ETag>` instance. If not provided, an etag will be generated. file_info: The output of calling :meth:`file_system.info `, equivalent to providing an :class:`os.stat_result`. file_system: An implementation of the :class:`FileSystemProtocol <.types.FileSystemProtocol>`. If provided it will be used to load the file. filename: An optional filename to set in the header. headers: A string keyed dictionary of response headers. Header keys are insensitive. media_type: A value for the response ``Content-Type`` header. If not provided, the value will be either derived from the filename if provided and supported by the stdlib, or will default to ``application/octet-stream``. stat_result: An optional result of calling :func:os.stat:. If not provided, this will be done by the response constructor. status_code: An HTTP status code. """ if file_system is not None and not ( callable(getattr(file_system, "info", None)) and callable(getattr(file_system, "open", None)) ): raise ImproperlyConfiguredException("file_system must adhere to the FileSystemProtocol type") self.chunk_size = chunk_size self.content_disposition_type = content_disposition_type self.etag = etag self.file_info = file_info self.file_path = path self.file_system = file_system self.filename = filename or "" self.stat_result = stat_result super().__init__( content=None, status_code=status_code, media_type=media_type, background=background, headers=headers, cookies=cookies, encoding=encoding, ) def to_asgi_response( self, app: Litestar | None, request: Request, *, background: BackgroundTask | BackgroundTasks | None = None, encoded_headers: Iterable[tuple[bytes, bytes]] | None = None, cookies: Iterable[Cookie] | None = None, headers: dict[str, str] | None = None, is_head_response: bool = False, media_type: MediaType | str | None = None, status_code: int | None = None, type_encoders: TypeEncodersMap | None = None, ) -> ASGIFileResponse: """Create an :class:`ASGIFileResponse ` instance. Args: app: The :class:`Litestar <.app.Litestar>` application instance. background: Background task(s) to be executed after the response is sent. cookies: A list of cookies to be set on the response. encoded_headers: A list of already encoded headers. headers: Additional headers to be merged with the response headers. Response headers take precedence. is_head_response: Whether the response is a HEAD response. media_type: Media type for the response. If ``media_type`` is already set on the response, this is ignored. request: The :class:`Request <.connection.Request>` instance. status_code: Status code for the response. If ``status_code`` is already set on the response, this is type_encoders: A dictionary of type encoders to use for encoding the response content. Returns: A low-level ASGI file response. """ if app is not None: warn_deprecation( version="2.1", deprecated_name="app", kind="parameter", removal_in="3.0.0", alternative="request.app", ) headers = {**headers, **self.headers} if headers is not None else self.headers cookies = self.cookies if cookies is None else itertools.chain(self.cookies, cookies) media_type = self.media_type or media_type if media_type is not None: media_type = get_enum_string_value(media_type) return ASGIFileResponse( background=self.background or background, body=b"", chunk_size=self.chunk_size, content_disposition_type=self.content_disposition_type, # pyright: ignore content_length=0, cookies=cookies, encoded_headers=encoded_headers, encoding=self.encoding, etag=self.etag, file_info=self.file_info, file_path=self.file_path, file_system=self.file_system, filename=self.filename, headers=headers, is_head_response=is_head_response, media_type=media_type, stat_result=self.stat_result, status_code=self.status_code or status_code, ) litestar-2.16.0/litestar/response/redirect.py000066400000000000000000000157541500564371300213070ustar00rootroot00000000000000from __future__ import annotations import itertools from typing import TYPE_CHECKING, Any, Iterable, Literal, Sequence from urllib.parse import urlencode from litestar.constants import REDIRECT_ALLOWED_MEDIA_TYPES, REDIRECT_STATUS_CODES from litestar.datastructures import MultiDict from litestar.enums import MediaType from litestar.exceptions import ImproperlyConfiguredException from litestar.response.base import ASGIResponse, Response from litestar.status_codes import HTTP_302_FOUND from litestar.utils import url_quote from litestar.utils.deprecation import warn_deprecation from litestar.utils.helpers import get_enum_string_value if TYPE_CHECKING: from collections.abc import Mapping from litestar.app import Litestar from litestar.background_tasks import BackgroundTask, BackgroundTasks from litestar.connection import Request from litestar.datastructures import Cookie from litestar.types import ResponseCookies, ResponseHeaders, TypeEncodersMap __all__ = ( "ASGIRedirectResponse", "Redirect", ) RedirectStatusType = Literal[301, 302, 303, 307, 308] """Acceptable status codes for redirect responses.""" class ASGIRedirectResponse(ASGIResponse): """A low-level ASGI redirect response class.""" def __init__( self, path: str | bytes, media_type: str | None = None, status_code: RedirectStatusType | None = None, headers: dict[str, Any] | None = None, encoded_headers: Iterable[tuple[bytes, bytes]] | None = None, background: BackgroundTask | BackgroundTasks | None = None, body: bytes | str = b"", content_length: int | None = None, cookies: Iterable[Cookie] | None = None, encoding: str = "utf-8", is_head_response: bool = False, ) -> None: headers = {**(headers or {}), "location": url_quote(path)} media_type = media_type or MediaType.TEXT status_code = status_code or HTTP_302_FOUND if status_code not in REDIRECT_STATUS_CODES: raise ImproperlyConfiguredException( f"{status_code} is not a valid for this response. " f"Redirect responses should have one of " f"the following status codes: {', '.join([str(s) for s in REDIRECT_STATUS_CODES])}" ) if media_type not in REDIRECT_ALLOWED_MEDIA_TYPES: raise ImproperlyConfiguredException( f"{media_type} media type is not supported yet. " f"Media type should be one of " f"the following values: {', '.join([str(s) for s in REDIRECT_ALLOWED_MEDIA_TYPES])}" ) super().__init__( status_code=status_code, headers=headers, media_type=media_type, background=background, is_head_response=is_head_response, encoding=encoding, cookies=cookies, content_length=content_length, body=body, encoded_headers=encoded_headers, ) class Redirect(Response[Any]): """A redirect response.""" __slots__ = ("url",) def __init__( self, path: str, *, background: BackgroundTask | BackgroundTasks | None = None, cookies: ResponseCookies | None = None, encoding: str = "utf-8", headers: ResponseHeaders | None = None, media_type: str | MediaType | None = None, status_code: RedirectStatusType | None = None, type_encoders: TypeEncodersMap | None = None, query_params: Mapping[str, str | Sequence[str]] | MultiDict | None = None, ) -> None: """Initialize the response. Args: path: A path to redirect to. background: A background task or tasks to be run after the response is sent. cookies: A list of :class:`Cookie <.datastructures.Cookie>` instances to be set under the response ``Set-Cookie`` header. encoding: The encoding to be used for the response headers. headers: A string keyed dictionary of response headers. Header keys are insensitive. media_type: A value for the response ``Content-Type`` header. status_code: An HTTP status code. The status code should be one of 301, 302, 303, 307 or 308, otherwise an exception will be raised. type_encoders: A mapping of types to callables that transform them into types supported for serialization. query_params: A dictionary of values from which the request's query will be generated. Raises: ImproperlyConfiguredException: Either if status code is not a redirect status code or media type is not supported. """ if query_params is None: self.url = path elif isinstance(query_params, MultiDict): # We can't use MultiDictMixin.dict() because it's not deterministic query_params_dict = {k: query_params.getall(k) for k in query_params} self.url = f"{path}?{urlencode(query_params_dict, doseq=True)}" else: self.url = f"{path}?{urlencode(query_params, doseq=True)}" if status_code is None: status_code = HTTP_302_FOUND super().__init__( background=background, content=b"", cookies=cookies, encoding=encoding, headers=headers, media_type=media_type, status_code=status_code, type_encoders=type_encoders, ) def to_asgi_response( self, app: Litestar | None, request: Request, *, background: BackgroundTask | BackgroundTasks | None = None, cookies: Iterable[Cookie] | None = None, encoded_headers: Iterable[tuple[bytes, bytes]] | None = None, headers: dict[str, str] | None = None, is_head_response: bool = False, media_type: MediaType | str | None = None, status_code: int | None = None, type_encoders: TypeEncodersMap | None = None, ) -> ASGIResponse: headers = {**headers, **self.headers} if headers is not None else self.headers cookies = self.cookies if cookies is None else itertools.chain(self.cookies, cookies) media_type = get_enum_string_value(self.media_type or media_type or MediaType.TEXT) if app is not None: warn_deprecation( version="2.1", deprecated_name="app", kind="parameter", removal_in="3.0.0", alternative="request.app", ) return ASGIRedirectResponse( path=self.url, background=self.background or background, body=b"", content_length=None, cookies=cookies, encoded_headers=encoded_headers, encoding=self.encoding, headers=headers, is_head_response=is_head_response, media_type=media_type, status_code=self.status_code or status_code, # type:ignore[arg-type] ) litestar-2.16.0/litestar/response/sse.py000066400000000000000000000157371500564371300203010ustar00rootroot00000000000000from __future__ import annotations import io import re from dataclasses import dataclass from typing import TYPE_CHECKING, Any, AsyncGenerator, AsyncIterable, AsyncIterator, Iterable, Iterator from litestar.concurrency import sync_to_thread from litestar.exceptions import ImproperlyConfiguredException from litestar.response.streaming import Stream from litestar.utils import AsyncIteratorWrapper if TYPE_CHECKING: from litestar.background_tasks import BackgroundTask, BackgroundTasks from litestar.types import ResponseCookies, ResponseHeaders, SSEData, StreamType _LINE_BREAK_RE = re.compile(r"\r\n|\r|\n") DEFAULT_SEPARATOR = "\r\n" class _ServerSentEventIterator(AsyncIteratorWrapper[bytes]): __slots__ = ("comment_message", "content_async_iterator", "event_id", "event_type", "retry_duration") content_async_iterator: AsyncIterable[SSEData] def __init__( self, content: str | bytes | StreamType[SSEData], event_type: str | None = None, event_id: int | str | None = None, retry_duration: int | None = None, comment_message: str | None = None, ) -> None: self.comment_message = comment_message self.event_id = event_id self.event_type = event_type self.retry_duration = retry_duration chunks: list[bytes] = [] if comment_message is not None: chunks.extend([f": {chunk}\r\n".encode() for chunk in _LINE_BREAK_RE.split(comment_message)]) if event_id is not None: chunks.append(f"id: {event_id}\r\n".encode()) if event_type is not None: chunks.append(f"event: {event_type}\r\n".encode()) if retry_duration is not None: chunks.append(f"retry: {retry_duration}\r\n".encode()) super().__init__(iterator=chunks) if not isinstance(content, (Iterator, AsyncIterator, AsyncIteratorWrapper)) and callable(content): content = content() # type: ignore[unreachable] if isinstance(content, (str, bytes)): self.content_async_iterator = AsyncIteratorWrapper([content]) elif isinstance(content, (Iterable, Iterator)): self.content_async_iterator = AsyncIteratorWrapper(content) elif isinstance(content, (AsyncIterable, AsyncIterator, AsyncIteratorWrapper)): self.content_async_iterator = content else: raise ImproperlyConfiguredException(f"Invalid type {type(content)} for ServerSentEvent") def ensure_bytes(self, data: str | int | bytes | dict | ServerSentEventMessage | Any, sep: str) -> bytes: if isinstance(data, ServerSentEventMessage): return data.encode() if isinstance(data, dict): data["sep"] = sep return ServerSentEventMessage(**data).encode() return ServerSentEventMessage( data=data, id=self.event_id, event=self.event_type, retry=self.retry_duration, sep=sep ).encode() def _call_next(self) -> bytes: try: return next(self.iterator) except StopIteration as e: raise ValueError from e async def _async_generator(self) -> AsyncGenerator[bytes, None]: while True: try: yield await sync_to_thread(self._call_next) except ValueError: async for value in self.content_async_iterator: yield self.ensure_bytes(value, DEFAULT_SEPARATOR) break @dataclass class ServerSentEventMessage: data: str | int | bytes | None = "" event: str | None = None id: int | str | None = None retry: int | None = None comment: str | None = None sep: str = DEFAULT_SEPARATOR def encode(self) -> bytes: buffer = io.StringIO() if self.comment is not None: for chunk in _LINE_BREAK_RE.split(str(self.comment)): buffer.write(f": {chunk}") buffer.write(self.sep) if self.id is not None: buffer.write(_LINE_BREAK_RE.sub("", f"id: {self.id}")) buffer.write(self.sep) if self.event is not None: buffer.write(_LINE_BREAK_RE.sub("", f"event: {self.event}")) buffer.write(self.sep) if self.data is not None: data = self.data for chunk in _LINE_BREAK_RE.split(data.decode() if isinstance(data, bytes) else str(data)): buffer.write(f"data: {chunk}") buffer.write(self.sep) if self.retry is not None: buffer.write(f"retry: {self.retry}") buffer.write(self.sep) buffer.write(self.sep) return buffer.getvalue().encode("utf-8") class ServerSentEvent(Stream): def __init__( self, content: str | bytes | StreamType[SSEData], *, background: BackgroundTask | BackgroundTasks | None = None, cookies: ResponseCookies | None = None, encoding: str = "utf-8", headers: ResponseHeaders | None = None, event_type: str | None = None, event_id: int | str | None = None, retry_duration: int | None = None, comment_message: str | None = None, status_code: int | None = None, ) -> None: """Initialize the response. Args: content: Bytes, string or a sync or async iterator or iterable. background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. Defaults to None. cookies: A list of :class:`Cookie <.datastructures.Cookie>` instances to be set under the response ``Set-Cookie`` header. encoding: The encoding to be used for the response headers. headers: A string keyed dictionary of response headers. Header keys are insensitive. status_code: The response status code. Defaults to 200. event_type: The type of the SSE event. If given, the browser will sent the event to any 'event-listener' declared for it (e.g. via 'addEventListener' in JS). event_id: The event ID. This sets the event source's 'last event id'. retry_duration: Retry duration in milliseconds. comment_message: A comment message. This value is ignored by clients and is used mostly for pinging. """ super().__init__( content=_ServerSentEventIterator( content=content, event_type=event_type, event_id=event_id, retry_duration=retry_duration, comment_message=comment_message, ), media_type="text/event-stream", background=background, cookies=cookies, encoding=encoding, headers=headers, status_code=status_code, ) self.headers.setdefault("Cache-Control", "no-cache") self.headers["Connection"] = "keep-alive" self.headers["X-Accel-Buffering"] = "no" litestar-2.16.0/litestar/response/streaming.py000066400000000000000000000233761500564371300214760ustar00rootroot00000000000000from __future__ import annotations import itertools from functools import partial from typing import TYPE_CHECKING, Any, AsyncGenerator, AsyncIterable, AsyncIterator, Callable, Iterable, Iterator, Union from anyio import CancelScope, create_task_group from litestar.enums import MediaType from litestar.response.base import ASGIResponse, Response from litestar.types.helper_types import StreamType from litestar.utils.deprecation import warn_deprecation from litestar.utils.helpers import get_enum_string_value from litestar.utils.sync import AsyncIteratorWrapper if TYPE_CHECKING: from litestar.app import Litestar from litestar.background_tasks import BackgroundTask, BackgroundTasks from litestar.connection import Request from litestar.datastructures.cookie import Cookie from litestar.enums import OpenAPIMediaType from litestar.types import HTTPResponseBodyEvent, Receive, ResponseCookies, ResponseHeaders, Send, TypeEncodersMap __all__ = ( "ASGIStreamingResponse", "Stream", ) class ASGIStreamingResponse(ASGIResponse): """A streaming response.""" __slots__ = ("iterator",) _should_set_content_length = False def __init__( self, *, iterator: StreamType, background: BackgroundTask | BackgroundTasks | None = None, body: bytes | str = b"", content_length: int | None = None, cookies: Iterable[Cookie] | None = None, encoded_headers: Iterable[tuple[bytes, bytes]] | None = None, encoding: str = "utf-8", headers: dict[str, Any] | None = None, is_head_response: bool = False, media_type: MediaType | str | None = None, status_code: int | None = None, ) -> None: """A low-level ASGI streaming response. Args: background: A background task or a list of background tasks to be executed after the response is sent. body: encoded content to send in the response body. .. deprecated:: 2.16 content_length: The response content length. cookies: The response cookies. encoded_headers: The response headers. encoding: The response encoding. headers: The response headers. is_head_response: A boolean indicating if the response is a HEAD response. iterator: An async iterator or iterable. media_type: The response media type. status_code: The response status code. """ if body: warn_deprecation( version="2.16", kind="parameter", deprecated_name="body", removal_in="3.0", info="'body' passed to a streaming response will be ignored. Streams should always be iterables", ) super().__init__( background=background, body=body, content_length=content_length, cookies=cookies, encoding=encoding, headers=headers, is_head_response=is_head_response, media_type=media_type, status_code=status_code, encoded_headers=encoded_headers, ) self.iterator: AsyncIterable[str | bytes] | AsyncGenerator[str | bytes, None] = ( iterator if isinstance(iterator, (AsyncIterable, AsyncIterator)) else AsyncIteratorWrapper(iterator) ) async def _listen_for_disconnect(self, cancel_scope: CancelScope, receive: Receive) -> None: """Listen for a cancellation message, and if received - call cancel on the cancel scope. Args: cancel_scope: A task group cancel scope instance. receive: The ASGI receive function. Returns: None """ if not cancel_scope.cancel_called: message = await receive() if message["type"] == "http.disconnect": # despite the IDE warning, this is not a coroutine because anyio 3+ changed this. # therefore make sure not to await this. cancel_scope.cancel() else: await self._listen_for_disconnect(cancel_scope=cancel_scope, receive=receive) async def _stream(self, send: Send) -> None: """Send the chunks from the iterator as a stream of ASGI 'http.response.body' events. Args: send: The ASGI Send function. Returns: None """ async for chunk in self.iterator: stream_event: HTTPResponseBodyEvent = { "type": "http.response.body", "body": chunk if isinstance(chunk, bytes) else chunk.encode(self.encoding), "more_body": True, } await send(stream_event) terminus_event: HTTPResponseBodyEvent = {"type": "http.response.body", "body": b"", "more_body": False} await send(terminus_event) async def send_body(self, send: Send, receive: Receive) -> None: """Emit a stream of events correlating with the response body. Args: send: The ASGI send function. receive: The ASGI receive function. Returns: None """ async with create_task_group() as task_group: task_group.start_soon(partial(self._stream, send)) await self._listen_for_disconnect(cancel_scope=task_group.cancel_scope, receive=receive) class Stream(Response[StreamType[Union[str, bytes]]]): """An HTTP response that streams the response data as a series of ASGI ``http.response.body`` events.""" __slots__ = ("iterator",) def __init__( self, content: StreamType[str | bytes] | Callable[[], StreamType[str | bytes]], *, background: BackgroundTask | BackgroundTasks | None = None, cookies: ResponseCookies | None = None, encoding: str = "utf-8", headers: ResponseHeaders | None = None, media_type: MediaType | OpenAPIMediaType | str | None = None, status_code: int | None = None, ) -> None: """Initialize the response. Args: content: A sync or async iterator or iterable. background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. Defaults to None. cookies: A list of :class:`Cookie <.datastructures.Cookie>` instances to be set under the response ``Set-Cookie`` header. encoding: The encoding to be used for the response headers. headers: A string keyed dictionary of response headers. Header keys are insensitive. media_type: A value for the response ``Content-Type`` header. status_code: An HTTP status code. """ super().__init__( background=background, content=b"", # type: ignore[arg-type] cookies=cookies, encoding=encoding, headers=headers, media_type=media_type, status_code=status_code, ) self.iterator = content def to_asgi_response( self, app: Litestar | None, request: Request, *, background: BackgroundTask | BackgroundTasks | None = None, cookies: Iterable[Cookie] | None = None, encoded_headers: Iterable[tuple[bytes, bytes]] | None = None, headers: dict[str, str] | None = None, is_head_response: bool = False, media_type: MediaType | str | None = None, status_code: int | None = None, type_encoders: TypeEncodersMap | None = None, ) -> ASGIResponse: """Create an ASGIStreamingResponse from a StremaingResponse instance. Args: app: The :class:`Litestar <.app.Litestar>` application instance. background: Background task(s) to be executed after the response is sent. cookies: A list of cookies to be set on the response. encoded_headers: A list of already encoded headers. headers: Additional headers to be merged with the response headers. Response headers take precedence. is_head_response: Whether the response is a HEAD response. media_type: Media type for the response. If ``media_type`` is already set on the response, this is ignored. request: The :class:`Request <.connection.Request>` instance. status_code: Status code for the response. If ``status_code`` is already set on the response, this is type_encoders: A dictionary of type encoders to use for encoding the response content. Returns: An ASGIStreamingResponse instance. """ if app is not None: warn_deprecation( version="2.1", deprecated_name="app", kind="parameter", removal_in="3.0.0", alternative="request.app", ) headers = {**headers, **self.headers} if headers is not None else self.headers cookies = self.cookies if cookies is None else itertools.chain(self.cookies, cookies) media_type = get_enum_string_value(media_type or self.media_type or MediaType.JSON) iterator = self.iterator if not isinstance(iterator, (Iterable, Iterator, AsyncIterable, AsyncIterator)) and callable(iterator): iterator = iterator() return ASGIStreamingResponse( background=self.background or background, content_length=0, cookies=cookies, encoded_headers=encoded_headers, encoding=self.encoding, headers=headers, is_head_response=is_head_response, iterator=iterator, media_type=media_type, status_code=self.status_code or status_code, ) litestar-2.16.0/litestar/response/template.py000066400000000000000000000145401500564371300213110ustar00rootroot00000000000000from __future__ import annotations import itertools from mimetypes import guess_type from pathlib import PurePath from typing import TYPE_CHECKING, Any, Iterable, cast from litestar.enums import MediaType from litestar.exceptions import ImproperlyConfiguredException from litestar.response.base import ASGIResponse, Response from litestar.status_codes import HTTP_200_OK from litestar.utils.deprecation import warn_deprecation from litestar.utils.empty import value_or_default from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from litestar.app import Litestar from litestar.background_tasks import BackgroundTask, BackgroundTasks from litestar.connection import Request from litestar.datastructures import Cookie from litestar.types import ResponseCookies, TypeEncodersMap __all__ = ("Template",) class Template(Response[bytes]): """Template-based response, rendering a given template into a bytes string.""" __slots__ = ( "context", "template_name", "template_str", ) def __init__( self, template_name: str | None = None, *, template_str: str | None = None, background: BackgroundTask | BackgroundTasks | None = None, context: dict[str, Any] | None = None, cookies: ResponseCookies | None = None, encoding: str = "utf-8", headers: dict[str, Any] | None = None, media_type: MediaType | str | None = None, status_code: int = HTTP_200_OK, ) -> None: """Handle the rendering of a given template into a bytes string. Args: template_name: Path-like name for the template to be rendered, e.g. ``index.html``. template_str: A string representing the template, e.g. ``tmpl = "Hello World"``. background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished. Defaults to ``None``. context: A dictionary of key/value pairs to be passed to the temple engine's render method. cookies: A list of :class:`Cookie <.datastructures.Cookie>` instances to be set under the response ``Set-Cookie`` header. encoding: Content encoding headers: A string keyed dictionary of response headers. Header keys are insensitive. media_type: A string or member of the :class:`MediaType <.enums.MediaType>` enum. If not set, try to infer the media type based on the template name. If this fails, fall back to ``text/plain``. status_code: A value for the response HTTP status code. """ if not (template_name or template_str): raise ValueError("Either template_name or template_str must be provided.") if template_name and template_str: raise ValueError("Either template_name or template_str must be provided, not both.") super().__init__( background=background, content=b"", cookies=cookies, encoding=encoding, headers=headers, media_type=media_type, status_code=status_code, ) self.context = context or {} self.template_name = template_name self.template_str = template_str def create_template_context(self, request: Request) -> dict[str, Any]: """Create a context object for the template. Args: request: A :class:`Request <.connection.Request>` instance. Returns: A dictionary holding the template context """ csrf_token = value_or_default(ScopeState.from_scope(request.scope).csrf_token, "") return { **self.context, "request": request, "csrf_input": f'', } def to_asgi_response( self, app: Litestar | None, request: Request, *, background: BackgroundTask | BackgroundTasks | None = None, cookies: Iterable[Cookie] | None = None, encoded_headers: Iterable[tuple[bytes, bytes]] | None = None, headers: dict[str, str] | None = None, is_head_response: bool = False, media_type: MediaType | str | None = None, status_code: int | None = None, type_encoders: TypeEncodersMap | None = None, ) -> ASGIResponse: if app is not None: warn_deprecation( version="2.1", deprecated_name="app", kind="parameter", removal_in="3.0.0", alternative="request.app", ) if not (template_engine := request.app.template_engine): raise ImproperlyConfiguredException("Template engine is not configured") headers = {**headers, **self.headers} if headers is not None else self.headers cookies = self.cookies if cookies is None else itertools.chain(self.cookies, cookies) media_type = self.media_type or media_type if not media_type: if self.template_name: suffixes = PurePath(self.template_name).suffixes for suffix in suffixes: if _type := guess_type(f"name{suffix}")[0]: media_type = _type break else: media_type = MediaType.TEXT else: media_type = MediaType.HTML context = self.create_template_context(request) if self.template_str is not None: body = template_engine.render_string(self.template_str, context) else: # cast to str b/c we know that either template_name cannot be None if template_str is None template = template_engine.get_template(cast("str", self.template_name)) body = template.render(**context).encode(self.encoding) return ASGIResponse( background=self.background or background, body=body, content_length=None, cookies=cookies, encoded_headers=encoded_headers, encoding=self.encoding, headers=headers, is_head_response=is_head_response, media_type=media_type, status_code=self.status_code or status_code, ) litestar-2.16.0/litestar/router.py000066400000000000000000000374261500564371300171700ustar00rootroot00000000000000from __future__ import annotations from collections import defaultdict from copy import copy, deepcopy from typing import TYPE_CHECKING, Any, Mapping, Sequence, cast from litestar._layers.utils import narrow_response_cookies, narrow_response_headers from litestar.controller import Controller from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers.asgi_handlers import ASGIRouteHandler from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.handlers.websocket_handlers import WebsocketListener, WebsocketRouteHandler from litestar.routes import ASGIRoute, HTTPRoute, WebSocketRoute from litestar.types.empty import Empty from litestar.utils import find_index, is_class_and_subclass, join_paths, normalize_path, unique from litestar.utils.signature import add_types_to_signature_namespace from litestar.utils.sync import ensure_async_callable __all__ = ("Router",) if TYPE_CHECKING: from litestar.connection import Request, WebSocket from litestar.datastructures import CacheControlHeader, ETag from litestar.dto import AbstractDTO from litestar.openapi.spec import SecurityRequirement from litestar.response import Response from litestar.routes import BaseRoute from litestar.types import ( AfterRequestHookHandler, AfterResponseHookHandler, BeforeRequestHookHandler, ControllerRouterHandler, ExceptionHandlersMap, Guard, Middleware, ParametersMap, ResponseCookies, RouteHandlerMapItem, RouteHandlerType, TypeEncodersMap, ) from litestar.types.composite_types import Dependencies, ResponseHeaders, TypeDecodersSequence from litestar.types.empty import EmptyType class Router: """The Litestar Router class. A Router instance is used to group controller, routers and route handler functions under a shared path fragment """ __slots__ = ( "after_request", "after_response", "before_request", "cache_control", "dependencies", "dto", "etag", "exception_handlers", "guards", "include_in_schema", "middleware", "opt", "owner", "parameters", "path", "registered_route_handler_ids", "request_class", "request_max_body_size", "response_class", "response_cookies", "response_headers", "return_dto", "routes", "security", "signature_namespace", "tags", "type_decoders", "type_encoders", "websocket_class", ) def __init__( self, path: str, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, before_request: BeforeRequestHookHandler | None = None, cache_control: CacheControlHeader | None = None, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, guards: Sequence[Guard] | None = None, include_in_schema: bool | EmptyType = Empty, middleware: Sequence[Middleware] | None = None, opt: Mapping[str, Any] | None = None, parameters: ParametersMap | None = None, request_class: type[Request] | None = None, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, route_handlers: Sequence[ControllerRouterHandler], security: Sequence[SecurityRequirement] | None = None, signature_namespace: Mapping[str, Any] | None = None, signature_types: Sequence[Any] | None = None, tags: Sequence[str] | None = None, type_decoders: TypeDecodersSequence | None = None, type_encoders: TypeEncodersMap | None = None, websocket_class: type[WebSocket] | None = None, request_max_body_size: int | None | EmptyType = Empty, ) -> None: """Initialize a ``Router``. Args: after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed to any route handler. If this function returns a value, the request will not reach the route handler, and instead this value will be used. after_response: A sync or async function called after the response has been awaited. It receives the :class:`Request <.connection.Request>` object and should not return any values. before_request: A sync or async function called immediately before calling the route handler. Receives the :class:`litestar.connection.Request` instance and any non-``None`` return value is used for the response, bypassing the route handler. cache_control: A ``cache-control`` header of type :class:`CacheControlHeader <.datastructures.CacheControlHeader>` to add to route handlers of this router. Can be overridden by route handlers. dependencies: A string keyed mapping of dependency :class:`Provide <.di.Provide>` instances. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` to add to route handlers of this app. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :data:`Guard <.types.Guard>` callables. include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. middleware: A sequence of :data:`Middleware <.types.Middleware>`. opt: A string keyed mapping of arbitrary values that can be accessed in :data:`Guards <.types.Guard>` or wherever you have access to :class:`Request <.connection.Request>` or :data:`ASGI Scope <.types.Scope>`. parameters: A mapping of :func:`Parameter <.params.Parameter>` definitions available to all application paths. path: A path fragment that is prefixed to all route handlers, controllers and other routers associated with the router instance. request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as the default for all route handlers, controllers and other routers associated with the router instance. request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded, a '413 - Request Entity Too Large" error response is returned. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as the default for all route handlers, controllers and other routers associated with the router instance. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances. response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` instances. return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. route_handlers: A required sequence of route handlers, which can include instances of :class:`Router <.router.Router>`, subclasses of :class:`Controller <.controller.Controller>` or any function decorated by the route handler decorators. security: A sequence of dicts that will be added to the schema of all route handlers in the application. See :data:`SecurityRequirement <.openapi.spec.SecurityRequirement>` for details. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. signature_types: A sequence of types for use in forward reference resolution during signature modelling. These types will be added to the signature namespace using their ``__name__`` attribute. tags: A sequence of string tags that will be appended to the schema of all route handlers under the application. type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec hook for deserialization. type_encoders: A mapping of types to callables that transform them into types supported for serialization. websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as the default for all route handlers, controllers and other routers associated with the router instance. """ self.after_request = ensure_async_callable(after_request) if after_request else None # pyright: ignore self.after_response = ensure_async_callable(after_response) if after_response else None self.before_request = ensure_async_callable(before_request) if before_request else None self.cache_control = cache_control self.dto = dto self.etag = etag self.dependencies = dict(dependencies or {}) self.exception_handlers = dict(exception_handlers or {}) self.guards = list(guards or []) self.include_in_schema = include_in_schema self.middleware = list(middleware or []) self.opt = dict(opt or {}) self.owner: Router | None = None self.parameters = dict(parameters or {}) self.path = normalize_path(path) self.request_class = request_class self.response_class = response_class self.response_cookies = narrow_response_cookies(response_cookies) self.response_headers = narrow_response_headers(response_headers) self.return_dto = return_dto self.routes: list[HTTPRoute | ASGIRoute | WebSocketRoute] = [] self.security = list(security or []) self.signature_namespace = add_types_to_signature_namespace( signature_types or [], dict(signature_namespace or {}) ) self.tags = list(tags or []) self.registered_route_handler_ids: set[int] = set() self.type_encoders = dict(type_encoders) if type_encoders is not None else None self.type_decoders = list(type_decoders) if type_decoders is not None else None self.websocket_class = websocket_class self.request_max_body_size = request_max_body_size for route_handler in route_handlers or []: self.register(value=route_handler) def register(self, value: ControllerRouterHandler) -> list[BaseRoute]: """Register a Controller, Route instance or RouteHandler on the router. Args: value: a subclass or instance of Controller, an instance of :class:`Router` or a function/method that has been decorated by any of the routing decorators, e.g. :class:`get <.handlers.get>`, :class:`post <.handlers.post>`. Returns: Collection of handlers added to the router. """ validated_value = self._validate_registration_value(value) routes: list[BaseRoute] = [] for route_path, handlers_map in self.get_route_handler_map(value=validated_value).items(): path = join_paths([self.path, route_path]) if http_handlers := unique( [handler for handler in handlers_map.values() if isinstance(handler, HTTPRouteHandler)] ): if existing_handlers := unique( [ handler for handler in self.route_handler_method_map.get(path, {}).values() if isinstance(handler, HTTPRouteHandler) ] ): http_handlers.extend(existing_handlers) existing_route_index = find_index(self.routes, lambda x: x.path == path) # noqa: B023 if existing_route_index == -1: # pragma: no cover raise ImproperlyConfiguredException("unable to find_index existing route index") route: WebSocketRoute | ASGIRoute | HTTPRoute = HTTPRoute( path=path, route_handlers=http_handlers, ) self.routes[existing_route_index] = route else: route = HTTPRoute(path=path, route_handlers=http_handlers) self.routes.append(route) routes.append(route) if websocket_handler := handlers_map.get("websocket"): route = WebSocketRoute(path=path, route_handler=cast("WebsocketRouteHandler", websocket_handler)) self.routes.append(route) routes.append(route) if asgi_handler := handlers_map.get("asgi"): route = ASGIRoute(path=path, route_handler=cast("ASGIRouteHandler", asgi_handler)) self.routes.append(route) routes.append(route) return routes @property def route_handler_method_map(self) -> dict[str, RouteHandlerMapItem]: """Map route paths to :class:`RouteHandlerMapItem ` Returns: A dictionary mapping paths to route handlers """ route_map: dict[str, RouteHandlerMapItem] = defaultdict(dict) for route in self.routes: if isinstance(route, HTTPRoute): for route_handler in route.route_handlers: for method in route_handler.http_methods: route_map[route.path][method] = route_handler else: route_map[route.path]["websocket" if isinstance(route, WebSocketRoute) else "asgi"] = ( route.route_handler ) return route_map @classmethod def get_route_handler_map( cls, value: RouteHandlerType | Router, ) -> dict[str, RouteHandlerMapItem]: """Map route handlers to HTTP methods.""" if isinstance(value, Router): return value.route_handler_method_map copied_value = copy(value) if isinstance(value, HTTPRouteHandler): return {path: {http_method: copied_value for http_method in value.http_methods} for path in value.paths} return { path: {"websocket" if isinstance(value, WebsocketRouteHandler) else "asgi": copied_value} for path in value.paths } def _validate_registration_value(self, value: ControllerRouterHandler) -> RouteHandlerType | Router: """Ensure values passed to the register method are supported.""" if is_class_and_subclass(value, Controller): return value(owner=self).as_router() # this narrows down to an ABC, but we assume a non-abstract subclass of the ABC superclass if is_class_and_subclass(value, WebsocketListener): return value(owner=self).to_handler() # pyright: ignore if isinstance(value, Router): if value is self: raise ImproperlyConfiguredException("Cannot register a router on itself") router_copy = deepcopy(value) router_copy.owner = self return router_copy if isinstance(value, (ASGIRouteHandler, HTTPRouteHandler, WebsocketRouteHandler)): value.owner = self return value raise ImproperlyConfiguredException( "Unsupported value passed to `Router.register`. " "If you passed in a function or method, " "make sure to decorate it first with one of the routing decorators" ) litestar-2.16.0/litestar/routes/000077500000000000000000000000001500564371300166035ustar00rootroot00000000000000litestar-2.16.0/litestar/routes/__init__.py000066400000000000000000000002771500564371300207220ustar00rootroot00000000000000from .asgi import ASGIRoute from .base import BaseRoute from .http import HTTPRoute from .websocket import WebSocketRoute __all__ = ("ASGIRoute", "BaseRoute", "HTTPRoute", "WebSocketRoute") litestar-2.16.0/litestar/routes/asgi.py000066400000000000000000000046511500564371300201060ustar00rootroot00000000000000from __future__ import annotations import warnings from typing import TYPE_CHECKING, Any from litestar.connection import ASGIConnection from litestar.enums import ScopeType from litestar.exceptions import LitestarWarning from litestar.routes.base import BaseRoute if TYPE_CHECKING: from litestar.handlers.asgi_handlers import ASGIRouteHandler from litestar.types import Receive, Scope, Send class ASGIRoute(BaseRoute): """An ASGI route, handling a single ``ASGIRouteHandler``""" __slots__ = ("route_handler",) def __init__( self, *, path: str, route_handler: ASGIRouteHandler, ) -> None: """Initialize the route. Args: path: The path for the route. route_handler: An instance of :class:`~.handlers.ASGIRouteHandler`. """ self.route_handler = route_handler super().__init__( path=path, scope_type=ScopeType.ASGI, handler_names=[route_handler.handler_name], ) async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI app that authorizes the connection and then awaits the handler function. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ if self.route_handler.resolve_guards(): connection = ASGIConnection["ASGIRouteHandler", Any, Any, Any](scope=scope, receive=receive) await self.route_handler.authorize_connection(connection=connection) handler_scope = scope.copy() copy_scope = self.route_handler.copy_scope await self.route_handler.fn( scope=handler_scope if copy_scope is True else scope, receive=receive, send=send, ) if copy_scope is None and handler_scope != scope: warnings.warn( f"{self.route_handler}: Mounted ASGI app {self.route_handler.fn} modified 'scope' with 'copy_scope' " "set to 'None'. Set 'copy_scope=True' to avoid mutating the original scope or set 'copy_scope=False' " "if mutating the scope from within the mounted ASGI app is intentional. Note: 'copy_scope' will " "default to 'True' by default in Litestar 3", category=LitestarWarning, stacklevel=1, ) litestar-2.16.0/litestar/routes/base.py000066400000000000000000000135261500564371300200760ustar00rootroot00000000000000from __future__ import annotations import re from abc import ABC, abstractmethod from datetime import date, datetime, time, timedelta from decimal import Decimal from pathlib import Path from typing import TYPE_CHECKING, Any, Callable from uuid import UUID import msgspec from litestar.exceptions import ImproperlyConfiguredException from litestar.types.internal_types import PathParameterDefinition from litestar.utils import join_paths, normalize_path if TYPE_CHECKING: from litestar.enums import ScopeType from litestar.types import Method, Receive, Scope, Send def _parse_datetime(value: str) -> datetime: return msgspec.convert(value, datetime) def _parse_date(value: str) -> date: return msgspec.convert(value, date) def _parse_time(value: str) -> time: return msgspec.convert(value, time) def _parse_timedelta(value: str) -> timedelta: try: return msgspec.convert(value, timedelta) except msgspec.ValidationError: return timedelta(seconds=int(float(value))) param_match_regex = re.compile(r"{(.*?)}") param_type_map = { "str": str, "int": int, "float": float, "uuid": UUID, "decimal": Decimal, "date": date, "datetime": datetime, "time": time, "timedelta": timedelta, "path": Path, } parsers_map: dict[Any, Callable[[Any], Any]] = { float: float, int: int, Decimal: Decimal, UUID: UUID, date: _parse_date, datetime: _parse_datetime, time: _parse_time, timedelta: _parse_timedelta, } class BaseRoute(ABC): """Base Route class used by Litestar. It's an abstract class meant to be extended. """ __slots__ = ( "app", "handler_names", "methods", "path", "path_components", "path_format", "path_parameters", "scope_type", ) def __init__( self, *, handler_names: list[str], path: str, scope_type: ScopeType, methods: list[Method] | None = None, ) -> None: """Initialize the route. Args: handler_names: Names of the associated handler functions path: Base path of the route scope_type: Type of the ASGI scope methods: Supported methods """ self.path, self.path_format, self.path_components, self.path_parameters = self._parse_path(path) self.handler_names = handler_names self.scope_type = scope_type self.methods = set(methods or []) @abstractmethod async def handle(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI App of the route. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ raise NotImplementedError("Route subclasses must implement handle which serves as the ASGI app entry point") @staticmethod def _validate_path_parameter(param: str, path: str) -> None: """Validate that a path parameter adheres to the required format and datatypes. Raises: ImproperlyConfiguredException: If the parameter has an invalid format. """ if len(param.split(":")) != 2: raise ImproperlyConfiguredException( f"Path parameters should be declared with a type using the following pattern: '{{parameter_name:type}}', e.g. '/my-path/{{my_param:int}}' in path: '{path}'" ) param_name, param_type = (p.strip() for p in param.split(":")) if not param_name: raise ImproperlyConfiguredException("Path parameter names should be of length greater than zero") if param_type not in param_type_map: raise ImproperlyConfiguredException( f"Path parameters should be declared with an allowed type, i.e. one of {', '.join(param_type_map.keys())} in path: '{path}'" ) @classmethod def _parse_path( cls, path: str ) -> tuple[str, str, list[str | PathParameterDefinition], dict[str, PathParameterDefinition]]: """Normalize and parse a path. Splits the path into a list of components, parsing any that are path parameters. Also builds the OpenAPI compatible path, which does not include the type of the path parameters. Returns: A 3-tuple of the normalized path, the OpenAPI formatted path, and the list of parsed components. """ path = normalize_path(path) parsed_components: list[str | PathParameterDefinition] = [] path_format_components = [] path_parameters: dict[str, PathParameterDefinition] = {} components = [component for component in path.split("/") if component] for component in components: if param_match := param_match_regex.fullmatch(component): param = param_match.group(1) cls._validate_path_parameter(param, path) param_name, param_type = (p.strip() for p in param.split(":")) type_class = param_type_map[param_type] parser = parsers_map[type_class] if type_class not in {str, Path} else None if param_name in path_parameters: raise ImproperlyConfiguredException(f"Duplicate parameter '{param_name}' detected in '{path}'.") param_definition = PathParameterDefinition(name=param_name, type=type_class, full=param, parser=parser) parsed_components.append(param_definition) path_parameters[param_name] = param_definition path_format_components.append("{" + param_name + "}") else: parsed_components.append(component) path_format_components.append(component) path_format = join_paths(path_format_components) return path, path_format, parsed_components, path_parameters litestar-2.16.0/litestar/routes/http.py000066400000000000000000000227551500564371300201470ustar00rootroot00000000000000from __future__ import annotations import contextlib from itertools import chain from typing import TYPE_CHECKING, Any from msgspec.msgpack import decode as _decode_msgpack_plain from litestar.datastructures.multi_dicts import FormMultiDict from litestar.enums import HttpMethod, MediaType, ScopeType from litestar.exceptions import ClientException, ImproperlyConfiguredException, SerializationException from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.response import Response from litestar.routes.base import BaseRoute from litestar.status_codes import HTTP_204_NO_CONTENT from litestar.types.empty import Empty from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from litestar._kwargs import KwargsModel from litestar.connection import Request from litestar.types import ASGIApp, HTTPScope, Method, Receive, Scope, Send class HTTPRoute(BaseRoute): """An HTTP route, capable of handling multiple ``HTTPRouteHandler``\\ s.""" # noqa: D301 __slots__ = ( "route_handler_map", "route_handlers", ) def __init__( self, *, path: str, route_handlers: list[HTTPRouteHandler], ) -> None: """Initialize ``HTTPRoute``. Args: path: The path for the route. route_handlers: A list of :class:`~.handlers.HTTPRouteHandler`. """ methods = list(chain.from_iterable([route_handler.http_methods for route_handler in route_handlers])) if "OPTIONS" not in methods: methods.append("OPTIONS") options_handler = self.create_options_handler(path) options_handler.owner = route_handlers[0].owner route_handlers.append(options_handler) self.route_handlers = route_handlers self.route_handler_map: dict[Method, tuple[HTTPRouteHandler, KwargsModel]] = {} super().__init__( methods=methods, path=path, scope_type=ScopeType.HTTP, handler_names=[route_handler.handler_name for route_handler in self.route_handlers], ) async def handle(self, scope: HTTPScope, receive: Receive, send: Send) -> None: # type: ignore[override] """ASGI app that creates a Request from the passed in args, determines which handler function to call and then handles the call. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ route_handler, parameter_model = self.route_handler_map[scope["method"]] request: Request[Any, Any, Any] = route_handler.resolve_request_class()(scope=scope, receive=receive, send=send) if route_handler.resolve_guards(): await route_handler.authorize_connection(connection=request) try: response = await self._get_response_for_request( scope=scope, request=request, route_handler=route_handler, parameter_model=parameter_model ) await response(scope, receive, send) if after_response_handler := route_handler.resolve_after_response(): await after_response_handler(request) finally: if (form_data := ScopeState.from_scope(scope).form) is not Empty: await FormMultiDict.from_form_data(form_data).close() def create_handler_map(self) -> None: """Parse the ``router_handlers`` of this route and return a mapping of http- methods and route handlers. """ for route_handler in self.route_handlers: kwargs_model = route_handler.create_kwargs_model(path_parameters=self.path_parameters) for http_method in route_handler.http_methods: if self.route_handler_map.get(http_method): raise ImproperlyConfiguredException( f"Handler already registered for path {self.path!r} and http method {http_method}" ) self.route_handler_map[http_method] = (route_handler, kwargs_model) async def _get_response_for_request( self, scope: Scope, request: Request[Any, Any, Any], route_handler: HTTPRouteHandler, parameter_model: KwargsModel, ) -> ASGIApp: """Return a response for the request. If caching is enabled and a response exist in the cache, the cached response will be returned. If caching is enabled and a response does not exist in the cache, the newly created response will be cached. Args: scope: The Request's scope request: The Request instance route_handler: The HTTPRouteHandler instance parameter_model: The Handler's KwargsModel Returns: An instance of Response or a compatible ASGIApp or a subclass of it """ if route_handler.cache and ( response := await self._get_cached_response(request=request, route_handler=route_handler) ): return response return await self._call_handler_function( scope=scope, request=request, parameter_model=parameter_model, route_handler=route_handler ) async def _call_handler_function( # type: ignore[return] self, scope: Scope, request: Request, parameter_model: KwargsModel, route_handler: HTTPRouteHandler ) -> ASGIApp: # pyright: ignore[reportGeneralTypeIssues] """Call the before request handlers, retrieve any data required for the route handler, and call the route handler's ``to_response`` method. This is wrapped in a try except block - and if an exception is raised, it tries to pass it to an appropriate exception handler - if defined. """ response_data: Any = None if before_request_handler := route_handler.resolve_before_request(): response_data = await before_request_handler(request) # create and enter an AsyncExit stack as we may or may not have a # 'DependencyCleanupGroup' to enter and exit stack = contextlib.AsyncExitStack() # mypy cannot infer that 'stack' never swallows exceptions, therefore it thinks # this method is potentially missing a 'return' statement async with stack: if not response_data: parsed_kwargs: dict[str, Any] = {} if parameter_model.has_kwargs and route_handler.signature_model: try: kwargs = await parameter_model.to_kwargs(connection=request) except SerializationException as e: raise ClientException(str(e)) from e if kwargs.get("data") is Empty: del kwargs["data"] if parameter_model.dependency_batches: cleanup_group = await parameter_model.resolve_dependencies(request, kwargs) await stack.enter_async_context(cleanup_group) parsed_kwargs = route_handler.signature_model.parse_values_from_connection_kwargs( connection=request, kwargs=kwargs ) response_data = ( route_handler.fn(**parsed_kwargs) if route_handler.has_sync_callable else await route_handler.fn(**parsed_kwargs) ) return await route_handler.to_response(app=scope["litestar_app"], data=response_data, request=request) @staticmethod async def _get_cached_response(request: Request, route_handler: HTTPRouteHandler) -> ASGIApp | None: """Retrieve and un-pickle the cached response, if existing. Args: request: The :class:`Request ` instance route_handler: The :class:`~.handlers.HTTPRouteHandler` instance Returns: A cached response instance, if existing. """ cache_config = request.app.response_cache_config cache_key = (route_handler.cache_key_builder or cache_config.key_builder)(request) store = cache_config.get_store_from_app(request.app) if not (cached_response_data := await store.get(key=cache_key)): return None # we use the regular msgspec.msgpack.decode here since we don't need any of # the added decoders messages = _decode_msgpack_plain(cached_response_data) async def cached_response(scope: Scope, receive: Receive, send: Send) -> None: ScopeState.from_scope(scope).is_cached = True for message in messages: await send(message) return cached_response def create_options_handler(self, path: str) -> HTTPRouteHandler: """Args: path: The route path Returns: An HTTP route handler for OPTIONS requests. """ def options_handler(scope: Scope) -> Response: """Handler function for OPTIONS requests. Args: scope: The ASGI Scope. Returns: Response """ return Response( content=None, status_code=HTTP_204_NO_CONTENT, headers={"Allow": ", ".join(sorted(self.methods))}, # pyright: ignore media_type=MediaType.TEXT, ) return HTTPRouteHandler( path=path, http_method=[HttpMethod.OPTIONS], include_in_schema=False, sync_to_thread=False, )(options_handler) litestar-2.16.0/litestar/routes/websocket.py000066400000000000000000000056661500564371300211600ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from litestar.enums import ScopeType from litestar.exceptions import ImproperlyConfiguredException from litestar.routes.base import BaseRoute if TYPE_CHECKING: from litestar._kwargs import KwargsModel from litestar._kwargs.cleanup import DependencyCleanupGroup from litestar.connection import WebSocket from litestar.handlers.websocket_handlers import WebsocketRouteHandler from litestar.types import Receive, Send, WebSocketScope class WebSocketRoute(BaseRoute): """A websocket route, handling a single ``WebsocketRouteHandler``""" __slots__ = ( "handler_parameter_model", "route_handler", ) def __init__( self, *, path: str, route_handler: WebsocketRouteHandler, ) -> None: """Initialize the route. Args: path: The path for the route. route_handler: An instance of :class:`~.handlers.WebsocketRouteHandler`. """ self.route_handler = route_handler self.handler_parameter_model: KwargsModel | None = None super().__init__( path=path, scope_type=ScopeType.WEBSOCKET, handler_names=[route_handler.handler_name], ) async def handle(self, scope: WebSocketScope, receive: Receive, send: Send) -> None: # type: ignore[override] """ASGI app that creates a WebSocket from the passed in args, and then awaits the handler function. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ if not self.handler_parameter_model: # pragma: no cover raise ImproperlyConfiguredException("handler parameter model not defined") websocket: WebSocket[Any, Any, Any] = self.route_handler.resolve_websocket_class()( scope=scope, receive=receive, send=send ) if self.route_handler.resolve_guards(): await self.route_handler.authorize_connection(connection=websocket) parsed_kwargs: dict[str, Any] = {} cleanup_group: DependencyCleanupGroup | None = None if self.handler_parameter_model.has_kwargs and self.route_handler.signature_model: parsed_kwargs = await self.handler_parameter_model.to_kwargs(connection=websocket) if self.handler_parameter_model.dependency_batches: cleanup_group = await self.handler_parameter_model.resolve_dependencies(websocket, parsed_kwargs) parsed_kwargs = self.route_handler.signature_model.parse_values_from_connection_kwargs( connection=websocket, kwargs=parsed_kwargs ) if cleanup_group: async with cleanup_group: await self.route_handler.fn(**parsed_kwargs) else: await self.route_handler.fn(**parsed_kwargs) litestar-2.16.0/litestar/security/000077500000000000000000000000001500564371300171315ustar00rootroot00000000000000litestar-2.16.0/litestar/security/__init__.py000066400000000000000000000001411500564371300212360ustar00rootroot00000000000000from litestar.security.base import AbstractSecurityConfig __all__ = ("AbstractSecurityConfig",) litestar-2.16.0/litestar/security/base.py000066400000000000000000000161771500564371300204310ustar00rootroot00000000000000from __future__ import annotations from abc import ABC, abstractmethod from copy import copy from dataclasses import field from typing import TYPE_CHECKING, Any, Callable, Generic, Iterable, Sequence, TypeVar, cast from litestar import Response from litestar.utils.sync import ensure_async_callable if TYPE_CHECKING: from litestar.config.app import AppConfig from litestar.connection import ASGIConnection from litestar.di import Provide from litestar.enums import MediaType, OpenAPIMediaType from litestar.middleware.authentication import AbstractAuthenticationMiddleware from litestar.middleware.base import DefineMiddleware from litestar.openapi.spec import Components, SecurityRequirement from litestar.types import ( ControllerRouterHandler, Guard, Method, ResponseCookies, Scopes, SyncOrAsyncUnion, TypeEncodersMap, ) __all__ = ("AbstractSecurityConfig",) UserType = TypeVar("UserType") AuthType = TypeVar("AuthType") class AbstractSecurityConfig(ABC, Generic[UserType, AuthType]): """A base class for Security Configs - this class can be used on the application level or be manually configured on the router / controller level to provide auth. """ authentication_middleware_class: type[AbstractAuthenticationMiddleware] """The authentication middleware class to use. Must inherit from :class:`AbstractAuthenticationMiddleware ` """ guards: Iterable[Guard] | None = None """An iterable of guards to call for requests, providing authorization functionalities.""" exclude: str | list[str] | None = None """A pattern or list of patterns to skip in the authentication middleware.""" exclude_opt_key: str = "exclude_from_auth" """An identifier to use on routes to disable authentication and authorization checks for a particular route.""" exclude_http_methods: Sequence[Method] | None = field( default_factory=lambda: cast("Sequence[Method]", ["OPTIONS", "HEAD"]) ) """A sequence of http methods that do not require authentication. Defaults to ['OPTIONS', 'HEAD']""" scopes: Scopes | None = None """ASGI scopes processed by the authentication middleware, if ``None``, both ``http`` and ``websocket`` will be processed.""" route_handlers: Iterable[ControllerRouterHandler] | None = None """An optional iterable of route handlers to register.""" dependencies: dict[str, Provide] | None = None """An optional dictionary of dependency providers.""" retrieve_user_handler: Callable[[Any, ASGIConnection], SyncOrAsyncUnion[Any | None]] """Callable that receives the ``auth`` value from the authentication middleware and returns a ``user`` value. Notes: - User and Auth can be any arbitrary values specified by the security backend. - The User and Auth values will be set by the middleware as ``scope["user"]`` and ``scope["auth"]`` respectively. Once provided, they can access via the ``connection.user`` and ``connection.auth`` properties. - The callable can be sync or async. If it is sync, it will be wrapped to support async. """ type_encoders: TypeEncodersMap | None = None """A mapping of types to callables that transform them into types supported for serialization.""" def on_app_init(self, app_config: AppConfig) -> AppConfig: """Handle app init by injecting middleware, guards etc. into the app. This method can be used only on the app level. Args: app_config: An instance of :class:`AppConfig <.config.app.AppConfig>` Returns: The :class:`AppConfig <.config.app.AppConfig>`. """ app_config.middleware.insert(0, self.middleware) if app_config.openapi_config: app_config.openapi_config = copy(app_config.openapi_config) if isinstance(app_config.openapi_config.components, list): app_config.openapi_config.components.append(self.openapi_components) else: app_config.openapi_config.components = [self.openapi_components, app_config.openapi_config.components] if isinstance(app_config.openapi_config.security, list): app_config.openapi_config.security.append(self.security_requirement) else: app_config.openapi_config.security = [self.security_requirement] if self.guards: app_config.guards.extend(self.guards) if self.dependencies: app_config.dependencies.update(self.dependencies) if self.route_handlers: app_config.route_handlers.extend(self.route_handlers) if self.type_encoders is None: self.type_encoders = app_config.type_encoders return app_config def create_response( self, content: Any | None, status_code: int, media_type: MediaType | OpenAPIMediaType | str, headers: dict[str, Any] | None = None, cookies: ResponseCookies | None = None, ) -> Response[Any]: """Create a response object. Handles setting the type encoders mapping on the response. Args: content: A value for the response body that will be rendered into bytes string. status_code: An HTTP status code. media_type: A value for the response 'Content-Type' header. headers: A string keyed dictionary of response headers. Header keys are insensitive. cookies: A list of :class:`Cookie ` instances to be set under the response 'Set-Cookie' header. Returns: A response object. """ return Response( content=content, status_code=status_code, media_type=media_type, headers=headers, cookies=cookies, type_encoders=self.type_encoders, ) def __post_init__(self) -> None: self.retrieve_user_handler = ensure_async_callable(self.retrieve_user_handler) @property @abstractmethod def openapi_components(self) -> Components: """Create OpenAPI documentation for the JWT auth schema used. Returns: An :class:`Components ` instance. """ raise NotImplementedError @property @abstractmethod def security_requirement(self) -> SecurityRequirement: """Return OpenAPI 3.1. :data:`SecurityRequirement <.openapi.spec.SecurityRequirement>` for the auth backend. Returns: An OpenAPI 3.1 :data:`SecurityRequirement <.openapi.spec.SecurityRequirement>` dictionary. """ raise NotImplementedError @property @abstractmethod def middleware(self) -> DefineMiddleware: """Create an instance of the config's ``authentication_middleware_class`` attribute and any required kwargs, wrapping it in Litestar's ``DefineMiddleware``. Returns: An instance of :class:`DefineMiddleware `. """ raise NotImplementedError litestar-2.16.0/litestar/security/jwt/000077500000000000000000000000001500564371300177355ustar00rootroot00000000000000litestar-2.16.0/litestar/security/jwt/__init__.py000066400000000000000000000010041500564371300220410ustar00rootroot00000000000000from litestar.security.jwt.auth import ( BaseJWTAuth, JWTAuth, JWTCookieAuth, OAuth2Login, OAuth2PasswordBearerAuth, ) from litestar.security.jwt.middleware import ( JWTAuthenticationMiddleware, JWTCookieAuthenticationMiddleware, ) from litestar.security.jwt.token import Token __all__ = ( "BaseJWTAuth", "JWTAuth", "JWTAuthenticationMiddleware", "JWTCookieAuth", "JWTCookieAuthenticationMiddleware", "OAuth2Login", "OAuth2PasswordBearerAuth", "Token", ) litestar-2.16.0/litestar/security/jwt/auth.py000066400000000000000000001115271500564371300212570ustar00rootroot00000000000000from __future__ import annotations from dataclasses import asdict, dataclass, field from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Any, Callable, Generic, Iterable, Literal, Sequence, cast from typing_extensions import TypeVar from litestar.datastructures import Cookie from litestar.enums import MediaType from litestar.middleware import DefineMiddleware from litestar.openapi.spec import Components, OAuthFlow, OAuthFlows, SecurityRequirement, SecurityScheme from litestar.security.base import AbstractSecurityConfig from litestar.security.jwt.middleware import JWTAuthenticationMiddleware, JWTCookieAuthenticationMiddleware from litestar.security.jwt.token import Token from litestar.status_codes import HTTP_201_CREATED from litestar.types import ControllerRouterHandler, Empty, Guard, Method, Scopes, SyncOrAsyncUnion, TypeEncodersMap __all__ = ("BaseJWTAuth", "JWTAuth", "JWTCookieAuth", "OAuth2Login", "OAuth2PasswordBearerAuth") if TYPE_CHECKING: from litestar import Response from litestar.connection import ASGIConnection from litestar.di import Provide UserType = TypeVar("UserType") TokenT = TypeVar("TokenT", bound=Token, default=Token) class BaseJWTAuth(Generic[UserType, TokenT], AbstractSecurityConfig[UserType, TokenT]): """Base class for JWT Auth backends""" token_secret: str """Key with which to generate the token hash. Notes: - This value should be kept as a secret and the standard practice is to inject it into the environment. """ retrieve_user_handler: Callable[[Any, ASGIConnection], SyncOrAsyncUnion[Any | None]] """Callable that receives the ``auth`` value from the authentication middleware and returns a ``user`` value. Notes: - User and Auth can be any arbitrary values specified by the security backend. - The User and Auth values will be set by the middleware as ``scope["user"]`` and ``scope["auth"]`` respectively. Once provided, they can access via the ``connection.user`` and ``connection.auth`` properties. - The callable can be sync or async. If it is sync, it will be wrapped to support async. """ revoked_token_handler: Callable[[Any, ASGIConnection], SyncOrAsyncUnion[bool]] | None = field(default=None) """Callable that receives the auth value from the authentication middleware and checks whether the token has been revoked, returning True if revoked, False otherwise.""" algorithm: str """Algorithm to use for JWT hashing.""" auth_header: str """Request header key from which to retrieve the token. E.g. ``Authorization`` or ``X-Api-Key``. """ default_token_expiration: timedelta """The default value for token expiration.""" openapi_security_scheme_name: str """The value to use for the OpenAPI security scheme and security requirements.""" description: str """Description for the OpenAPI security scheme.""" authentication_middleware_class: type[JWTAuthenticationMiddleware] # pyright: ignore """The authentication middleware class to use. Must inherit from :class:`JWTAuthenticationMiddleware` """ token_cls: type[Token] = Token """Target type the JWT payload will be converted into""" accepted_audiences: Sequence[str] | None = None """Audiences to accept when verifying the token. If given, and the audience in the token does not match, a 401 response is returned """ accepted_issuers: Sequence[str] | None = None """Issuers to accept when verifying the token. If given, and the issuer in the token does not match, a 401 response is returned """ require_claims: Sequence[str] | None = None """Require these claims to be present in the JWT payload. If any of those claims is missing, a 401 response is returned """ verify_expiry: bool = True """Verify that the value of the ``exp`` (*expiration*) claim is in the future""" verify_not_before: bool = True """Verify that the value of the ``nbf`` (*not before*) claim is in the past""" strict_audience: bool = False """Verify that the value of the ``aud`` (*audience*) claim is a single value, and not a list of values, and matches ``audience`` exactly. Requires that ``accepted_audiences`` is a sequence of length 1 """ @property def openapi_components(self) -> Components: """Create OpenAPI documentation for the JWT auth schema used. Returns: An :class:`Components ` instance. """ return Components( security_schemes={ self.openapi_security_scheme_name: SecurityScheme( type="http", scheme="Bearer", name=self.auth_header, bearer_format="JWT", description=self.description, ) } ) @property def security_requirement(self) -> SecurityRequirement: """Return OpenAPI 3.1. :data:`SecurityRequirement <.openapi.spec.SecurityRequirement>` Returns: An OpenAPI 3.1 :data:`SecurityRequirement <.openapi.spec.SecurityRequirement>` dictionary. """ return {self.openapi_security_scheme_name: []} @property def middleware(self) -> DefineMiddleware: """Create :class:`JWTAuthenticationMiddleware` wrapped in :class:`DefineMiddleware <.middleware.base.DefineMiddleware>`. Returns: An instance of :class:`DefineMiddleware <.middleware.base.DefineMiddleware>`. """ return DefineMiddleware( self.authentication_middleware_class, algorithm=self.algorithm, auth_header=self.auth_header, exclude=self.exclude, exclude_opt_key=self.exclude_opt_key, exclude_http_methods=self.exclude_http_methods, retrieve_user_handler=self.retrieve_user_handler, revoked_token_handler=self.revoked_token_handler, scopes=self.scopes, token_secret=self.token_secret, token_cls=self.token_cls, token_issuer=self.accepted_issuers, token_audience=self.accepted_audiences, require_claims=self.require_claims, verify_expiry=self.verify_expiry, verify_not_before=self.verify_not_before, strict_audience=self.strict_audience, ) def login( self, identifier: str, *, response_body: Any = Empty, response_media_type: str | MediaType = MediaType.JSON, response_status_code: int = HTTP_201_CREATED, token_expiration: timedelta | None = None, token_issuer: str | None = None, token_audience: str | None = None, token_unique_jwt_id: str | None = None, token_extras: dict[str, Any] | None = None, send_token_as_response_body: bool = False, ) -> Response[Any]: """Create a response with a JWT header. Args: identifier: Unique identifier of the token subject. Usually this is a user ID or equivalent kind of value. response_body: An optional response body to send. response_media_type: An optional ``Content-Type``. Defaults to ``application/json``. response_status_code: An optional status code for the response. Defaults to ``201``. token_expiration: An optional timedelta for the token expiration. token_issuer: An optional value of the token ``iss`` field. token_audience: An optional value for the token ``aud`` field. token_unique_jwt_id: An optional value for the token ``jti`` field. token_extras: An optional dictionary to include in the token ``extras`` field. send_token_as_response_body: If ``True`` the response will be a dict including the token: ``{ "token": }`` will be returned as the response body. Note: if a response body is passed this setting will be ignored. Returns: A :class:`Response <.response.Response>` instance. """ encoded_token = self.create_token( identifier=identifier, token_expiration=token_expiration, token_issuer=token_issuer, token_audience=token_audience, token_unique_jwt_id=token_unique_jwt_id, token_extras=token_extras, ) if response_body is not Empty: body = response_body elif send_token_as_response_body: body = {"token": encoded_token} else: body = None return self.create_response( content=body, headers={self.auth_header: self.format_auth_header(encoded_token)}, media_type=response_media_type, status_code=response_status_code, ) def create_token( self, identifier: str, token_expiration: timedelta | None = None, token_issuer: str | None = None, token_audience: str | None = None, token_unique_jwt_id: str | None = None, token_extras: dict | None = None, **kwargs: Any, ) -> str: """Create a Token instance from the passed in parameters, persists and returns it. Args: identifier: Unique identifier of the token subject. Usually this is a user ID or equivalent kind of value. token_expiration: An optional timedelta for the token expiration. token_issuer: An optional value of the token ``iss`` field. token_audience: An optional value for the token ``aud`` field. token_unique_jwt_id: An optional value for the token ``jti`` field. token_extras: An optional dictionary to include in the token ``extras`` field. **kwargs: Additional attributes to set on the token Returns: The created token. """ token = self.token_cls( sub=identifier, exp=(datetime.now(timezone.utc) + (token_expiration or self.default_token_expiration)), iss=token_issuer, aud=token_audience, jti=token_unique_jwt_id, extras=token_extras or {}, **kwargs, ) return token.encode(secret=self.token_secret, algorithm=self.algorithm) def format_auth_header(self, encoded_token: str) -> str: """Format a token according to the specified OpenAPI scheme. Args: encoded_token: An encoded JWT token Returns: The encoded token formatted for the HTTP headers """ security = self.openapi_components.security_schemes.get(self.openapi_security_scheme_name, None) # type: ignore[union-attr] return f"{security.scheme} {encoded_token}" if isinstance(security, SecurityScheme) else encoded_token @dataclass class JWTAuth(Generic[UserType, TokenT], BaseJWTAuth[UserType, TokenT]): """JWT Authentication Configuration. This class is the main entry point to the library, and it includes methods to create the middleware, provide login functionality, and create OpenAPI documentation. """ token_secret: str """Key with which to generate the token hash. Notes: - This value should be kept as a secret and the standard practice is to inject it into the environment. """ retrieve_user_handler: Callable[[Any, ASGIConnection], SyncOrAsyncUnion[Any | None]] """Callable that receives the ``auth`` value from the authentication middleware and returns a ``user`` value. Notes: - User and Auth can be any arbitrary values specified by the security backend. - The User and Auth values will be set by the middleware as ``scope["user"]`` and ``scope["auth"]`` respectively. Once provided, they can access via the ``connection.user`` and ``connection.auth`` properties. - The callable can be sync or async. If it is sync, it will be wrapped to support async. """ revoked_token_handler: Callable[[Any, ASGIConnection], SyncOrAsyncUnion[bool]] | None = field(default=None) """Callable that receives the auth value from the authentication middleware and checks whether the token has been revoked, returning True if revoked, False otherwise.""" guards: Iterable[Guard] | None = field(default=None) """An iterable of guards to call for requests, providing authorization functionalities.""" exclude: str | list[str] | None = field(default=None) """A pattern or list of patterns to skip in the authentication middleware.""" exclude_opt_key: str = field(default="exclude_from_auth") """An identifier to use on routes to disable authentication and authorization checks for a particular route.""" exclude_http_methods: Sequence[Method] | None = field( default_factory=lambda: cast("Sequence[Method]", ["OPTIONS", "HEAD"]) ) """A sequence of http methods that do not require authentication. Defaults to ['OPTIONS', 'HEAD']""" scopes: Scopes | None = field(default=None) """ASGI scopes processed by the authentication middleware, if ``None``, both ``http`` and ``websocket`` will be processed.""" route_handlers: Iterable[ControllerRouterHandler] | None = field(default=None) """An optional iterable of route handlers to register.""" dependencies: dict[str, Provide] | None = field(default=None) """An optional dictionary of dependency providers.""" type_encoders: TypeEncodersMap | None = field(default=None) """A mapping of types to callables that transform them into types supported for serialization.""" algorithm: str = field(default="HS256") """Algorithm to use for JWT hashing.""" auth_header: str = field(default="Authorization") """Request header key from which to retrieve the token. E.g. ``Authorization`` or ``X-Api-Key``. """ default_token_expiration: timedelta = field(default_factory=lambda: timedelta(days=1)) """The default value for token expiration.""" openapi_security_scheme_name: str = field(default="BearerToken") """The value to use for the OpenAPI security scheme and security requirements.""" description: str = field(default="JWT api-key authentication and authorization.") """Description for the OpenAPI security scheme.""" authentication_middleware_class: type[JWTAuthenticationMiddleware] = field(default=JWTAuthenticationMiddleware) """The authentication middleware class to use. Must inherit from :class:`JWTAuthenticationMiddleware` """ token_cls: type[Token] = Token """Target type the JWT payload will be converted into""" accepted_audiences: Sequence[str] | None = None """Audiences to accept when verifying the token. If given, and the audience in the token does not match, a 401 response is returned """ accepted_issuers: Sequence[str] | None = None """Issuers to accept when verifying the token. If given, and the issuer in the token does not match, a 401 response is returned """ require_claims: Sequence[str] | None = None """Require these claims to be present in the JWT payload. If any of those claims is missing, a 401 response is returned """ verify_expiry: bool = True """Verify that the value of the ``exp`` (*expiration*) claim is in the future""" verify_not_before: bool = True """Verify that the value of the ``nbf`` (*not before*) claim is in the past""" strict_audience: bool = False """Verify that the value of the ``aud`` (*audience*) claim is a single value, and not a list of values, and matches ``audience`` exactly. Requires that ``accepted_audiences`` is a sequence of length 1 """ @dataclass class JWTCookieAuth(Generic[UserType, TokenT], BaseJWTAuth[UserType, TokenT]): """JWT Cookie Authentication Configuration. This class is an alternate entry point to the library, and it includes all the functionality of the :class:`JWTAuth` class and adds support for passing JWT tokens ``HttpOnly`` cookies. """ token_secret: str """Key with which to generate the token hash. Notes: - This value should be kept as a secret and the standard practice is to inject it into the environment. """ retrieve_user_handler: Callable[[Any, ASGIConnection], SyncOrAsyncUnion[Any | None]] """Callable that receives the ``auth`` value from the authentication middleware and returns a ``user`` value. Notes: - User and Auth can be any arbitrary values specified by the security backend. - The User and Auth values will be set by the middleware as ``scope["user"]`` and ``scope["auth"]`` respectively. Once provided, they can access via the ``connection.user`` and ``connection.auth`` properties. - The callable can be sync or async. If it is sync, it will be wrapped to support async. """ revoked_token_handler: Callable[[Any, ASGIConnection], SyncOrAsyncUnion[bool]] | None = field(default=None) """Callable that receives the auth value from the authentication middleware and checks whether the token has been revoked, returning True if revoked, False otherwise.""" guards: Iterable[Guard] | None = field(default=None) """An iterable of guards to call for requests, providing authorization functionalities.""" exclude: str | list[str] | None = field(default=None) """A pattern or list of patterns to skip in the authentication middleware.""" exclude_opt_key: str = field(default="exclude_from_auth") """An identifier to use on routes to disable authentication and authorization checks for a particular route.""" scopes: Scopes | None = field(default=None) """ASGI scopes processed by the authentication middleware, if ``None``, both ``http`` and ``websocket`` will be processed.""" exclude_http_methods: Sequence[Method] | None = field( default_factory=lambda: cast("Sequence[Method]", ["OPTIONS", "HEAD"]) ) """A sequence of http methods that do not require authentication. Defaults to ['OPTIONS', 'HEAD']""" route_handlers: Iterable[ControllerRouterHandler] | None = field(default=None) """An optional iterable of route handlers to register.""" dependencies: dict[str, Provide] | None = field(default=None) """An optional dictionary of dependency providers.""" type_encoders: TypeEncodersMap | None = field(default=None) """A mapping of types to callables that transform them into types supported for serialization.""" algorithm: str = field(default="HS256") """Algorithm to use for JWT hashing.""" auth_header: str = field(default="Authorization") """Request header key from which to retrieve the token. E.g. ``Authorization`` or ``X-Api-Key``. """ default_token_expiration: timedelta = field(default_factory=lambda: timedelta(days=1)) """The default value for token expiration.""" openapi_security_scheme_name: str = field(default="BearerToken") """The value to use for the OpenAPI security scheme and security requirements.""" key: str = field(default="token") """Key for the cookie.""" path: str = field(default="/") """Path fragment that must exist in the request url for the cookie to be valid. Defaults to ``/``. """ domain: str | None = field(default=None) """Domain for which the cookie is valid.""" secure: bool | None = field(default=None) """Https is required for the cookie.""" samesite: Literal["lax", "strict", "none"] = field(default="lax") """Controls whether or not a cookie is sent with cross-site requests. Defaults to ``lax``. """ description: str = field(default="JWT cookie-based authentication and authorization.") """Description for the OpenAPI security scheme.""" authentication_middleware_class: type[JWTCookieAuthenticationMiddleware] = field( # pyright: ignore default=JWTCookieAuthenticationMiddleware ) """The authentication middleware class to use. Must inherit from :class:`JWTCookieAuthenticationMiddleware` """ token_cls: type[Token] = Token """Target type the JWT payload will be converted into""" accepted_audiences: Sequence[str] | None = None """Audiences to accept when verifying the token. If given, and the audience in the token does not match, a 401 response is returned """ accepted_issuers: Sequence[str] | None = None """Issuers to accept when verifying the token. If given, and the issuer in the token does not match, a 401 response is returned """ require_claims: Sequence[str] | None = None """Require these claims to be present in the JWT payload. If any of those claims is missing, a 401 response is returned """ verify_expiry: bool = True """Verify that the value of the ``exp`` (*expiration*) claim is in the future""" verify_not_before: bool = True """Verify that the value of the ``nbf`` (*not before*) claim is in the past""" strict_audience: bool = False """Verify that the value of the ``aud`` (*audience*) claim is a single value, and not a list of values, and matches ``audience`` exactly. Requires that ``accepted_audiences`` is a sequence of length 1 """ @property def openapi_components(self) -> Components: """Create OpenAPI documentation for the JWT Cookie auth scheme. Returns: A :class:`Components ` instance. """ return Components( security_schemes={ self.openapi_security_scheme_name: SecurityScheme( type="http", scheme="Bearer", name=self.key, security_scheme_in="cookie", bearer_format="JWT", description=self.description, ) } ) @property def middleware(self) -> DefineMiddleware: """Create :class:`JWTCookieAuthenticationMiddleware` wrapped in :class:`DefineMiddleware <.middleware.base.DefineMiddleware>`. Returns: An instance of :class:`DefineMiddleware <.middleware.base.DefineMiddleware>`. """ return DefineMiddleware( self.authentication_middleware_class, algorithm=self.algorithm, auth_cookie_key=self.key, auth_header=self.auth_header, exclude=self.exclude, exclude_opt_key=self.exclude_opt_key, exclude_http_methods=self.exclude_http_methods, retrieve_user_handler=self.retrieve_user_handler, revoked_token_handler=self.revoked_token_handler, scopes=self.scopes, token_secret=self.token_secret, token_cls=self.token_cls, token_issuer=self.accepted_issuers, token_audience=self.accepted_audiences, require_claims=self.require_claims, verify_expiry=self.verify_expiry, verify_not_before=self.verify_not_before, strict_audience=self.strict_audience, ) def login( self, identifier: str, *, response_body: Any = Empty, response_media_type: str | MediaType = MediaType.JSON, response_status_code: int = HTTP_201_CREATED, token_expiration: timedelta | None = None, token_issuer: str | None = None, token_audience: str | None = None, token_unique_jwt_id: str | None = None, token_extras: dict[str, Any] | None = None, send_token_as_response_body: bool = False, ) -> Response[Any]: """Create a response with a JWT header. Args: identifier: Unique identifier of the token subject. Usually this is a user ID or equivalent kind of value. response_body: An optional response body to send. response_media_type: An optional 'Content-Type'. Defaults to 'application/json'. response_status_code: An optional status code for the response. Defaults to '201 Created'. token_expiration: An optional timedelta for the token expiration. token_issuer: An optional value of the token ``iss`` field. token_audience: An optional value for the token ``aud`` field. token_unique_jwt_id: An optional value for the token ``jti`` field. token_extras: An optional dictionary to include in the token ``extras`` field. send_token_as_response_body: If ``True`` the response will be a dict including the token: ``{ "token": }`` will be returned as the response body. Note: if a response body is passed this setting will be ignored. Returns: A :class:`Response <.response.Response>` instance. """ encoded_token = self.create_token( identifier=identifier, token_expiration=token_expiration, token_issuer=token_issuer, token_audience=token_audience, token_unique_jwt_id=token_unique_jwt_id, token_extras=token_extras, ) cookie = Cookie( key=self.key, path=self.path, httponly=True, value=self.format_auth_header(encoded_token), max_age=int((token_expiration or self.default_token_expiration).total_seconds()), secure=self.secure, samesite=self.samesite, domain=self.domain, ) if response_body is not Empty: body = response_body elif send_token_as_response_body: body = {"token": encoded_token} else: body = None return self.create_response( content=body, headers={self.auth_header: self.format_auth_header(encoded_token)}, cookies=[cookie], media_type=response_media_type, status_code=response_status_code, ) @dataclass class OAuth2Login: """OAuth2 Login DTO""" access_token: str """Valid JWT access token""" token_type: str """Type of the OAuth token used""" refresh_token: str | None = field(default=None) """Optional valid refresh token JWT""" expires_in: int | None = field(default=None) """Expiration time of the token in seconds. """ @dataclass class OAuth2PasswordBearerAuth(Generic[UserType, TokenT], BaseJWTAuth[UserType, TokenT]): """OAUTH2 Schema for Password Bearer Authentication. This class implements an OAUTH2 authentication flow entry point to the library, and it includes all the functionality of the :class:`JWTAuth` class and adds support for passing JWT tokens ``HttpOnly`` cookies. ``token_url`` is the only additional argument that is required, and it should point at your login route """ token_secret: str """Key with which to generate the token hash. Notes: - This value should be kept as a secret and the standard practice is to inject it into the environment. """ token_url: str """The URL for retrieving a new token.""" retrieve_user_handler: Callable[[Any, ASGIConnection], SyncOrAsyncUnion[Any | None]] """Callable that receives the ``auth`` value from the authentication middleware and returns a ``user`` value. Notes: - User and Auth can be any arbitrary values specified by the security backend. - The User and Auth values will be set by the middleware as ``scope["user"]`` and ``scope["auth"]`` respectively. Once provided, they can access via the ``connection.user`` and ``connection.auth`` properties. - The callable can be sync or async. If it is sync, it will be wrapped to support async. """ revoked_token_handler: Callable[[Any, ASGIConnection], SyncOrAsyncUnion[bool]] | None = field(default=None) """Callable that receives the auth value from the authentication middleware and checks whether the token has been revoked, returning True if revoked, False otherwise.""" guards: Iterable[Guard] | None = field(default=None) """An iterable of guards to call for requests, providing authorization functionalities.""" exclude: str | list[str] | None = field(default=None) """A pattern or list of patterns to skip in the authentication middleware.""" exclude_opt_key: str = field(default="exclude_from_auth") """An identifier to use on routes to disable authentication and authorization checks for a particular route.""" exclude_http_methods: Sequence[Method] | None = field( default_factory=lambda: cast("Sequence[Method]", ["OPTIONS", "HEAD"]) ) """A sequence of http methods that do not require authentication. Defaults to ['OPTIONS', 'HEAD']""" scopes: Scopes | None = field(default=None) """ASGI scopes processed by the authentication middleware, if ``None``, both ``http`` and ``websocket`` will be processed.""" route_handlers: Iterable[ControllerRouterHandler] | None = field(default=None) """An optional iterable of route handlers to register.""" dependencies: dict[str, Provide] | None = field(default=None) """An optional dictionary of dependency providers.""" type_encoders: TypeEncodersMap | None = field(default=None) """A mapping of types to callables that transform them into types supported for serialization.""" algorithm: str = field(default="HS256") """Algorithm to use for JWT hashing.""" auth_header: str = field(default="Authorization") """Request header key from which to retrieve the token. E.g. ``Authorization`` or 'X-Api-Key'. """ default_token_expiration: timedelta = field(default_factory=lambda: timedelta(days=1)) """The default value for token expiration.""" openapi_security_scheme_name: str = field(default="BearerToken") """The value to use for the OpenAPI security scheme and security requirements.""" oauth_scopes: dict[str, str] | None = field(default=None) """Oauth Scopes available for the token.""" key: str = field(default="token") """Key for the cookie.""" path: str = field(default="/") """Path fragment that must exist in the request url for the cookie to be valid. Defaults to ``/``. """ domain: str | None = field(default=None) """Domain for which the cookie is valid.""" secure: bool | None = field(default=None) """Https is required for the cookie.""" samesite: Literal["lax", "strict", "none"] = field(default="lax") """Controls whether or not a cookie is sent with cross-site requests. Defaults to ``lax``. """ description: str = field(default="OAUTH2 password bearer authentication and authorization.") """Description for the OpenAPI security scheme.""" authentication_middleware_class: type[JWTCookieAuthenticationMiddleware] = field( # pyright: ignore default=JWTCookieAuthenticationMiddleware ) """The authentication middleware class to use. Must inherit from :class:`JWTCookieAuthenticationMiddleware` """ token_cls: type[Token] = Token """Target type the JWT payload will be converted into""" accepted_audiences: Sequence[str] | None = None """Audiences to accept when verifying the token. If given, and the audience in the token does not match, a 401 response is returned """ accepted_issuers: Sequence[str] | None = None """Issuers to accept when verifying the token. If given, and the issuer in the token does not match, a 401 response is returned """ require_claims: Sequence[str] | None = None """Require these claims to be present in the JWT payload. If any of those claims is missing, a 401 response is returned """ verify_expiry: bool = True """Verify that the value of the ``exp`` (*expiration*) claim is in the future""" verify_not_before: bool = True """Verify that the value of the ``nbf`` (*not before*) claim is in the past""" strict_audience: bool = False """Verify that the value of the ``aud`` (*audience*) claim is a single value, and not a list of values, and matches ``audience`` exactly. Requires that ``accepted_audiences`` is a sequence of length 1 """ @property def middleware(self) -> DefineMiddleware: """Create ``JWTCookieAuthenticationMiddleware`` wrapped in :class:`DefineMiddleware <.middleware.base.DefineMiddleware>`. Returns: An instance of :class:`DefineMiddleware <.middleware.base.DefineMiddleware>`. """ return DefineMiddleware( self.authentication_middleware_class, algorithm=self.algorithm, auth_cookie_key=self.key, auth_header=self.auth_header, exclude=self.exclude, exclude_opt_key=self.exclude_opt_key, exclude_http_methods=self.exclude_http_methods, retrieve_user_handler=self.retrieve_user_handler, revoked_token_handler=self.revoked_token_handler, scopes=self.scopes, token_secret=self.token_secret, token_cls=self.token_cls, token_issuer=self.accepted_issuers, token_audience=self.accepted_audiences, require_claims=self.require_claims, verify_expiry=self.verify_expiry, verify_not_before=self.verify_not_before, strict_audience=self.strict_audience, ) @property def oauth_flow(self) -> OAuthFlow: """Create an OpenAPI OAuth2 flow for the password bearer authentication scheme. Returns: An :class:`OAuthFlow ` instance. """ return OAuthFlow( token_url=self.token_url, scopes=self.oauth_scopes, ) @property def openapi_components(self) -> Components: """Create OpenAPI documentation for the OAUTH2 Password bearer auth scheme. Returns: An :class:`Components ` instance. """ return Components( security_schemes={ self.openapi_security_scheme_name: SecurityScheme( type="oauth2", scheme="Bearer", name=self.auth_header, security_scheme_in="header", flows=OAuthFlows(password=self.oauth_flow), # pyright: ignore[reportGeneralTypeIssues] bearer_format="JWT", description=self.description, ) } ) def login( self, identifier: str, *, response_body: Any = Empty, response_media_type: str | MediaType = MediaType.JSON, response_status_code: int = HTTP_201_CREATED, token_expiration: timedelta | None = None, token_issuer: str | None = None, token_audience: str | None = None, token_unique_jwt_id: str | None = None, token_extras: dict[str, Any] | None = None, send_token_as_response_body: bool = True, ) -> Response[Any]: """Create a response with a JWT header. Args: identifier: Unique identifier of the token subject. Usually this is a user ID or equivalent kind of value. response_body: An optional response body to send. response_media_type: An optional ``Content-Type``. Defaults to ``application/json``. response_status_code: An optional status code for the response. Defaults to ``201``. token_expiration: An optional timedelta for the token expiration. token_issuer: An optional value of the token ``iss`` field. token_audience: An optional value for the token ``aud`` field. token_unique_jwt_id: An optional value for the token ``jti`` field. token_extras: An optional dictionary to include in the token ``extras`` field. send_token_as_response_body: If ``True`` the response will be an oAuth2 token response dict. Note: if a response body is passed this setting will be ignored. Returns: A :class:`Response <.response.Response>` instance. """ encoded_token = self.create_token( identifier=identifier, token_expiration=token_expiration, token_issuer=token_issuer, token_audience=token_audience, token_unique_jwt_id=token_unique_jwt_id, token_extras=token_extras, ) expires_in = int((token_expiration or self.default_token_expiration).total_seconds()) cookie = Cookie( key=self.key, path=self.path, httponly=True, value=self.format_auth_header(encoded_token), max_age=expires_in, secure=self.secure, samesite=self.samesite, domain=self.domain, ) if response_body is not Empty: body = response_body elif send_token_as_response_body: token_dto = OAuth2Login( access_token=encoded_token, expires_in=expires_in, token_type="bearer", # noqa: S106 ) body = asdict(token_dto) else: body = None return self.create_response( content=body, headers={self.auth_header: self.format_auth_header(encoded_token)}, cookies=[cookie], media_type=response_media_type, status_code=response_status_code, ) litestar-2.16.0/litestar/security/jwt/middleware.py000066400000000000000000000277001500564371300224320ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Awaitable, Callable, Sequence from litestar.exceptions import NotAuthorizedException from litestar.middleware.authentication import ( AbstractAuthenticationMiddleware, AuthenticationResult, ) from litestar.security.jwt.token import Token __all__ = ("JWTAuthenticationMiddleware", "JWTCookieAuthenticationMiddleware") if TYPE_CHECKING: from typing import Any from litestar.connection import ASGIConnection from litestar.types import ASGIApp, Method, Scopes class JWTAuthenticationMiddleware(AbstractAuthenticationMiddleware): """JWT Authentication middleware. This class provides JWT authentication functionalities. """ __slots__ = ( "algorithm", "auth_header", "require_claims", "retrieve_user_handler", "revoked_token_handler", "strict_audience", "token_audience", "token_cls", "token_issuer", "token_secret", "verify_expiry", "verify_not_before", ) def __init__( self, algorithm: str, app: ASGIApp, auth_header: str, exclude: str | list[str] | None, exclude_http_methods: Sequence[Method] | None, exclude_opt_key: str, retrieve_user_handler: Callable[[Token, ASGIConnection[Any, Any, Any, Any]], Awaitable[Any]], scopes: Scopes, token_secret: str, token_cls: type[Token] = Token, token_audience: Sequence[str] | None = None, token_issuer: Sequence[str] | None = None, require_claims: Sequence[str] | None = None, verify_expiry: bool = True, verify_not_before: bool = True, strict_audience: bool = False, revoked_token_handler: Callable[[Token, ASGIConnection[Any, Any, Any, Any]], Awaitable[Any]] | None = None, ) -> None: """Check incoming requests for an encoded token in the auth header specified, and if present retrieve the user from persistence using the provided function. Args: algorithm: JWT hashing algorithm to use. app: An ASGIApp, this value is the next ASGI handler to call in the middleware stack. auth_header: Request header key from which to retrieve the token. E.g. ``Authorization`` or ``X-Api-Key``. exclude: A pattern or list of patterns to skip. exclude_opt_key: An identifier to use on routes to disable authentication for a particular route. exclude_http_methods: A sequence of http methods that do not require authentication. retrieve_user_handler: A function that receives a :class:`Token <.security.jwt.Token>` and returns a user, which can be any arbitrary value. scopes: ASGI scopes processed by the authentication middleware. token_secret: Secret for decoding the JWT. This value should be equivalent to the secret used to encode it. token_cls: Token class used when encoding / decoding JWTs token_audience: Verify the audience when decoding the token. If the audience in the token does not match any audience given, raise a :exc:`NotAuthorizedException` token_issuer: Verify the issuer when decoding the token. If the issuer in the token does not match any issuer given, raise a :exc:`NotAuthorizedException` require_claims: Require these claims to be present in the JWT payload verify_expiry: Verify that the value of the ``exp`` (*expiration*) claim is in the future verify_not_before: Verify that the value of the ``nbf`` (*not before*) claim is in the past strict_audience: Verify that the value of the ``aud`` (*audience*) claim is a single value, and not a list of values, and matches ``audience`` exactly. Requires that ``accepted_audiences`` is a sequence of length 1 revoked_token_handler: A function that receives a :class:`Token <.security.jwt.Token>` and returns a boolean indicating whether the token has been revoked. """ super().__init__( app=app, exclude=exclude, exclude_from_auth_key=exclude_opt_key, exclude_http_methods=exclude_http_methods, scopes=scopes, ) self.algorithm = algorithm self.auth_header = auth_header self.retrieve_user_handler = retrieve_user_handler self.revoked_token_handler = revoked_token_handler self.token_secret = token_secret self.token_cls = token_cls self.token_audience = token_audience self.token_issuer = token_issuer self.require_claims = require_claims self.verify_expiry = verify_expiry self.verify_not_before = verify_not_before self.strict_audience = strict_audience async def authenticate_request(self, connection: ASGIConnection[Any, Any, Any, Any]) -> AuthenticationResult: """Given an HTTP Connection, parse the JWT api key stored in the header and retrieve the user correlating to the token from the DB. Args: connection: An Litestar HTTPConnection instance. Returns: AuthenticationResult Raises: NotAuthorizedException: If token is invalid or user is not found. """ auth_header = connection.headers.get(self.auth_header) if not auth_header: raise NotAuthorizedException("No JWT token found in request header") encoded_token = auth_header.partition(" ")[-1] return await self.authenticate_token(encoded_token=encoded_token, connection=connection) async def authenticate_token( self, encoded_token: str, connection: ASGIConnection[Any, Any, Any, Any] ) -> AuthenticationResult: """Given an encoded JWT token, parse, validate and look up sub within token. Args: encoded_token: Encoded JWT token. connection: An ASGI connection instance. Raises: NotAuthorizedException: If token is invalid or user is not found. Returns: AuthenticationResult """ token = self.token_cls.decode( encoded_token=encoded_token, secret=self.token_secret, algorithm=self.algorithm, audience=self.token_audience, issuer=self.token_issuer, require_claims=self.require_claims, verify_exp=self.verify_expiry, verify_nbf=self.verify_not_before, strict_audience=self.strict_audience, ) user = await self.retrieve_user_handler(token, connection) token_revoked = False if self.revoked_token_handler: token_revoked = await self.revoked_token_handler(token, connection) if not user or token_revoked: raise NotAuthorizedException() return AuthenticationResult(user=user, auth=token) class JWTCookieAuthenticationMiddleware(JWTAuthenticationMiddleware): """Cookie based JWT authentication middleware.""" __slots__ = ("auth_cookie_key",) def __init__( self, algorithm: str, app: ASGIApp, auth_cookie_key: str, auth_header: str, exclude: str | list[str] | None, exclude_opt_key: str, exclude_http_methods: Sequence[Method] | None, retrieve_user_handler: Callable[[Token, ASGIConnection[Any, Any, Any, Any]], Awaitable[Any]], scopes: Scopes, token_secret: str, token_cls: type[Token] = Token, token_audience: Sequence[str] | None = None, token_issuer: Sequence[str] | None = None, require_claims: Sequence[str] | None = None, verify_expiry: bool = True, verify_not_before: bool = True, strict_audience: bool = False, revoked_token_handler: Callable[[Token, ASGIConnection[Any, Any, Any, Any]], Awaitable[Any]] | None = None, ) -> None: """Check incoming requests for an encoded token in the auth header or cookie name specified, and if present retrieves the user from persistence using the provided function. Args: algorithm: JWT hashing algorithm to use. app: An ASGIApp, this value is the next ASGI handler to call in the middleware stack. auth_cookie_key: Cookie name from which to retrieve the token. E.g. ``token`` or ``accessToken``. auth_header: Request header key from which to retrieve the token. E.g. ``Authorization`` or ``X-Api-Key``. exclude: A pattern or list of patterns to skip. exclude_opt_key: An identifier to use on routes to disable authentication for a particular route. exclude_http_methods: A sequence of http methods that do not require authentication. retrieve_user_handler: A function that receives a :class:`Token <.security.jwt.Token>` and returns a user, which can be any arbitrary value. scopes: ASGI scopes processed by the authentication middleware. token_secret: Secret for decoding the JWT. This value should be equivalent to the secret used to encode it. token_cls: Token class used when encoding / decoding JWTs token_audience: Verify the audience when decoding the token. If the audience in the token does not match any audience given, raise a :exc:`NotAuthorizedException` token_issuer: Verify the issuer when decoding the token. If the issuer in the token does not match any issuer given, raise a :exc:`NotAuthorizedException` require_claims: Require these claims to be present in the JWT payload verify_expiry: Verify that the value of the ``exp`` (*expiration*) claim is in the future verify_not_before: Verify that the value of the ``nbf`` (*not before*) claim is in the past strict_audience: Verify that the value of the ``aud`` (*audience*) claim is a single value, and not a list of values, and matches ``audience`` exactly. Requires that ``accepted_audiences`` is a sequence of length 1 revoked_token_handler: A function that receives a :class:`Token <.security.jwt.Token>` and returns a boolean indicating whether the token has been revoked. """ super().__init__( algorithm=algorithm, app=app, auth_header=auth_header, exclude=exclude, exclude_http_methods=exclude_http_methods, exclude_opt_key=exclude_opt_key, retrieve_user_handler=retrieve_user_handler, revoked_token_handler=revoked_token_handler, scopes=scopes, token_secret=token_secret, token_cls=token_cls, token_audience=token_audience, token_issuer=token_issuer, require_claims=require_claims, verify_expiry=verify_expiry, verify_not_before=verify_not_before, strict_audience=strict_audience, ) self.auth_cookie_key = auth_cookie_key async def authenticate_request(self, connection: ASGIConnection[Any, Any, Any, Any]) -> AuthenticationResult: """Given an HTTP Connection, parse the JWT api key stored in the header and retrieve the user correlating to the token from the DB. Args: connection: An Litestar HTTPConnection instance. Raises: NotAuthorizedException: If token is invalid or user is not found. Returns: AuthenticationResult """ auth_header = connection.headers.get(self.auth_header) or connection.cookies.get(self.auth_cookie_key) if not auth_header: raise NotAuthorizedException("No JWT token found in request header or cookies") encoded_token = auth_header.partition(" ")[-1] return await self.authenticate_token(encoded_token=encoded_token, connection=connection) litestar-2.16.0/litestar/security/jwt/token.py000066400000000000000000000173301500564371300214330ustar00rootroot00000000000000from __future__ import annotations import dataclasses from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, TypedDict import jwt import msgspec from litestar.exceptions import ImproperlyConfiguredException, NotAuthorizedException if TYPE_CHECKING: from typing_extensions import Self __all__ = ( "JWTDecodeOptions", "Token", ) def _normalize_datetime(value: datetime) -> datetime: """Convert the given value into UTC and strip microseconds. Args: value: A datetime instance Returns: A datetime instance """ if value.tzinfo is not None: value.astimezone(timezone.utc) return value.replace(microsecond=0) class JWTDecodeOptions(TypedDict, total=False): """``options`` for PyJWTs :func:`jwt.decode`""" verify_aud: bool verify_iss: bool verify_exp: bool verify_nbf: bool strict_aud: bool require: list[str] @dataclass class Token: """JWT Token DTO.""" exp: datetime """Expiration - datetime for token expiration.""" sub: str """Subject - usually a unique identifier of the user or equivalent entity.""" iat: datetime = field(default_factory=lambda: _normalize_datetime(datetime.now(timezone.utc))) """Issued at - should always be current now.""" iss: Optional[str] = field(default=None) # noqa: UP007 """Issuer - optional unique identifier for the issuer.""" aud: Optional[str] = field(default=None) # noqa: UP007 """Audience - intended audience.""" jti: Optional[str] = field(default=None) # noqa: UP007 """JWT ID - a unique identifier of the JWT between different issuers.""" extras: Dict[str, Any] = field(default_factory=dict) # noqa: UP006 """Extra fields that were found on the JWT token.""" def __post_init__(self) -> None: if len(self.sub) < 1: raise ImproperlyConfiguredException("sub must be a string with a length greater than 0") if isinstance(self.exp, datetime) and ( (exp := _normalize_datetime(self.exp)).timestamp() >= _normalize_datetime(datetime.now(timezone.utc)).timestamp() ): self.exp = exp else: raise ImproperlyConfiguredException("exp value must be a datetime in the future") if isinstance(self.iat, datetime) and ( (iat := _normalize_datetime(self.iat)).timestamp() <= _normalize_datetime(datetime.now(timezone.utc)).timestamp() ): self.iat = iat else: raise ImproperlyConfiguredException("iat must be a current or past time") @classmethod def decode_payload( cls, encoded_token: str, secret: str, algorithms: list[str], issuer: list[str] | None = None, audience: str | Sequence[str] | None = None, options: JWTDecodeOptions | None = None, ) -> Any: """Decode and verify the JWT and return its payload""" return jwt.decode( jwt=encoded_token, key=secret, algorithms=algorithms, issuer=issuer, audience=audience, options=options, # type: ignore[arg-type] ) @classmethod def decode( cls, encoded_token: str, secret: str, algorithm: str, audience: str | Sequence[str] | None = None, issuer: str | Sequence[str] | None = None, require_claims: Sequence[str] | None = None, verify_exp: bool = True, verify_nbf: bool = True, strict_audience: bool = False, ) -> Self: """Decode a passed in token string and return a Token instance. Args: encoded_token: A base64 string containing an encoded JWT. secret: The secret with which the JWT is encoded. algorithm: The algorithm used to encode the JWT. audience: Verify the audience when decoding the token. If the audience in the token does not match any audience given, raise a :exc:`NotAuthorizedException` issuer: Verify the issuer when decoding the token. If the issuer in the token does not match any issuer given, raise a :exc:`NotAuthorizedException` require_claims: Verify that the given claims are present in the token verify_exp: Verify that the value of the ``exp`` (*expiration*) claim is in the future verify_nbf: Verify that the value of the ``nbf`` (*not before*) claim is in the past strict_audience: Verify that the value of the ``aud`` (*audience*) claim is a single value, and not a list of values, and matches ``audience`` exactly. Requires the value passed to the ``audience`` to be a sequence of length 1 Returns: A decoded Token instance. Raises: NotAuthorizedException: If the token is invalid. """ options: JWTDecodeOptions = { "verify_aud": bool(audience), "verify_iss": bool(issuer), } if require_claims: options["require"] = list(require_claims) if verify_exp is False: options["verify_exp"] = False if verify_nbf is False: options["verify_nbf"] = False if strict_audience: if audience is None or (not isinstance(audience, str) and len(audience) != 1): raise ValueError("When using 'strict_audience=True', 'audience' must be a sequence of length 1") options["strict_aud"] = True # although not documented, pyjwt requires audience to be a string if # using the strict_aud option if not isinstance(audience, str): audience = audience[0] try: payload = cls.decode_payload( encoded_token=encoded_token, secret=secret, algorithms=[algorithm], audience=audience, issuer=list(issuer) if issuer else None, options=options, ) # msgspec can do these conversions as well, but to keep backwards # compatibility, we do it ourselves, since the datetime parsing works a # little bit different there payload["exp"] = datetime.fromtimestamp(payload["exp"], tz=timezone.utc) payload["iat"] = datetime.fromtimestamp(payload["iat"], tz=timezone.utc) extra_fields = payload.keys() - {f.name for f in dataclasses.fields(cls)} extras = payload.setdefault("extras", {}) for key in extra_fields: extras[key] = payload.pop(key) return msgspec.convert(payload, cls, strict=False) except ( KeyError, jwt.exceptions.InvalidTokenError, ImproperlyConfiguredException, msgspec.ValidationError, ) as e: raise NotAuthorizedException("Invalid token") from e def encode(self, secret: str, algorithm: str) -> str: """Encode the token instance into a string. Args: secret: The secret with which the JWT is encoded. algorithm: The algorithm used to encode the JWT. Returns: An encoded token string. Raises: ImproperlyConfiguredException: If encoding fails. """ try: return jwt.encode( payload={k: v for k, v in asdict(self).items() if v is not None}, key=secret, algorithm=algorithm, ) except (jwt.DecodeError, NotImplementedError) as e: raise ImproperlyConfiguredException("Failed to encode token") from e litestar-2.16.0/litestar/security/session_auth/000077500000000000000000000000001500564371300216355ustar00rootroot00000000000000litestar-2.16.0/litestar/security/session_auth/__init__.py000066400000000000000000000002741500564371300237510ustar00rootroot00000000000000from litestar.security.session_auth.auth import SessionAuth from litestar.security.session_auth.middleware import SessionAuthMiddleware __all__ = ("SessionAuth", "SessionAuthMiddleware") litestar-2.16.0/litestar/security/session_auth/auth.py000066400000000000000000000134301500564371300231510ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Iterable, Sequence, cast from litestar.middleware.base import DefineMiddleware from litestar.middleware.session.base import BaseBackendConfig, BaseSessionBackendT from litestar.openapi.spec import Components, SecurityRequirement, SecurityScheme from litestar.security.base import AbstractSecurityConfig, UserType from litestar.security.session_auth.middleware import MiddlewareWrapper, SessionAuthMiddleware __all__ = ("SessionAuth",) if TYPE_CHECKING: from litestar.connection import ASGIConnection from litestar.di import Provide from litestar.types import ControllerRouterHandler, Guard, Method, Scopes, SyncOrAsyncUnion, TypeEncodersMap @dataclass class SessionAuth(Generic[UserType, BaseSessionBackendT], AbstractSecurityConfig[UserType, Dict[str, Any]]): """Session Based Security Backend.""" session_backend_config: BaseBackendConfig[BaseSessionBackendT] # pyright: ignore """A session backend config.""" retrieve_user_handler: Callable[[Any, ASGIConnection], SyncOrAsyncUnion[Any | None]] """Callable that receives the ``auth`` value from the authentication middleware and returns a ``user`` value. Notes: - User and Auth can be any arbitrary values specified by the security backend. - The User and Auth values will be set by the middleware as ``scope["user"]`` and ``scope["auth"]`` respectively. Once provided, they can access via the ``connection.user`` and ``connection.auth`` properties. - The callable can be sync or async. If it is sync, it will be wrapped to support async. """ authentication_middleware_class: type[SessionAuthMiddleware] = field(default=SessionAuthMiddleware) # pyright: ignore """The authentication middleware class to use. Must inherit from :class:`SessionAuthMiddleware ` """ guards: Iterable[Guard] | None = field(default=None) """An iterable of guards to call for requests, providing authorization functionalities.""" exclude: str | list[str] | None = field(default=None) """A pattern or list of patterns to skip in the authentication middleware.""" exclude_opt_key: str = field(default="exclude_from_auth") """An identifier to use on routes to disable authentication and authorization checks for a particular route.""" exclude_http_methods: Sequence[Method] | None = field( default_factory=lambda: cast("Sequence[Method]", ["OPTIONS", "HEAD"]) ) """A sequence of http methods that do not require authentication. Defaults to ['OPTIONS', 'HEAD']""" scopes: Scopes | None = field(default=None) """ASGI scopes processed by the authentication middleware, if ``None``, both ``http`` and ``websocket`` will be processed.""" route_handlers: Iterable[ControllerRouterHandler] | None = field(default=None) """An optional iterable of route handlers to register.""" dependencies: dict[str, Provide] | None = field(default=None) """An optional dictionary of dependency providers.""" type_encoders: TypeEncodersMap | None = field(default=None) """A mapping of types to callables that transform them into types supported for serialization.""" @property def middleware(self) -> DefineMiddleware: """Use this property to insert the config into a middleware list on one of the application layers. Examples: .. code-block:: python from typing import Any from os import urandom from litestar import Litestar, Request, get from litestar_session import SessionAuth async def retrieve_user_from_session(session: dict[str, Any]) -> Any: # implement logic here to retrieve a ``user`` datum given the session dictionary ... session_auth_config = SessionAuth( secret=urandom(16), retrieve_user_handler=retrieve_user_from_session ) @get("/") def my_handler(request: Request) -> None: ... app = Litestar(route_handlers=[my_handler], middleware=[session_auth_config.middleware]) Returns: An instance of DefineMiddleware including ``self`` as the config kwarg value. """ return DefineMiddleware(MiddlewareWrapper, config=self) @property def session_backend(self) -> BaseSessionBackendT: """Create a session backend. Returns: A subclass of :class:`BaseSessionBackend ` """ return self.session_backend_config._backend_class(config=self.session_backend_config) # pyright: ignore @property def openapi_components(self) -> Components: """Create OpenAPI documentation for the Session Authentication schema used. Returns: An :class:`Components ` instance. """ return Components( security_schemes={ "sessionCookie": SecurityScheme( type="apiKey", name=self.session_backend_config.key, security_scheme_in="cookie", # pyright: ignore description="Session cookie authentication.", ) } ) @property def security_requirement(self) -> SecurityRequirement: """Return OpenAPI 3.1. :data:`SecurityRequirement <.openapi.spec.SecurityRequirement>` for the auth backend. Returns: An OpenAPI 3.1 :data:`SecurityRequirement <.openapi.spec.SecurityRequirement>` dictionary. """ return {"sessionCookie": []} litestar-2.16.0/litestar/security/session_auth/middleware.py000066400000000000000000000117371500564371300243350ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Awaitable, Callable, Sequence from litestar.exceptions import NotAuthorizedException from litestar.middleware._internal.exceptions import ExceptionHandlerMiddleware from litestar.middleware.authentication import ( AbstractAuthenticationMiddleware, AuthenticationResult, ) from litestar.types import Empty, Method, Scopes __all__ = ("MiddlewareWrapper", "SessionAuthMiddleware") if TYPE_CHECKING: from litestar.connection import ASGIConnection from litestar.security.session_auth.auth import SessionAuth from litestar.types import ASGIApp, Receive, Scope, Send class MiddlewareWrapper: """Wrapper class that serves as the middleware entry point.""" def __init__(self, app: ASGIApp, config: SessionAuth[Any, Any]) -> None: """Wrap the SessionAuthMiddleware inside ExceptionHandlerMiddleware, and it wraps this inside SessionMiddleware. This allows the auth middleware to raise exceptions and still have the response handled, while having the session cleared. Args: app: An ASGIApp, this value is the next ASGI handler to call in the middleware stack. config: An instance of SessionAuth. """ self.app = app self.config = config self.has_wrapped_middleware = False async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """Handle creating a middleware stack and calling it. Args: scope: The ASGI connection scope. receive: The ASGI receive function. send: The ASGI send function. Returns: None """ if not self.has_wrapped_middleware: auth_middleware = self.config.authentication_middleware_class( app=self.app, exclude=self.config.exclude, exclude_http_methods=self.config.exclude_http_methods, exclude_opt_key=self.config.exclude_opt_key, scopes=self.config.scopes, retrieve_user_handler=self.config.retrieve_user_handler, # type: ignore[arg-type] ) exception_middleware = ExceptionHandlerMiddleware(app=auth_middleware, debug=None) self.app = self.config.session_backend_config.middleware.middleware( app=exception_middleware, backend=self.config.session_backend, ) self.has_wrapped_middleware = True await self.app(scope, receive, send) class SessionAuthMiddleware(AbstractAuthenticationMiddleware): """Session Authentication Middleware.""" def __init__( self, app: ASGIApp, exclude: str | list[str] | None, exclude_http_methods: Sequence[Method] | None, exclude_opt_key: str, retrieve_user_handler: Callable[[dict[str, Any], ASGIConnection[Any, Any, Any, Any]], Awaitable[Any]], scopes: Scopes | None, ) -> None: """Session based authentication middleware. Args: app: An ASGIApp, this value is the next ASGI handler to call in the middleware stack. exclude: A pattern or list of patterns to skip in the authentication middleware. exclude_http_methods: A sequence of http methods that do not require authentication. exclude_opt_key: An identifier to use on routes to disable authentication and authorization checks for a particular route. scopes: ASGI scopes processed by the authentication middleware. retrieve_user_handler: Callable that receives the ``session`` value from the authentication middleware and returns a ``user`` value. """ super().__init__( app=app, exclude=exclude, exclude_from_auth_key=exclude_opt_key, exclude_http_methods=exclude_http_methods, scopes=scopes, ) self.retrieve_user_handler = retrieve_user_handler async def authenticate_request(self, connection: ASGIConnection[Any, Any, Any, Any]) -> AuthenticationResult: """Authenticate an incoming connection. Args: connection: An :class:`ASGIConnection <.connection.ASGIConnection>` instance. Raises: NotAuthorizedException: if session data is empty or user is not found. Returns: :class:`AuthenticationResult <.middleware.authentication.AuthenticationResult>` """ if not connection.session or connection.scope["session"] is Empty: # the assignment of 'Empty' forces the session middleware to clear session data. connection.scope["session"] = Empty raise NotAuthorizedException("no session data found") user = await self.retrieve_user_handler(connection.session, connection) if not user: connection.scope["session"] = Empty raise NotAuthorizedException("no user correlating to session found") return AuthenticationResult(user=user, auth=connection.session) litestar-2.16.0/litestar/serialization/000077500000000000000000000000001500564371300201375ustar00rootroot00000000000000litestar-2.16.0/litestar/serialization/__init__.py000066400000000000000000000005341500564371300222520ustar00rootroot00000000000000from .msgspec_hooks import ( decode_json, decode_msgpack, default_deserializer, default_serializer, encode_json, encode_msgpack, get_serializer, ) __all__ = ( "decode_json", "decode_msgpack", "default_deserializer", "default_serializer", "encode_json", "encode_msgpack", "get_serializer", ) litestar-2.16.0/litestar/serialization/msgspec_hooks.py000066400000000000000000000224511500564371300233610ustar00rootroot00000000000000from __future__ import annotations from collections import deque from datetime import date, datetime, time from decimal import Decimal from functools import partial from ipaddress import ( IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network, ) from pathlib import Path, PurePath from re import Pattern from typing import TYPE_CHECKING, Any, Callable, Mapping, TypeVar, overload from uuid import UUID import msgspec from litestar.datastructures.secret_values import SecretBytes, SecretString from litestar.exceptions import SerializationException from litestar.types import Empty, EmptyType, Serializer, TypeDecodersSequence from litestar.utils.typing import get_origin_or_inner_type if TYPE_CHECKING: from litestar.types import TypeEncodersMap __all__ = ( "decode_json", "decode_msgpack", "default_deserializer", "default_serializer", "encode_json", "encode_msgpack", "get_serializer", ) T = TypeVar("T") DEFAULT_TYPE_ENCODERS: TypeEncodersMap = { Path: str, PurePath: str, IPv4Address: str, IPv4Interface: str, IPv4Network: str, IPv6Address: str, IPv6Interface: str, IPv6Network: str, datetime: lambda val: val.isoformat(), date: lambda val: val.isoformat(), time: lambda val: val.isoformat(), deque: list, Decimal: lambda val: int(val) if val.as_tuple().exponent >= 0 else float(val), Pattern: lambda val: val.pattern, SecretBytes: lambda val: val.get_obscured().decode("utf-8"), SecretString: lambda val: val.get_obscured(), # support subclasses of stdlib types, If no previous type matched, these will be # the last type in the mro, so we use this to (attempt to) convert a subclass into # its base class. # see https://github.com/jcrist/msgspec/issues/248 # and https://github.com/litestar-org/litestar/issues/1003 str: str, int: int, float: float, set: set, frozenset: frozenset, bytes: bytes, } def default_serializer(value: Any, type_encoders: Mapping[Any, Callable[[Any], Any]] | None = None) -> Any: """Transform values non-natively supported by ``msgspec`` Args: value: A value to serialized type_encoders: Mapping of types to callables to transforming types Returns: A serialized value Raises: TypeError: if value is not supported """ type_encoders = {**DEFAULT_TYPE_ENCODERS, **(type_encoders or {})} for base in value.__class__.__mro__[:-1]: try: encoder = type_encoders[base] return encoder(value) except KeyError: continue raise TypeError(f"Unsupported type: {type(value)!r}") def default_deserializer( target_type: Any, value: Any, type_decoders: TypeDecodersSequence | None = None ) -> Any: # pragma: no cover """Transform values non-natively supported by ``msgspec`` Args: target_type: Encountered type value: Value to coerce type_decoders: Optional sequence of type decoders Returns: A ``msgspec``-supported type """ from litestar.datastructures.state import ImmutableState try: if isinstance(value, target_type): return value except TypeError as exc: # we might get a TypeError here if target_type is a subscribed generic. For # performance reasons, we let this happen and only unwrap this when we're # certain this might be the case if (origin := get_origin_or_inner_type(target_type)) is not None: target_type = origin if isinstance(value, target_type): return value else: raise exc if type_decoders: for predicate, decoder in type_decoders: if predicate(target_type): return decoder(target_type, value) if issubclass(target_type, (Path, PurePath, ImmutableState, UUID)): return target_type(value) if issubclass(target_type, SecretBytes) and isinstance(value, (bytes, str)): return SecretBytes(value.encode("utf-8") if isinstance(value, str) else value) if issubclass(target_type, SecretString) and isinstance(value, str): return SecretString(value) raise TypeError(f"Unsupported type: {type(value)!r}") _msgspec_json_encoder = msgspec.json.Encoder(enc_hook=default_serializer) _msgspec_json_decoder = msgspec.json.Decoder(dec_hook=default_deserializer) _msgspec_msgpack_encoder = msgspec.msgpack.Encoder(enc_hook=default_serializer) _msgspec_msgpack_decoder = msgspec.msgpack.Decoder(dec_hook=default_deserializer) def encode_json(value: Any, serializer: Callable[[Any], Any] | None = None) -> bytes: """Encode a value into JSON. Args: value: Value to encode serializer: Optional callable to support non-natively supported types. Returns: JSON as bytes Raises: SerializationException: If error encoding ``obj``. """ try: return msgspec.json.encode(value, enc_hook=serializer) if serializer else _msgspec_json_encoder.encode(value) except (TypeError, msgspec.EncodeError) as msgspec_error: raise SerializationException(str(msgspec_error)) from msgspec_error @overload def decode_json(value: str | bytes, strict: bool = ...) -> Any: ... @overload def decode_json(value: str | bytes, type_decoders: TypeDecodersSequence | None, strict: bool = ...) -> Any: ... @overload def decode_json(value: str | bytes, target_type: type[T], strict: bool = ...) -> T: ... @overload def decode_json( value: str | bytes, target_type: type[T], type_decoders: TypeDecodersSequence | None, strict: bool = ... ) -> T: ... def decode_json( # type: ignore[misc] value: str | bytes, target_type: type[T] | EmptyType = Empty, # pyright: ignore type_decoders: TypeDecodersSequence | None = None, strict: bool = True, ) -> Any: """Decode a JSON string/bytes into an object. Args: value: Value to decode target_type: An optional type to decode the data into type_decoders: Optional sequence of type decoders strict: Whether type coercion rules should be strict. Setting to False enables a wider set of coercion rules from string to non-string types for all values Returns: An object Raises: SerializationException: If error decoding ``value``. """ try: if target_type is Empty: return _msgspec_json_decoder.decode(value) return msgspec.json.decode( value, dec_hook=partial( default_deserializer, type_decoders=type_decoders, ), type=target_type, strict=strict, ) except msgspec.DecodeError as msgspec_error: raise SerializationException(str(msgspec_error)) from msgspec_error def encode_msgpack(value: Any, serializer: Callable[[Any], Any] | None = default_serializer) -> bytes: """Encode a value into MessagePack. Args: value: Value to encode serializer: Optional callable to support non-natively supported types Returns: MessagePack as bytes Raises: SerializationException: If error encoding ``obj``. """ try: if serializer is None or serializer is default_serializer: return _msgspec_msgpack_encoder.encode(value) return msgspec.msgpack.encode(value, enc_hook=serializer) except (TypeError, msgspec.EncodeError) as msgspec_error: raise SerializationException(str(msgspec_error)) from msgspec_error @overload def decode_msgpack(value: bytes, strict: bool = ...) -> Any: ... @overload def decode_msgpack(value: bytes, type_decoders: TypeDecodersSequence | None, strict: bool = ...) -> Any: ... @overload def decode_msgpack(value: bytes, target_type: type[T], strict: bool = ...) -> T: ... @overload def decode_msgpack( value: bytes, target_type: type[T], type_decoders: TypeDecodersSequence | None, strict: bool = ... ) -> T: ... def decode_msgpack( # type: ignore[misc] value: bytes, target_type: type[T] | EmptyType = Empty, # pyright: ignore[reportInvalidTypeVarUse] type_decoders: TypeDecodersSequence | None = None, strict: bool = True, ) -> Any: """Decode a MessagePack string/bytes into an object. Args: value: Value to decode target_type: An optional type to decode the data into type_decoders: Optional sequence of type decoders strict: Whether type coercion rules should be strict. Setting to False enables a wider set of coercion rules from string to non-string types for all values Returns: An object Raises: SerializationException: If error decoding ``value``. """ try: if target_type is Empty: return _msgspec_msgpack_decoder.decode(value) return msgspec.msgpack.decode( value, dec_hook=partial(default_deserializer, type_decoders=type_decoders), type=target_type, strict=strict, ) except msgspec.DecodeError as msgspec_error: raise SerializationException(str(msgspec_error)) from msgspec_error def get_serializer(type_encoders: TypeEncodersMap | None = None) -> Serializer: """Get the serializer for the given type encoders.""" if type_encoders: return partial(default_serializer, type_encoders=type_encoders) return default_serializer litestar-2.16.0/litestar/static_files/000077500000000000000000000000001500564371300177335ustar00rootroot00000000000000litestar-2.16.0/litestar/static_files/__init__.py000066400000000000000000000003301500564371300220400ustar00rootroot00000000000000from litestar.static_files.base import StaticFiles from litestar.static_files.config import StaticFilesConfig, create_static_files_router __all__ = ("StaticFiles", "StaticFilesConfig", "create_static_files_router") litestar-2.16.0/litestar/static_files/base.py000066400000000000000000000137711500564371300212300ustar00rootroot00000000000000# ruff: noqa: PTH118 from __future__ import annotations import os.path from pathlib import Path from typing import TYPE_CHECKING, Literal, Sequence from litestar.enums import ScopeType from litestar.exceptions import MethodNotAllowedException, NotFoundException from litestar.file_system import FileSystemAdapter from litestar.response.file import ASGIFileResponse from litestar.status_codes import HTTP_404_NOT_FOUND __all__ = ("StaticFiles",) if TYPE_CHECKING: from litestar.types import Receive, Scope, Send from litestar.types.composite_types import PathType from litestar.types.file_types import FileInfo, FileSystemProtocol class StaticFiles: """ASGI App that handles file sending.""" __slots__ = ("adapter", "directories", "headers", "is_html_mode", "send_as_attachment") def __init__( self, is_html_mode: bool, directories: Sequence[PathType], file_system: FileSystemProtocol, send_as_attachment: bool = False, resolve_symlinks: bool = True, headers: dict[str, str] | None = None, ) -> None: """Initialize the Application. Args: is_html_mode: Flag dictating whether serving html. If true, the default file will be ``index.html``. directories: A list of directories to serve files from. file_system: The file_system spec to use for serving files. send_as_attachment: Whether to send the file with a ``content-disposition`` header of ``attachment`` or ``inline`` resolve_symlinks: Resolve symlinks to the directories headers: Headers that will be sent with every response. """ self.adapter = FileSystemAdapter(file_system) self.directories = tuple( os.path.normpath(Path(p).resolve() if resolve_symlinks else Path(p)) for p in directories ) self.is_html_mode = is_html_mode self.send_as_attachment = send_as_attachment self.headers = headers async def get_fs_info( self, directories: Sequence[PathType], file_path: PathType ) -> tuple[Path, FileInfo] | tuple[None, None]: """Return the resolved path and a :class:`stat_result `. .. versionchanged:: 2.8.3 Prevent `CVE-2024-32982 `_ by ensuring that the resolved path is within the configured directory as part of `advisory GHSA-83pv-qr33-2vcf `_. Args: directories: A list of directory paths. file_path: A file path to resolve Returns: A tuple with an optional resolved :class:`Path ` instance and an optional :class:`stat_result `. """ for directory in directories: try: joined_path = Path(directory, file_path) normalized_file_path = os.path.normpath(joined_path) if os.path.commonpath([directory, normalized_file_path]) == str(directory) and ( file_info := await self.adapter.info(joined_path) ): return joined_path, file_info except FileNotFoundError: continue return None, None async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: """ASGI callable. Args: scope: ASGI scope receive: ASGI ``receive`` callable send: ASGI ``send`` callable Returns: None """ if scope["type"] != ScopeType.HTTP or scope["method"] not in {"GET", "HEAD"}: raise MethodNotAllowedException() res = await self.handle(path=scope["path"], is_head_response=scope["method"] == "HEAD") await res(scope=scope, receive=receive, send=send) async def handle(self, path: str, is_head_response: bool) -> ASGIFileResponse: split_path = path.split("/") filename = split_path[-1] joined_path = Path(*split_path) resolved_path, fs_info = await self.get_fs_info(directories=self.directories, file_path=joined_path) content_disposition_type: Literal["inline", "attachment"] = ( "attachment" if self.send_as_attachment else "inline" ) if self.is_html_mode and fs_info and fs_info["type"] == "directory": filename = "index.html" resolved_path, fs_info = await self.get_fs_info( directories=self.directories, file_path=Path(resolved_path or joined_path) / filename, ) if fs_info and fs_info["type"] == "file": return ASGIFileResponse( file_path=resolved_path or joined_path, file_info=fs_info, file_system=self.adapter.file_system, filename=filename, content_disposition_type=content_disposition_type, is_head_response=is_head_response, headers=self.headers, ) if self.is_html_mode: # for some reason coverage doesn't catch these two lines filename = "404.html" # pragma: no cover resolved_path, fs_info = await self.get_fs_info( # pragma: no cover directories=self.directories, file_path=filename ) if fs_info and fs_info["type"] == "file": return ASGIFileResponse( file_path=resolved_path or joined_path, file_info=fs_info, file_system=self.adapter.file_system, filename=filename, status_code=HTTP_404_NOT_FOUND, content_disposition_type=content_disposition_type, is_head_response=is_head_response, headers=self.headers, ) raise NotFoundException( f"no file or directory match the path {resolved_path or joined_path} was found" ) # pragma: no cover litestar-2.16.0/litestar/static_files/config.py000066400000000000000000000206671500564371300215650ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from pathlib import PurePath # noqa: TC003 from typing import TYPE_CHECKING, Any, Sequence from litestar.exceptions import ImproperlyConfiguredException from litestar.file_system import BaseLocalFileSystem from litestar.handlers import asgi, get, head from litestar.response.file import ASGIFileResponse # noqa: TC001 from litestar.router import Router from litestar.static_files.base import StaticFiles from litestar.utils import normalize_path, warn_deprecation __all__ = ("StaticFilesConfig",) if TYPE_CHECKING: from litestar.datastructures import CacheControlHeader from litestar.handlers.asgi_handlers import ASGIRouteHandler from litestar.openapi.spec import SecurityRequirement from litestar.types import ( AfterRequestHookHandler, AfterResponseHookHandler, BeforeRequestHookHandler, EmptyType, ExceptionHandlersMap, Guard, Middleware, PathType, ) @dataclass class StaticFilesConfig: """Configuration for static file service. To enable static files, pass an instance of this class to the :class:`Litestar ` constructor using the 'static_files_config' key. """ path: str """Path to serve static files from. Note that the path cannot contain path parameters. """ directories: list[PathType] """A list of directories to serve files from.""" html_mode: bool = False """Flag dictating whether serving html. If true, the default file will be 'index.html'. """ name: str | None = None """An optional string identifying the static files handler.""" file_system: Any = BaseLocalFileSystem() # noqa: RUF009 """The file_system spec to use for serving files. Notes: - A file_system is a class that adheres to the :class:`FileSystemProtocol `. - You can use any of the file systems exported from the [fsspec](https://filesystem-spec.readthedocs.io/en/latest/) library for this purpose. """ opt: dict[str, Any] | None = None """A string key dictionary of arbitrary values that will be added to the static files handler.""" guards: list[Guard] | None = None """A list of :class:`Guard ` callables.""" exception_handlers: ExceptionHandlersMap | None = None """A dictionary that maps handler functions to status codes and/or exception types.""" send_as_attachment: bool = False """Whether to send the file as an attachment.""" def __post_init__(self) -> None: _validate_config(path=self.path, directories=self.directories, file_system=self.file_system) self.path = normalize_path(self.path) warn_deprecation( "2.6.0", kind="class", deprecated_name="StaticFilesConfig", removal_in="3.0", alternative="create_static_files_router", info='Replace static_files_config=[StaticFilesConfig(path="/static", directories=["assets"])] with ' 'route_handlers=[..., create_static_files_router(path="/static", directories=["assets"])]', ) def to_static_files_app(self) -> ASGIRouteHandler: """Return an ASGI app serving static files based on the config. Returns: :class:`StaticFiles ` """ static_files = StaticFiles( is_html_mode=self.html_mode, directories=self.directories, file_system=self.file_system, send_as_attachment=self.send_as_attachment, ) return asgi( path=self.path, name=self.name, is_static=True, opt=self.opt, guards=self.guards, exception_handlers=self.exception_handlers, )(static_files) def create_static_files_router( path: str, directories: list[PathType], file_system: Any = None, send_as_attachment: bool = False, html_mode: bool = False, name: str = "static", after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, before_request: BeforeRequestHookHandler | None = None, cache_control: CacheControlHeader | None = None, exception_handlers: ExceptionHandlersMap | None = None, guards: list[Guard] | None = None, include_in_schema: bool | EmptyType = False, middleware: Sequence[Middleware] | None = None, opt: dict[str, Any] | None = None, security: Sequence[SecurityRequirement] | None = None, tags: Sequence[str] | None = None, router_class: type[Router] = Router, resolve_symlinks: bool = True, ) -> Router: """Create a router with handlers to serve static files. Args: path: Path to serve static files under directories: Directories to serve static files from file_system: A *file system* implementing :class:`~litestar.types.FileSystemProtocol`. `fsspec `_ can be passed here as well send_as_attachment: Whether to send the file as an attachment html_mode: When in HTML: - Serve an ``index.html`` file from ``/`` - Serve ``404.html`` when a file could not be found name: Name to pass to the generated handlers after_request: ``after_request`` handlers passed to the router after_response: ``after_response`` handlers passed to the router before_request: ``before_request`` handlers passed to the router cache_control: ``cache_control`` passed to the router exception_handlers: Exception handlers passed to the router guards: Guards passed to the router include_in_schema: Include the routes / router in the OpenAPI schema middleware: Middlewares passed to the router opt: Opts passed to the router security: Security options passed to the router tags: ``tags`` passed to the router router_class: The class used to construct a router from resolve_symlinks: Resolve symlinks of ``directories`` """ if file_system is None: file_system = BaseLocalFileSystem() _validate_config(path=path, directories=directories, file_system=file_system) path = normalize_path(path) headers = None if cache_control: headers = {cache_control.HEADER_NAME: cache_control.to_header()} static_files = StaticFiles( is_html_mode=html_mode, directories=directories, file_system=file_system, send_as_attachment=send_as_attachment, resolve_symlinks=resolve_symlinks, headers=headers, ) @get("{file_path:path}", name=name) async def get_handler(file_path: PurePath) -> ASGIFileResponse: return await static_files.handle(path=file_path.as_posix(), is_head_response=False) @head("/{file_path:path}", name=f"{name}/head") async def head_handler(file_path: PurePath) -> ASGIFileResponse: return await static_files.handle(path=file_path.as_posix(), is_head_response=True) handlers = [get_handler, head_handler] if html_mode: @get("/", name=f"{name}/index") async def index_handler() -> ASGIFileResponse: return await static_files.handle(path="/", is_head_response=False) handlers.append(index_handler) return router_class( after_request=after_request, after_response=after_response, before_request=before_request, cache_control=cache_control, exception_handlers=exception_handlers, guards=guards, include_in_schema=include_in_schema, middleware=middleware, opt=opt, path=path, route_handlers=handlers, security=security, tags=tags, ) def _validate_config(path: str, directories: list[PathType], file_system: Any) -> None: if not path: raise ImproperlyConfiguredException("path must be a non-zero length string,") if not directories or not any(bool(d) for d in directories): raise ImproperlyConfiguredException("directories must include at least one path.") if "{" in path: raise ImproperlyConfiguredException("path parameters are not supported for static files") if not (callable(getattr(file_system, "info", None)) and callable(getattr(file_system, "open", None))): raise ImproperlyConfiguredException("file_system must adhere to the FileSystemProtocol type") litestar-2.16.0/litestar/status_codes.py000066400000000000000000000214151500564371300203370ustar00rootroot00000000000000from typing import Final # HTTP Status Codes HTTP_100_CONTINUE: Final = 100 """HTTP status code 'Continue'""" HTTP_101_SWITCHING_PROTOCOLS: Final = 101 """HTTP status code 'Switching Protocols'""" HTTP_102_PROCESSING: Final = 102 """HTTP status code 'Processing'""" HTTP_103_EARLY_HINTS: Final = 103 """HTTP status code 'Early Hints'""" HTTP_200_OK: Final = 200 """HTTP status code 'OK'""" HTTP_201_CREATED: Final = 201 """HTTP status code 'Created'""" HTTP_202_ACCEPTED: Final = 202 """HTTP status code 'Accepted'""" HTTP_203_NON_AUTHORITATIVE_INFORMATION: Final = 203 """HTTP status code 'Non Authoritative Information'""" HTTP_204_NO_CONTENT: Final = 204 """HTTP status code 'No Content'""" HTTP_205_RESET_CONTENT: Final = 205 """HTTP status code 'Reset Content'""" HTTP_206_PARTIAL_CONTENT: Final = 206 """HTTP status code 'Partial Content'""" HTTP_207_MULTI_STATUS: Final = 207 """HTTP status code 'Multi Status'""" HTTP_208_ALREADY_REPORTED: Final = 208 """HTTP status code 'Already Reported'""" HTTP_226_IM_USED: Final = 226 """HTTP status code 'I'm Used'""" HTTP_300_MULTIPLE_CHOICES: Final = 300 """HTTP status code 'Multiple Choices'""" HTTP_301_MOVED_PERMANENTLY: Final = 301 """HTTP status code 'Moved Permanently'""" HTTP_302_FOUND: Final = 302 """HTTP status code 'Found'""" HTTP_303_SEE_OTHER: Final = 303 """HTTP status code 'See Other'""" HTTP_304_NOT_MODIFIED: Final = 304 """HTTP status code 'Not Modified'""" HTTP_305_USE_PROXY: Final = 305 """HTTP status code 'Use Proxy'""" HTTP_306_RESERVED: Final = 306 """HTTP status code 'Reserved'""" HTTP_307_TEMPORARY_REDIRECT: Final = 307 """HTTP status code 'Temporary Redirect'""" HTTP_308_PERMANENT_REDIRECT: Final = 308 """HTTP status code 'Permanent Redirect'""" HTTP_400_BAD_REQUEST: Final = 400 """HTTP status code 'Bad Request'""" HTTP_401_UNAUTHORIZED: Final = 401 """HTTP status code 'Unauthorized'""" HTTP_402_PAYMENT_REQUIRED: Final = 402 """HTTP status code 'Payment Required'""" HTTP_403_FORBIDDEN: Final = 403 """HTTP status code 'Forbidden'""" HTTP_404_NOT_FOUND: Final = 404 """HTTP status code 'Not Found'""" HTTP_405_METHOD_NOT_ALLOWED: Final = 405 """HTTP status code 'Method Not Allowed'""" HTTP_406_NOT_ACCEPTABLE: Final = 406 """HTTP status code 'Not Acceptable'""" HTTP_407_PROXY_AUTHENTICATION_REQUIRED: Final = 407 """HTTP status code 'Proxy Authentication Required'""" HTTP_408_REQUEST_TIMEOUT: Final = 408 """HTTP status code 'Request Timeout'""" HTTP_409_CONFLICT: Final = 409 """HTTP status code 'Conflict'""" HTTP_410_GONE: Final = 410 """HTTP status code 'Gone'""" HTTP_411_LENGTH_REQUIRED: Final = 411 """HTTP status code 'Length Required'""" HTTP_412_PRECONDITION_FAILED: Final = 412 """HTTP status code 'Precondition Failed'""" HTTP_413_REQUEST_ENTITY_TOO_LARGE: Final = 413 """HTTP status code 'Request Entity Too Large'""" HTTP_414_REQUEST_URI_TOO_LONG: Final = 414 """HTTP status code 'Request URI Too Long'""" HTTP_415_UNSUPPORTED_MEDIA_TYPE: Final = 415 """HTTP status code 'Unsupported Media Type'""" HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE: Final = 416 """HTTP status code 'Requested Range Not Satisfiable'""" HTTP_417_EXPECTATION_FAILED: Final = 417 """HTTP status code 'Expectation Failed'""" HTTP_418_IM_A_TEAPOT: Final = 418 """HTTP status code 'I'm A Teapot'""" HTTP_421_MISDIRECTED_REQUEST: Final = 421 """HTTP status code 'Misdirected Request'""" HTTP_422_UNPROCESSABLE_ENTITY: Final = 422 """HTTP status code 'Unprocessable Entity'""" HTTP_423_LOCKED: Final = 423 """HTTP status code 'Locked'""" HTTP_424_FAILED_DEPENDENCY: Final = 424 """HTTP status code 'Failed Dependency'""" HTTP_425_TOO_EARLY: Final = 425 """HTTP status code 'Too Early'""" HTTP_426_UPGRADE_REQUIRED: Final = 426 """HTTP status code 'Upgrade Required'""" HTTP_428_PRECONDITION_REQUIRED: Final = 428 """HTTP status code 'Precondition Required'""" HTTP_429_TOO_MANY_REQUESTS: Final = 429 """HTTP status code 'Too Many Requests'""" HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE: Final = 431 """HTTP status code 'Request Header Fields Too Large'""" HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS: Final = 451 """HTTP status code 'Unavailable For Legal Reasons'""" HTTP_500_INTERNAL_SERVER_ERROR: Final = 500 """HTTP status code 'Internal Server Error'""" HTTP_501_NOT_IMPLEMENTED: Final = 501 """HTTP status code 'Not Implemented'""" HTTP_502_BAD_GATEWAY: Final = 502 """HTTP status code 'Bad Gateway'""" HTTP_503_SERVICE_UNAVAILABLE: Final = 503 """HTTP status code 'Service Unavailable'""" HTTP_504_GATEWAY_TIMEOUT: Final = 504 """HTTP status code 'Gateway Timeout'""" HTTP_505_HTTP_VERSION_NOT_SUPPORTED: Final = 505 """HTTP status code 'Http Version Not Supported'""" HTTP_506_VARIANT_ALSO_NEGOTIATES: Final = 506 """HTTP status code 'Variant Also Negotiates'""" HTTP_507_INSUFFICIENT_STORAGE: Final = 507 """HTTP status code 'Insufficient Storage'""" HTTP_508_LOOP_DETECTED: Final = 508 """HTTP status code 'Loop Detected'""" HTTP_510_NOT_EXTENDED: Final = 510 """HTTP status code 'Not Extended'""" HTTP_511_NETWORK_AUTHENTICATION_REQUIRED: Final = 511 """HTTP status code 'Network Authentication Required'""" # Websocket Codes WS_1000_NORMAL_CLOSURE: Final = 1000 """WebSocket status code 'Normal Closure'""" WS_1001_GOING_AWAY: Final = 1001 """WebSocket status code 'Going Away'""" WS_1002_PROTOCOL_ERROR: Final = 1002 """WebSocket status code 'Protocol Error'""" WS_1003_UNSUPPORTED_DATA: Final = 1003 """WebSocket status code 'Unsupported Data'""" WS_1005_NO_STATUS_RECEIVED: Final = 1005 """WebSocket status code 'No Status Received'""" WS_1006_ABNORMAL_CLOSURE: Final = 1006 """WebSocket status code 'Abnormal Closure'""" WS_1007_INVALID_FRAME_PAYLOAD_DATA: Final = 1007 """WebSocket status code 'Invalid Frame Payload Data'""" WS_1008_POLICY_VIOLATION: Final = 1008 """WebSocket status code 'Policy Violation'""" WS_1009_MESSAGE_TOO_BIG: Final = 1009 """WebSocket status code 'Message Too Big'""" WS_1010_MANDATORY_EXT: Final = 1010 """WebSocket status code 'Mandatory Ext.'""" WS_1011_INTERNAL_ERROR: Final = 1011 """WebSocket status code 'Internal Error'""" WS_1012_SERVICE_RESTART: Final = 1012 """WebSocket status code 'Service Restart'""" WS_1013_TRY_AGAIN_LATER: Final = 1013 """WebSocket status code 'Try Again Later'""" WS_1014_BAD_GATEWAY: Final = 1014 """WebSocket status code 'Bad Gateway'""" WS_1015_TLS_HANDSHAKE: Final = 1015 """WebSocket status code 'TLS Handshake'""" __all__ = ( "HTTP_100_CONTINUE", "HTTP_101_SWITCHING_PROTOCOLS", "HTTP_102_PROCESSING", "HTTP_103_EARLY_HINTS", "HTTP_200_OK", "HTTP_201_CREATED", "HTTP_202_ACCEPTED", "HTTP_203_NON_AUTHORITATIVE_INFORMATION", "HTTP_204_NO_CONTENT", "HTTP_205_RESET_CONTENT", "HTTP_206_PARTIAL_CONTENT", "HTTP_207_MULTI_STATUS", "HTTP_208_ALREADY_REPORTED", "HTTP_226_IM_USED", "HTTP_300_MULTIPLE_CHOICES", "HTTP_301_MOVED_PERMANENTLY", "HTTP_302_FOUND", "HTTP_303_SEE_OTHER", "HTTP_304_NOT_MODIFIED", "HTTP_305_USE_PROXY", "HTTP_306_RESERVED", "HTTP_307_TEMPORARY_REDIRECT", "HTTP_308_PERMANENT_REDIRECT", "HTTP_400_BAD_REQUEST", "HTTP_401_UNAUTHORIZED", "HTTP_402_PAYMENT_REQUIRED", "HTTP_403_FORBIDDEN", "HTTP_404_NOT_FOUND", "HTTP_405_METHOD_NOT_ALLOWED", "HTTP_406_NOT_ACCEPTABLE", "HTTP_407_PROXY_AUTHENTICATION_REQUIRED", "HTTP_408_REQUEST_TIMEOUT", "HTTP_409_CONFLICT", "HTTP_410_GONE", "HTTP_411_LENGTH_REQUIRED", "HTTP_412_PRECONDITION_FAILED", "HTTP_413_REQUEST_ENTITY_TOO_LARGE", "HTTP_414_REQUEST_URI_TOO_LONG", "HTTP_415_UNSUPPORTED_MEDIA_TYPE", "HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE", "HTTP_417_EXPECTATION_FAILED", "HTTP_418_IM_A_TEAPOT", "HTTP_421_MISDIRECTED_REQUEST", "HTTP_422_UNPROCESSABLE_ENTITY", "HTTP_423_LOCKED", "HTTP_424_FAILED_DEPENDENCY", "HTTP_425_TOO_EARLY", "HTTP_426_UPGRADE_REQUIRED", "HTTP_428_PRECONDITION_REQUIRED", "HTTP_429_TOO_MANY_REQUESTS", "HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE", "HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS", "HTTP_500_INTERNAL_SERVER_ERROR", "HTTP_501_NOT_IMPLEMENTED", "HTTP_502_BAD_GATEWAY", "HTTP_503_SERVICE_UNAVAILABLE", "HTTP_504_GATEWAY_TIMEOUT", "HTTP_505_HTTP_VERSION_NOT_SUPPORTED", "HTTP_506_VARIANT_ALSO_NEGOTIATES", "HTTP_507_INSUFFICIENT_STORAGE", "HTTP_508_LOOP_DETECTED", "HTTP_510_NOT_EXTENDED", "HTTP_511_NETWORK_AUTHENTICATION_REQUIRED", "WS_1000_NORMAL_CLOSURE", "WS_1001_GOING_AWAY", "WS_1002_PROTOCOL_ERROR", "WS_1003_UNSUPPORTED_DATA", "WS_1005_NO_STATUS_RECEIVED", "WS_1006_ABNORMAL_CLOSURE", "WS_1007_INVALID_FRAME_PAYLOAD_DATA", "WS_1008_POLICY_VIOLATION", "WS_1009_MESSAGE_TOO_BIG", "WS_1010_MANDATORY_EXT", "WS_1011_INTERNAL_ERROR", "WS_1012_SERVICE_RESTART", "WS_1013_TRY_AGAIN_LATER", "WS_1014_BAD_GATEWAY", "WS_1015_TLS_HANDSHAKE", ) litestar-2.16.0/litestar/stores/000077500000000000000000000000001500564371300166015ustar00rootroot00000000000000litestar-2.16.0/litestar/stores/__init__.py000066400000000000000000000000001500564371300207000ustar00rootroot00000000000000litestar-2.16.0/litestar/stores/base.py000066400000000000000000000112021500564371300200610ustar00rootroot00000000000000from __future__ import annotations from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Optional from msgspec import Struct from msgspec.msgpack import decode as msgpack_decode from msgspec.msgpack import encode as msgpack_encode if TYPE_CHECKING: from types import TracebackType from typing_extensions import Self __all__ = ("NamespacedStore", "StorageObject", "Store") class Store(ABC): """Thread and process safe asynchronous key/value store.""" __slots__ = () @abstractmethod async def set(self, key: str, value: str | bytes, expires_in: int | timedelta | None = None) -> None: """Set a value. Args: key: Key to associate the value with value: Value to store expires_in: Time in seconds before the key is considered expired Returns: ``None`` """ raise NotImplementedError @abstractmethod async def get(self, key: str, renew_for: int | timedelta | None = None) -> bytes | None: """Get a value. Args: key: Key associated with the value renew_for: If given and the value had an initial expiry time set, renew the expiry time for ``renew_for`` seconds. If the value has not been set with an expiry time this is a no-op Returns: The value associated with ``key`` if it exists and is not expired, else ``None`` """ raise NotImplementedError @abstractmethod async def delete(self, key: str) -> None: """Delete a value. If no such key exists, this is a no-op. Args: key: Key of the value to delete """ raise NotImplementedError @abstractmethod async def delete_all(self) -> None: """Delete all stored values.""" raise NotImplementedError @abstractmethod async def exists(self, key: str) -> bool: """Check if a given ``key`` exists.""" raise NotImplementedError @abstractmethod async def expires_in(self, key: str) -> int | None: """Get the time in seconds ``key`` expires in. If no such ``key`` exists or no expiry time was set, return ``None``. """ raise NotImplementedError async def __aenter__(self) -> None: # noqa: B027 pass async def __aexit__( # noqa: B027 self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: pass class NamespacedStore(Store): """A subclass of :class:`Store`, offering hierarchical namespacing. Bulk actions on a parent namespace should affect all child namespaces, whereas other operations on all namespaces should be isolated. """ __slots__ = ("namespace",) @abstractmethod def with_namespace(self, namespace: str) -> Self: """Return a new instance of :class:`NamespacedStore`, which exists in a child namespace of the current namespace. Bulk actions on the parent namespace should affect all child namespaces, whereas other operations on all namespaces should be isolated. """ class StorageObject(Struct): """:class:`msgspec.Struct` to store serialized data alongside with their expiry time.""" expires_at: Optional[datetime] # noqa: UP007 data: bytes @classmethod def new(cls, data: bytes, expires_in: int | timedelta | None) -> StorageObject: """Construct a new :class:`StorageObject` instance.""" if expires_in is not None and not isinstance(expires_in, timedelta): expires_in = timedelta(seconds=expires_in) return cls( data=data, expires_at=(datetime.now(tz=timezone.utc) + expires_in) if expires_in else None, ) @property def expired(self) -> bool: """Return if the :class:`StorageObject` is expired""" return self.expires_at is not None and datetime.now(tz=timezone.utc) >= self.expires_at @property def expires_in(self) -> int: """Return the expiry time of this ``StorageObject`` in seconds. If no expiry time was set, return ``-1``. """ if self.expires_at: return int(self.expires_at.timestamp() - datetime.now(tz=timezone.utc).timestamp()) return -1 def to_bytes(self) -> bytes: """Encode the instance to bytes""" return msgpack_encode(self) @classmethod def from_bytes(cls, raw: bytes) -> StorageObject: """Load a previously encoded with :meth:`StorageObject.to_bytes`""" return msgpack_decode(raw, type=cls) litestar-2.16.0/litestar/stores/file.py000066400000000000000000000134611500564371300200770ustar00rootroot00000000000000from __future__ import annotations import os import shutil import unicodedata from tempfile import mkstemp from typing import TYPE_CHECKING from anyio import Path from litestar.concurrency import sync_to_thread from .base import NamespacedStore, StorageObject __all__ = ("FileStore",) if TYPE_CHECKING: from datetime import timedelta from os import PathLike def _safe_file_name(name: str) -> str: name = unicodedata.normalize("NFKD", name) return "".join(c if c.isalnum() else str(ord(c)) for c in name) class FileStore(NamespacedStore): """File based, thread and process safe, asynchronous key/value store.""" __slots__ = {"create_directories": "flag to create directories in path", "path": "file path"} def __init__(self, path: PathLike[str], *, create_directories: bool = False) -> None: """Initialize ``FileStorage``. Args: path: Path to store data under create_directories: Create the directories in ``path`` if they don't exist Default: ``False`` .. versionadded:: 2.9.0 """ self.path = Path(path) self.create_directories = create_directories async def __aenter__(self) -> None: if self.create_directories: await self.path.mkdir(exist_ok=True, parents=True) return def with_namespace(self, namespace: str) -> FileStore: """Return a new instance of :class:`FileStore`, using a sub-path of the current store's path.""" if not namespace.isalnum(): raise ValueError(f"Invalid namespace: {namespace!r}") return FileStore(self.path / namespace) def _path_from_key(self, key: str) -> Path: return self.path / _safe_file_name(key) @staticmethod async def _load_from_path(path: Path) -> StorageObject | None: try: data = await path.read_bytes() return StorageObject.from_bytes(data) except FileNotFoundError: return None def _write_sync(self, target_file: Path, storage_obj: StorageObject) -> None: try: tmp_file_fd, tmp_file_name = mkstemp(dir=self.path, prefix=f"{target_file.name}.tmp") renamed = False try: try: os.write(tmp_file_fd, storage_obj.to_bytes()) finally: os.close(tmp_file_fd) os.replace(tmp_file_name, target_file) # noqa: PTH105 renamed = True finally: if not renamed: os.unlink(tmp_file_name) # noqa: PTH108 except OSError: pass async def _write(self, target_file: Path, storage_obj: StorageObject) -> None: await sync_to_thread(self._write_sync, target_file, storage_obj) async def set(self, key: str, value: str | bytes, expires_in: int | timedelta | None = None) -> None: """Set a value. Args: key: Key to associate the value with value: Value to store expires_in: Time in seconds before the key is considered expired Returns: ``None`` """ await self.path.mkdir(exist_ok=True) path = self._path_from_key(key) if isinstance(value, str): value = value.encode("utf-8") storage_obj = StorageObject.new(data=value, expires_in=expires_in) await self._write(path, storage_obj) async def get(self, key: str, renew_for: int | timedelta | None = None) -> bytes | None: """Get a value. Args: key: Key associated with the value renew_for: If given and the value had an initial expiry time set, renew the expiry time for ``renew_for`` seconds. If the value has not been set with an expiry time this is a no-op Returns: The value associated with ``key`` if it exists and is not expired, else ``None`` """ path = self._path_from_key(key) storage_obj = await self._load_from_path(path) if not storage_obj: return None if storage_obj.expired: await path.unlink(missing_ok=True) return None if renew_for and storage_obj.expires_at: await self.set(key, value=storage_obj.data, expires_in=renew_for) return storage_obj.data async def delete(self, key: str) -> None: """Delete a value. If no such key exists, this is a no-op. Args: key: Key of the value to delete """ path = self._path_from_key(key) await path.unlink(missing_ok=True) async def delete_all(self) -> None: """Delete all stored values. Note: This deletes and recreates :attr:`FileStore.path` """ await sync_to_thread(shutil.rmtree, self.path) await self.path.mkdir(exist_ok=True) async def delete_expired(self) -> None: """Delete expired items. Since expired items are normally only cleared on access (i.e. when calling :meth:`.get`), this method should be called in regular intervals to free disk space. """ async for file in self.path.iterdir(): wrapper = await self._load_from_path(file) if wrapper and wrapper.expired: await file.unlink(missing_ok=True) async def exists(self, key: str) -> bool: """Check if a given ``key`` exists.""" path = self._path_from_key(key) return await path.exists() async def expires_in(self, key: str) -> int | None: """Get the time in seconds ``key`` expires in. If no such ``key`` exists or no expiry time was set, return ``None``. """ if storage_obj := await self._load_from_path(self._path_from_key(key)): return storage_obj.expires_in return None litestar-2.16.0/litestar/stores/memory.py000066400000000000000000000070441500564371300204700ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import anyio from anyio import Lock from .base import StorageObject, Store __all__ = ("MemoryStore",) if TYPE_CHECKING: from datetime import timedelta class MemoryStore(Store): """In memory, atomic, asynchronous key/value store.""" __slots__ = ("_lock", "_store") def __init__(self) -> None: """Initialize :class:`MemoryStore`""" self._store: dict[str, StorageObject] = {} self._lock = Lock() async def set(self, key: str, value: str | bytes, expires_in: int | timedelta | None = None) -> None: """Set a value. Args: key: Key to associate the value with value: Value to store expires_in: Time in seconds before the key is considered expired Returns: ``None`` """ if isinstance(value, str): value = value.encode("utf-8") async with self._lock: self._store[key] = StorageObject.new(data=value, expires_in=expires_in) async def get(self, key: str, renew_for: int | timedelta | None = None) -> bytes | None: """Get a value. Args: key: Key associated with the value renew_for: If given and the value had an initial expiry time set, renew the expiry time for ``renew_for`` seconds. If the value has not been set with an expiry time this is a no-op Returns: The value associated with ``key`` if it exists and is not expired, else ``None`` """ async with self._lock: storage_obj = self._store.get(key) if not storage_obj: return None if storage_obj.expired: self._store.pop(key) return None if renew_for and storage_obj.expires_at: # don't use .set() here, so we can hold onto the lock for the whole operation storage_obj = StorageObject.new(data=storage_obj.data, expires_in=renew_for) self._store[key] = storage_obj return storage_obj.data async def delete(self, key: str) -> None: """Delete a value. If no such key exists, this is a no-op. Args: key: Key of the value to delete """ async with self._lock: self._store.pop(key, None) async def delete_all(self) -> None: """Delete all stored values.""" async with self._lock: self._store.clear() async def delete_expired(self) -> None: """Delete expired items. Since expired items are normally only cleared on access (i.e. when calling :meth:`.get`), this method should be called in regular intervals to free memory. """ async with self._lock: new_store = {} for i, (key, storage_obj) in enumerate(self._store.items()): if not storage_obj.expired: new_store[key] = storage_obj if i % 1000 == 0: await anyio.sleep(0) self._store = new_store async def exists(self, key: str) -> bool: """Check if a given ``key`` exists.""" return key in self._store async def expires_in(self, key: str) -> int | None: """Get the time in seconds ``key`` expires in. If no such ``key`` exists or no expiry time was set, return ``None``. """ if storage_obj := self._store.get(key): return storage_obj.expires_in return None litestar-2.16.0/litestar/stores/redis.py000066400000000000000000000162761500564371300202750ustar00rootroot00000000000000from __future__ import annotations from datetime import timedelta from typing import TYPE_CHECKING, cast from redis.asyncio import Redis from redis.asyncio.connection import ConnectionPool from litestar.exceptions import ImproperlyConfiguredException from litestar.types import Empty, EmptyType from litestar.utils.empty import value_or_default from .base import NamespacedStore if TYPE_CHECKING: from types import TracebackType from redis.asyncio.connection import Connection __all__ = ("RedisStore",) class RedisStore(NamespacedStore): """Redis based, thread and process safe asynchronous key/value store.""" __slots__ = ( "_delete_all_script", "_get_and_renew_script", "_redis", "handle_client_shutdown", ) def __init__( self, redis: Redis, namespace: str | None | EmptyType = Empty, handle_client_shutdown: bool = False ) -> None: """Initialize :class:`RedisStore` Args: redis: An :class:`redis.asyncio.Redis` instance namespace: A key prefix to simulate a namespace in redis. If not given, defaults to ``LITESTAR``. Namespacing can be explicitly disabled by passing ``None``. This will make :meth:`.delete_all` unavailable. handle_client_shutdown: If ``True``, handle the shutdown of the `redis` instance automatically during the store's lifespan. Should be set to `True` unless the shutdown is handled externally """ self._redis = redis self.namespace: str | None = value_or_default(namespace, "LITESTAR") self.handle_client_shutdown = handle_client_shutdown # script to get and renew a key in one atomic step self._get_and_renew_script = self._redis.register_script( b""" local key = KEYS[1] local renew = tonumber(ARGV[1]) local data = redis.call('GET', key) local ttl = redis.call('TTL', key) if ttl > 0 then redis.call('EXPIRE', key, renew) end return data """ ) # script to delete all keys in the namespace self._delete_all_script = self._redis.register_script( b""" local cursor = 0 repeat local result = redis.call('SCAN', cursor, 'MATCH', ARGV[1]) for _,key in ipairs(result[2]) do redis.call('UNLINK', key) end cursor = tonumber(result[1]) until cursor == 0 """ ) async def _shutdown(self) -> None: if self.handle_client_shutdown: await self._redis.aclose(close_connection_pool=True) # type: ignore[attr-defined] async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: await self._shutdown() @classmethod def with_client( cls, url: str = "redis://localhost:6379", *, db: int | None = None, port: int | None = None, username: str | None = None, password: str | None = None, namespace: str | None | EmptyType = Empty, ) -> RedisStore: """Initialize a :class:`RedisStore` instance with a new class:`redis.asyncio.Redis` instance. Args: url: Redis URL to connect to db: Redis database to use port: Redis port to use username: Redis username to use password: Redis password to use namespace: Virtual key namespace to use """ pool: ConnectionPool[Connection] = ConnectionPool.from_url( url=url, db=db, decode_responses=False, port=port, username=username, password=password, ) return cls( redis=Redis(connection_pool=pool), namespace=namespace, handle_client_shutdown=True, ) def with_namespace(self, namespace: str) -> RedisStore: """Return a new :class:`RedisStore` with a nested virtual key namespace. The current instances namespace will serve as a prefix for the namespace, so it can be considered the parent namespace. """ return type(self)( redis=self._redis, namespace=f"{self.namespace}_{namespace}" if self.namespace else namespace, handle_client_shutdown=self.handle_client_shutdown, ) def _make_key(self, key: str) -> str: prefix = f"{self.namespace}:" if self.namespace else "" return prefix + key async def set(self, key: str, value: str | bytes, expires_in: int | timedelta | None = None) -> None: """Set a value. Args: key: Key to associate the value with value: Value to store expires_in: Time in seconds before the key is considered expired Returns: ``None`` """ if isinstance(value, str): value = value.encode("utf-8") await self._redis.set(self._make_key(key), value, ex=expires_in) async def get(self, key: str, renew_for: int | timedelta | None = None) -> bytes | None: """Get a value. Args: key: Key associated with the value renew_for: If given and the value had an initial expiry time set, renew the expiry time for ``renew_for`` seconds. If the value has not been set with an expiry time this is a no-op. Atomicity of this step is guaranteed by using a lua script to execute fetch and renewal. If ``renew_for`` is not given, the script will be bypassed so no overhead will occur Returns: The value associated with ``key`` if it exists and is not expired, else ``None`` """ key = self._make_key(key) if renew_for: if isinstance(renew_for, timedelta): renew_for = renew_for.seconds data = await self._get_and_renew_script(keys=[key], args=[renew_for]) return cast("bytes | None", data) return await self._redis.get(key) async def delete(self, key: str) -> None: """Delete a value. If no such key exists, this is a no-op. Args: key: Key of the value to delete """ await self._redis.delete(self._make_key(key)) async def delete_all(self) -> None: """Delete all stored values in the virtual key namespace. Raises: ImproperlyConfiguredException: If no namespace was configured """ if not self.namespace: raise ImproperlyConfiguredException("Cannot perform delete operation: No namespace configured") await self._delete_all_script(keys=[], args=[f"{self.namespace}*:*"]) async def exists(self, key: str) -> bool: """Check if a given ``key`` exists.""" return await self._redis.exists(self._make_key(key)) == 1 async def expires_in(self, key: str) -> int | None: """Get the time in seconds ``key`` expires in. If no such ``key`` exists or no expiry time was set, return ``None``. """ ttl = await self._redis.ttl(self._make_key(key)) return None if ttl == -2 else ttl litestar-2.16.0/litestar/stores/registry.py000066400000000000000000000042501500564371300210240ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Callable if TYPE_CHECKING: from .base import Store from .memory import MemoryStore __all__ = ("StoreRegistry",) def default_default_factory(name: str) -> Store: return MemoryStore() class StoreRegistry: """Registry for :class:`Store <.base.Store>` instances.""" __slots__ = ("_default_factory", "_stores") def __init__( self, stores: dict[str, Store] | None = None, default_factory: Callable[[str], Store] = default_default_factory ) -> None: """Initialize ``StoreRegistry``. Args: stores: A dictionary mapping store names to stores, used to initialize the registry default_factory: A callable used by :meth:`StoreRegistry.get` to provide a store, if the requested name hasn't been registered yet. This callable receives the requested name and should return a :class:`Store <.base.Store>` instance. """ self._stores = stores or {} self._default_factory = default_factory def register(self, name: str, store: Store, allow_override: bool = False) -> None: """Register a new :class:`Store <.base.Store>`. Args: name: Name to register the store under store: The store to register allow_override: Whether to allow overriding an existing store of the same name Raises: ValueError: If a store is already registered under this name and ``override`` is not ``True`` """ if not allow_override and name in self._stores: raise ValueError(f"Store with the name {name!r} already exists") self._stores[name] = store def get(self, name: str) -> Store: """Get a store registered under ``name``. If no such store is registered, create a store using the default factory with ``name`` and register the returned store under ``name``. Args: name: Name of the store Returns: A :class:`Store <.base.Store>` """ if not self._stores.get(name): self._stores[name] = self._default_factory(name) return self._stores[name] litestar-2.16.0/litestar/stores/valkey.py000066400000000000000000000163141500564371300204530ustar00rootroot00000000000000from __future__ import annotations from datetime import timedelta from typing import TYPE_CHECKING, cast from valkey.asyncio import Valkey from valkey.asyncio.connection import ConnectionPool from litestar.exceptions import ImproperlyConfiguredException from litestar.types import Empty, EmptyType from litestar.utils.empty import value_or_default from .base import NamespacedStore if TYPE_CHECKING: from types import TracebackType __all__ = ("ValkeyStore",) class ValkeyStore(NamespacedStore): """Valkey based, thread and process safe asynchronous key/value store.""" __slots__ = ( "_delete_all_script", "_get_and_renew_script", "_valkey", "handle_client_shutdown", ) def __init__( self, valkey: Valkey, namespace: str | None | EmptyType = Empty, handle_client_shutdown: bool = False ) -> None: """Initialize :class:`ValkeyStore` Args: valkey: An :class:`valkey.asyncio.Valkey` instance namespace: A key prefix to simulate a namespace in valkey. If not given, defaults to ``LITESTAR``. Namespacing can be explicitly disabled by passing ``None``. This will make :meth:`.delete_all` unavailable. handle_client_shutdown: If ``True``, handle the shutdown of the `valkey` instance automatically during the store's lifespan. Should be set to `True` unless the shutdown is handled externally """ self._valkey = valkey self.namespace: str | None = value_or_default(namespace, "LITESTAR") self.handle_client_shutdown = handle_client_shutdown # script to get and renew a key in one atomic step self._get_and_renew_script = self._valkey.register_script( b""" local key = KEYS[1] local renew = tonumber(ARGV[1]) local data = server.call('GET', key) local ttl = server.call('TTL', key) if ttl > 0 then server.call('EXPIRE', key, renew) end return data """ ) # script to delete all keys in the namespace self._delete_all_script = self._valkey.register_script( b""" local cursor = 0 repeat local result = server.call('SCAN', cursor, 'MATCH', ARGV[1]) for _,key in ipairs(result[2]) do server.call('UNLINK', key) end cursor = tonumber(result[1]) until cursor == 0 """ ) async def _shutdown(self) -> None: if self.handle_client_shutdown: await self._valkey.aclose(close_connection_pool=True) async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: await self._shutdown() @classmethod def with_client( cls, url: str = "valkey://localhost:6379", *, db: int | None = None, port: int | None = None, username: str | None = None, password: str | None = None, namespace: str | None | EmptyType = Empty, ) -> ValkeyStore: """Initialize a :class:`ValkeyStore` instance with a new class:`valkey.asyncio.Valkey` instance. Args: url: Valkey URL to connect to db: Valkey database to use port: Valkey port to use username: Valkey username to use password: Valkey password to use namespace: Virtual key namespace to use """ pool: ConnectionPool = ConnectionPool.from_url( url=url, db=db, decode_responses=False, port=port, username=username, password=password, ) return cls( valkey=Valkey(connection_pool=pool), namespace=namespace, handle_client_shutdown=True, ) def with_namespace(self, namespace: str) -> ValkeyStore: """Return a new :class:`ValkeyStore` with a nested virtual key namespace. The current instances namespace will serve as a prefix for the namespace, so it can be considered the parent namespace. """ return type(self)( valkey=self._valkey, namespace=f"{self.namespace}_{namespace}" if self.namespace else namespace, handle_client_shutdown=self.handle_client_shutdown, ) def _make_key(self, key: str) -> str: prefix = f"{self.namespace}:" if self.namespace else "" return prefix + key async def set(self, key: str, value: str | bytes, expires_in: int | timedelta | None = None) -> None: """Set a value. Args: key: Key to associate the value with value: Value to store expires_in: Time in seconds before the key is considered expired Returns: ``None`` """ if isinstance(value, str): value = value.encode("utf-8") await self._valkey.set(self._make_key(key), value, ex=expires_in) async def get(self, key: str, renew_for: int | timedelta | None = None) -> bytes | None: """Get a value. Args: key: Key associated with the value renew_for: If given and the value had an initial expiry time set, renew the expiry time for ``renew_for`` seconds. If the value has not been set with an expiry time this is a no-op. Atomicity of this step is guaranteed by using a lua script to execute fetch and renewal. If ``renew_for`` is not given, the script will be bypassed so no overhead will occur Returns: The value associated with ``key`` if it exists and is not expired, else ``None`` """ key = self._make_key(key) if renew_for: if isinstance(renew_for, timedelta): renew_for = renew_for.seconds data = await self._get_and_renew_script(keys=[key], args=[renew_for]) return cast("bytes | None", data) return await self._valkey.get(key) # type: ignore[no-any-return] async def delete(self, key: str) -> None: """Delete a value. If no such key exists, this is a no-op. Args: key: Key of the value to delete """ await self._valkey.delete(self._make_key(key)) async def delete_all(self) -> None: """Delete all stored values in the virtual key namespace. Raises: ImproperlyConfiguredException: If no namespace was configured """ if not self.namespace: raise ImproperlyConfiguredException("Cannot perform delete operation: No namespace configured") await self._delete_all_script(keys=[], args=[f"{self.namespace}*:*"]) async def exists(self, key: str) -> bool: """Check if a given ``key`` exists.""" return await self._valkey.exists(self._make_key(key)) == 1 # type: ignore[no-any-return] async def expires_in(self, key: str) -> int | None: """Get the time in seconds ``key`` expires in. If no such ``key`` exists or no expiry time was set, return ``None``. """ ttl = await self._valkey.ttl(self._make_key(key)) return None if ttl == -2 else ttl litestar-2.16.0/litestar/template/000077500000000000000000000000001500564371300170755ustar00rootroot00000000000000litestar-2.16.0/litestar/template/__init__.py000066400000000000000000000003141500564371300212040ustar00rootroot00000000000000from litestar.template.base import TemplateEngineProtocol, TemplateProtocol from litestar.template.config import TemplateConfig __all__ = ("TemplateConfig", "TemplateEngineProtocol", "TemplateProtocol") litestar-2.16.0/litestar/template/base.py000066400000000000000000000134661500564371300203730ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable, Mapping, Protocol, TypedDict, TypeVar, cast, runtime_checkable from typing_extensions import Concatenate, ParamSpec, TypeAlias from litestar.utils.deprecation import warn_deprecation from litestar.utils.empty import value_or_default from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from pathlib import Path from litestar.connection import Request __all__ = ( "TemplateCallableType", "TemplateEngineProtocol", "TemplateProtocol", "csrf_token", "url_for", "url_for_static_asset", ) def _get_request_from_context(context: Mapping[str, Any]) -> Request: """Get the request from the template context. Args: context: The template context. Returns: The request object. """ return cast("Request", context["request"]) def url_for(context: Mapping[str, Any], /, route_name: str, **path_parameters: Any) -> str: """Wrap :func:`route_reverse ` to be used in templates. Args: context: The template context. route_name: The name of the route handler. **path_parameters: Actual values for path parameters in the route. Raises: NoRouteMatchFoundException: If ``route_name`` does not exist, path parameters are missing in **path_parameters or have wrong type. Returns: A fully formatted url path. """ return _get_request_from_context(context).app.route_reverse(route_name, **path_parameters) def csrf_token(context: Mapping[str, Any], /) -> str: """Set a CSRF token on the template. Notes: - to use this function make sure to pass an instance of :ref:`CSRFConfig ` to the :class:`Litestar ` constructor. Args: context: The template context. Returns: A CSRF token if the app level ``csrf_config`` is set, otherwise an empty string. """ scope = _get_request_from_context(context).scope return value_or_default(ScopeState.from_scope(scope).csrf_token, "") def url_for_static_asset(context: Mapping[str, Any], /, name: str, file_path: str) -> str: """Wrap :meth:`url_for_static_asset ` to be used in templates. Args: context: The template context object. name: A static handler unique name. file_path: a string containing path to an asset. Raises: NoRouteMatchFoundException: If static files handler with ``name`` does not exist. Returns: A url path to the asset. """ return _get_request_from_context(context).app.url_for_static_asset(name, file_path) class TemplateProtocol(Protocol): """Protocol Defining a ``Template``. Template is a class that has a render method which renders the template into a string. """ def render(self, *args: Any, **kwargs: Any) -> str: """Return the rendered template as a string. Args: *args: Positional arguments passed to the TemplateEngine **kwargs: A string keyed mapping of values passed to the TemplateEngine Returns: The rendered template string """ raise NotImplementedError P = ParamSpec("P") R = TypeVar("R") ContextType = TypeVar("ContextType") ContextType_co = TypeVar("ContextType_co", covariant=True) TemplateType_co = TypeVar("TemplateType_co", bound=TemplateProtocol, covariant=True) TemplateCallableType: TypeAlias = Callable[Concatenate[ContextType, P], R] @runtime_checkable class TemplateEngineProtocol(Protocol[TemplateType_co, ContextType_co]): """Protocol for template engines.""" def __init__(self, directory: Path | list[Path] | None, engine_instance: Any | None) -> None: """Initialize the template engine with a directory. Args: directory: Direct path or list of directory paths from which to serve templates, if provided the implementation has to create the engine instance. engine_instance: A template engine object, if provided the implementation has to use it. """ def get_template(self, template_name: str) -> TemplateType_co: """Retrieve a template by matching its name (dotted path) with files in the directory or directories provided. Args: template_name: A dotted path Returns: Template instance Raises: TemplateNotFoundException: if no template is found. """ raise NotImplementedError def render_string(self, template_string: str, context: Mapping[str, Any]) -> str: """Render a template from a string with the given context. Args: template_string: The template string to render. context: A dictionary of variables to pass to the template. Returns: The rendered template as a string. """ raise NotImplementedError def register_template_callable( self, key: str, template_callable: TemplateCallableType[ContextType_co, P, R] ) -> None: """Register a callable on the template engine. Args: key: The callable key, i.e. the value to use inside the template to call the callable. template_callable: A callable to register. Returns: None """ class _TemplateContext(TypedDict): """Dictionary representing a template context.""" request: Request[Any, Any, Any] csrf_input: str def __getattr__(name: str) -> Any: if name == "TemplateContext": warn_deprecation( "2.3.0", "TemplateContext", "import", removal_in="3.0.0", alternative="Mapping", ) return _TemplateContext raise AttributeError(f"module {__name__!r} has no attribute {name!r}") litestar-2.16.0/litestar/template/config.py000066400000000000000000000046111500564371300207160ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from functools import cached_property from inspect import isclass from typing import TYPE_CHECKING, Callable, Generic, TypeVar, cast from litestar.exceptions import ImproperlyConfiguredException from litestar.template import TemplateEngineProtocol __all__ = ("TemplateConfig",) if TYPE_CHECKING: from litestar.types import PathType EngineType = TypeVar("EngineType", bound=TemplateEngineProtocol) @dataclass class TemplateConfig(Generic[EngineType]): """Configuration for Templating. To enable templating, pass an instance of this class to the :class:`Litestar ` constructor using the 'template_config' key. """ engine: type[EngineType] | EngineType | None = field(default=None) """A template engine adhering to the :class:`TemplateEngineProtocol `.""" directory: PathType | list[PathType] | None = field(default=None) """A directory or list of directories from which to serve templates.""" engine_callback: Callable[[EngineType], None] | None = field(default=None) """A callback function that allows modifying the instantiated templating protocol.""" instance: EngineType | None = field(default=None) """An instance of the templating protocol.""" def __post_init__(self) -> None: """Ensure that directory is set if engine is a class.""" if isclass(self.engine) and not self.directory: raise ImproperlyConfiguredException("directory is a required kwarg when passing a template engine class") """Ensure that directory is not set if instance is.""" if self.instance is not None and self.directory is not None: raise ImproperlyConfiguredException("directory cannot be set if instance is") def to_engine(self) -> EngineType: """Instantiate the template engine.""" template_engine = cast( "EngineType", self.engine(directory=self.directory, engine_instance=None) if isclass(self.engine) else self.engine, ) if callable(self.engine_callback): self.engine_callback(template_engine) return template_engine @cached_property def engine_instance(self) -> EngineType: """Return the template engine instance.""" return self.to_engine() if self.instance is None else self.instance litestar-2.16.0/litestar/testing/000077500000000000000000000000001500564371300167375ustar00rootroot00000000000000litestar-2.16.0/litestar/testing/__init__.py000066400000000000000000000013501500564371300210470ustar00rootroot00000000000000from litestar.testing.client.async_client import AsyncTestClient from litestar.testing.client.base import BaseTestClient from litestar.testing.client.subprocess_client import subprocess_async_client, subprocess_sync_client from litestar.testing.client.sync_client import TestClient from litestar.testing.helpers import create_async_test_client, create_test_client from litestar.testing.request_factory import RequestFactory from litestar.testing.websocket_test_session import WebSocketTestSession __all__ = ( "AsyncTestClient", "BaseTestClient", "RequestFactory", "TestClient", "WebSocketTestSession", "create_async_test_client", "create_test_client", "subprocess_async_client", "subprocess_sync_client", ) litestar-2.16.0/litestar/testing/client/000077500000000000000000000000001500564371300202155ustar00rootroot00000000000000litestar-2.16.0/litestar/testing/client/__init__.py000066400000000000000000000034301500564371300223260ustar00rootroot00000000000000"""Some code in this module was adapted from https://github.com/encode/starlette/blob/master/starlette/testclient.py. Copyright © 2018, [Encode OSS Ltd](https://www.encode.io/). All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ from .async_client import AsyncTestClient from .base import BaseTestClient from .sync_client import TestClient __all__ = ("AsyncTestClient", "BaseTestClient", "TestClient") litestar-2.16.0/litestar/testing/client/async_client.py000066400000000000000000000166141500564371300232520ustar00rootroot00000000000000from __future__ import annotations from contextlib import AsyncExitStack from typing import TYPE_CHECKING, Any, Generic, Mapping, Sequence, TypeVar from httpx import USE_CLIENT_DEFAULT, AsyncClient from litestar.testing.client.base import BaseTestClient from litestar.testing.life_span_handler import LifeSpanHandler from litestar.testing.transport import ConnectionUpgradeExceptionError, TestClientTransport from litestar.types import AnyIOBackend, ASGIApp if TYPE_CHECKING: from httpx._client import UseClientDefault from httpx._types import ( AuthTypes, CookieTypes, HeaderTypes, QueryParamTypes, TimeoutTypes, ) from typing_extensions import Self from litestar.middleware.session.base import BaseBackendConfig from litestar.testing.websocket_test_session import WebSocketTestSession T = TypeVar("T", bound=ASGIApp) class AsyncTestClient(AsyncClient, BaseTestClient, Generic[T]): # type: ignore[misc] lifespan_handler: LifeSpanHandler[Any] exit_stack: AsyncExitStack def __init__( self, app: T, base_url: str = "http://testserver.local", raise_server_exceptions: bool = True, root_path: str = "", backend: AnyIOBackend = "asyncio", backend_options: Mapping[str, Any] | None = None, session_config: BaseBackendConfig | None = None, timeout: float | None = None, cookies: CookieTypes | None = None, ) -> None: """An Async client implementation providing a context manager for testing applications asynchronously. Args: app: The instance of :class:`Litestar ` under test. base_url: URL scheme and domain for test request paths, e.g. ``http://testserver``. raise_server_exceptions: Flag for the underlying test client to raise server exceptions instead of wrapping them in an HTTP response. root_path: Path prefix for requests. backend: The async backend to use, options are "asyncio" or "trio". backend_options: 'anyio' options. session_config: Configuration for Session Middleware class to create raw session cookies for request to the route handlers. timeout: Request timeout cookies: Cookies to set on the client. """ BaseTestClient.__init__( self, app=app, base_url=base_url, backend=backend, backend_options=backend_options, session_config=session_config, cookies=cookies, ) AsyncClient.__init__( self, base_url=base_url, headers={"user-agent": "testclient"}, follow_redirects=True, cookies=cookies, transport=TestClientTransport( # type: ignore [arg-type] client=self, raise_server_exceptions=raise_server_exceptions, root_path=root_path, ), timeout=timeout, ) async def __aenter__(self) -> Self: async with AsyncExitStack() as stack: self.blocking_portal = portal = stack.enter_context(self.portal()) self.lifespan_handler = LifeSpanHandler(client=self) stack.enter_context(self.lifespan_handler) @stack.callback def reset_portal() -> None: delattr(self, "blocking_portal") @stack.callback def wait_shutdown() -> None: portal.call(self.lifespan_handler.wait_shutdown) self.exit_stack = stack.pop_all() return self async def __aexit__(self, *args: Any) -> None: await self.exit_stack.aclose() async def websocket_connect( self, url: str, subprotocols: Sequence[str] | None = None, params: QueryParamTypes | None = None, headers: HeaderTypes | None = None, cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: Mapping[str, Any] | None = None, ) -> WebSocketTestSession: """Sends a GET request to establish a websocket connection. Args: url: Request URL. subprotocols: Websocket subprotocols. params: Query parameters. headers: Request headers. cookies: Request cookies. auth: Auth headers. follow_redirects: Whether to follow redirects. timeout: Request timeout. extensions: Dictionary of ASGI extensions. Returns: A `WebSocketTestSession ` instance. """ try: await self.send( self._prepare_ws_connect_request( url=url, subprotocols=subprotocols, params=params, headers=headers, cookies=cookies, extensions=extensions, timeout=timeout, ), auth=auth, follow_redirects=follow_redirects, ) except ConnectionUpgradeExceptionError as exc: return exc.session raise RuntimeError("Expected WebSocket upgrade") # pragma: no cover async def get_session_data(self) -> dict[str, Any]: """Get session data. Returns: A dictionary containing session data. Examples: .. code-block:: python from litestar import Litestar, post from litestar.middleware.session.memory_backend import MemoryBackendConfig session_config = MemoryBackendConfig() @post(path="/test") def set_session_data(request: Request) -> None: request.session["foo"] == "bar" app = Litestar( route_handlers=[set_session_data], middleware=[session_config.middleware] ) async with AsyncTestClient(app=app, session_config=session_config) as client: await client.post("/test") assert await client.get_session_data() == {"foo": "bar"} """ return await super()._get_session_data() async def set_session_data(self, data: dict[str, Any]) -> None: """Set session data. Args: data: Session data Returns: None Examples: .. code-block:: python from litestar import Litestar, get from litestar.middleware.session.memory_backend import MemoryBackendConfig session_config = MemoryBackendConfig() @get(path="/test") def get_session_data(request: Request) -> Dict[str, Any]: return request.session app = Litestar( route_handlers=[get_session_data], middleware=[session_config.middleware] ) async with AsyncTestClient(app=app, session_config=session_config) as client: await client.set_session_data({"foo": "bar"}) assert await client.get("/test").json() == {"foo": "bar"} """ return await super()._set_session_data(data) litestar-2.16.0/litestar/testing/client/base.py000066400000000000000000000172121500564371300215040ustar00rootroot00000000000000from __future__ import annotations from contextlib import contextmanager from http.cookiejar import CookieJar from typing import TYPE_CHECKING, Any, Generator, Generic, Mapping, Sequence, TypeVar, cast from warnings import warn import httpx from anyio.from_thread import BlockingPortal, start_blocking_portal from httpx import Cookies, Request, Response from httpx._client import USE_CLIENT_DEFAULT, BaseClient, UseClientDefault from litestar import Litestar from litestar.connection import ASGIConnection from litestar.datastructures import MutableScopeHeaders from litestar.enums import ScopeType from litestar.exceptions import ( ImproperlyConfiguredException, ) from litestar.types import AnyIOBackend, ASGIApp, HTTPResponseStartEvent from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from httpx._types import ( CookieTypes, HeaderTypes, QueryParamTypes, TimeoutTypes, ) from litestar.middleware.session.base import BaseBackendConfig, BaseSessionBackend from litestar.types.asgi_types import HTTPScope, Receive, Scope, Send T = TypeVar("T", bound=ASGIApp) def fake_http_send_message(headers: MutableScopeHeaders) -> HTTPResponseStartEvent: headers.setdefault("content-type", "application/text") return HTTPResponseStartEvent(type="http.response.start", status=200, headers=headers.headers) def fake_asgi_connection(app: ASGIApp, cookies: dict[str, str]) -> ASGIConnection[Any, Any, Any, Any]: scope: HTTPScope = { "type": ScopeType.HTTP, "path": "/", "raw_path": b"/", "root_path": "", "scheme": "http", "query_string": b"", "client": ("testclient", 50000), "server": ("testserver", 80), "headers": [], "method": "GET", "http_version": "1.1", "extensions": {"http.response.template": {}}, "app": app, # type: ignore[typeddict-item] "litestar_app": app, "state": {}, "path_params": {}, "route_handler": None, "asgi": {"version": "3.0", "spec_version": "2.1"}, "auth": None, "session": None, "user": None, } ScopeState.from_scope(scope).cookies = cookies return ASGIConnection[Any, Any, Any, Any](scope=scope) def _wrap_app_to_add_state(app: ASGIApp) -> ASGIApp: """Wrap an ASGI app to add state to the scope. Litestar depends on `state` being present in the ASGI connection scope. Scope state is optional in the ASGI spec, however, the Litestar app always ensures it is present so that it can be depended on internally. When the ASGI app that is passed to the test client is _not_ a Litestar app, we need to add state to the scope, because httpx does not do this for us. This assists us in testing Litestar components that rely on state being present in the scope, without having to create a Litestar app for every test case. Args: app: The ASGI app to wrap. Returns: The wrapped ASGI app. """ async def wrapped(scope: Scope, receive: Receive, send: Send) -> None: scope["state"] = {} await app(scope, receive, send) return wrapped class BaseTestClient(Generic[T]): __test__ = False blocking_portal: BlockingPortal __slots__ = ( "_session_backend", "app", "backend", "backend_options", "base_url", "cookies", "session_config", ) def __init__( self, app: T, base_url: str = "http://testserver.local", backend: AnyIOBackend = "asyncio", backend_options: Mapping[str, Any] | None = None, session_config: BaseBackendConfig | None = None, cookies: CookieTypes | None = None, ) -> None: if "." not in base_url: warn( f"The base_url {base_url!r} might cause issues. Try adding a domain name such as .local: " f"'{base_url}.local'", UserWarning, stacklevel=1, ) self._session_backend: BaseSessionBackend | None = None if session_config: self._session_backend = session_config._backend_class(config=session_config) if not isinstance(app, Litestar): app = _wrap_app_to_add_state(app) # type: ignore[assignment] self.app = cast("T", app) # type: ignore[redundant-cast] # pyright needs this self.base_url = base_url self.backend = backend self.backend_options = backend_options self.cookies = cookies @property def session_backend(self) -> BaseSessionBackend[Any]: if not self._session_backend: raise ImproperlyConfiguredException( "Session has not been initialized for this TestClient instance. You can" "do so by passing a configuration object to TestClient: TestClient(app=app, session_config=...)" ) return self._session_backend @contextmanager def portal(self) -> Generator[BlockingPortal, None, None]: """Get a BlockingPortal. Returns: A contextmanager for a BlockingPortal. """ if hasattr(self, "blocking_portal"): yield self.blocking_portal else: with start_blocking_portal( backend=self.backend, backend_options=dict(self.backend_options or {}) ) as portal: yield portal async def _set_session_data(self, data: dict[str, Any]) -> None: mutable_headers = MutableScopeHeaders() connection = fake_asgi_connection( app=self.app, cookies=dict(self.cookies), # type: ignore[arg-type] ) session_id = self.session_backend.get_session_id(connection) connection._connection_state.session_id = session_id # pyright: ignore [reportGeneralTypeIssues] await self.session_backend.store_in_message( scope_session=data, message=fake_http_send_message(mutable_headers), connection=connection ) response = Response(200, request=Request("GET", self.base_url), headers=mutable_headers.headers) cookies = Cookies(CookieJar()) cookies.extract_cookies(response) self.cookies.update(cookies) # type: ignore[union-attr] async def _get_session_data(self) -> dict[str, Any]: return await self.session_backend.load_from_connection( connection=fake_asgi_connection( app=self.app, cookies=dict(self.cookies), # type: ignore[arg-type] ), ) def _prepare_ws_connect_request( # type: ignore[misc] self: BaseClient, # pyright: ignore url: str, subprotocols: Sequence[str] | None = None, params: QueryParamTypes | None = None, headers: HeaderTypes | None = None, cookies: CookieTypes | None = None, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: Mapping[str, Any] | None = None, ) -> httpx.Request: default_headers: dict[str, str] = {} default_headers.setdefault("connection", "upgrade") default_headers.setdefault("sec-websocket-key", "testserver==") default_headers.setdefault("sec-websocket-version", "13") if subprotocols is not None: default_headers.setdefault("sec-websocket-protocol", ", ".join(subprotocols)) return self.build_request( "GET", self.base_url.copy_with(scheme="ws").join(url), headers={**dict(headers or {}), **default_headers}, # type: ignore[misc] params=params, cookies=cookies, extensions=None if extensions is None else dict(extensions), timeout=timeout, ) litestar-2.16.0/litestar/testing/client/subprocess_client.py000066400000000000000000000056741500564371300243310ustar00rootroot00000000000000import pathlib import socket import subprocess import time from contextlib import asynccontextmanager, contextmanager from typing import AsyncIterator, Iterator import httpx class StartupError(RuntimeError): pass def _get_available_port() -> int: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: # Bind to a free port provided by the host try: sock.bind(("localhost", 0)) except OSError as e: # pragma: no cover raise StartupError("Could not find an open port") from e else: port: int = sock.getsockname()[1] return port @contextmanager def run_app(workdir: pathlib.Path, app: str, retry_count: int = 100, retry_timeout: int = 1) -> Iterator[str]: """Launch a litestar application in a subprocess with a random available port. Args: workdir: Path to working directory where run command will be executed app: Path to Litestar application, e.g.: "my_app:application" retry_count: Number of retries to wait for the application to start retry_timeout: Timeout in seconds to wait between retries Raises: StartupError: If the application fails to start with given retry count and timeout """ port = _get_available_port() with subprocess.Popen( args=["litestar", "--app", app, "run", "--port", str(port)], stderr=subprocess.PIPE, stdout=subprocess.PIPE, cwd=workdir, ) as proc: url = f"http://127.0.0.1:{port}" application_started = False for _ in range(retry_count): try: httpx.get(url, timeout=0.1) application_started = True break except httpx.TransportError: time.sleep(retry_timeout) if not application_started: proc.kill() raise StartupError("Application failed to start") yield url proc.kill() @asynccontextmanager async def subprocess_async_client(workdir: pathlib.Path, app: str) -> AsyncIterator[httpx.AsyncClient]: """Provides an async httpx client for a litestar app launched in a subprocess. Args: workdir: Path to the directory in which the app module resides. app: Uvicorn app string that can be resolved in the provided working directory, e.g.: "app:app" """ with run_app(workdir=workdir, app=app) as url: async with httpx.AsyncClient(base_url=url) as client: yield client @contextmanager def subprocess_sync_client(workdir: pathlib.Path, app: str) -> Iterator[httpx.Client]: """Provides a sync httpx client for a litestar app launched in a subprocess. Args: workdir: Path to the directory in which the app module resides. app: Uvicorn app string that can be resolved in the provided working directory, e.g.: "app:app" """ with run_app(workdir=workdir, app=app) as url, httpx.Client(base_url=url) as client: yield client litestar-2.16.0/litestar/testing/client/sync_client.py000066400000000000000000000165101500564371300231040ustar00rootroot00000000000000from __future__ import annotations from contextlib import ExitStack from typing import TYPE_CHECKING, Any, Generic, Mapping, Sequence, TypeVar from httpx import USE_CLIENT_DEFAULT, Client from litestar.testing.client.base import BaseTestClient from litestar.testing.life_span_handler import LifeSpanHandler from litestar.testing.transport import ConnectionUpgradeExceptionError, TestClientTransport from litestar.types import AnyIOBackend, ASGIApp if TYPE_CHECKING: from httpx._client import UseClientDefault from httpx._types import ( AuthTypes, CookieTypes, HeaderTypes, QueryParamTypes, TimeoutTypes, ) from typing_extensions import Self from litestar.middleware.session.base import BaseBackendConfig from litestar.testing.websocket_test_session import WebSocketTestSession T = TypeVar("T", bound=ASGIApp) class TestClient(Client, BaseTestClient, Generic[T]): # type: ignore[misc] lifespan_handler: LifeSpanHandler[Any] exit_stack: ExitStack def __init__( self, app: T, base_url: str = "http://testserver.local", raise_server_exceptions: bool = True, root_path: str = "", backend: AnyIOBackend = "asyncio", backend_options: Mapping[str, Any] | None = None, session_config: BaseBackendConfig | None = None, timeout: float | None = None, cookies: CookieTypes | None = None, ) -> None: """A client implementation providing a context manager for testing applications. Args: app: The instance of :class:`Litestar ` under test. base_url: URL scheme and domain for test request paths, e.g. ``http://testserver``. raise_server_exceptions: Flag for the underlying test client to raise server exceptions instead of wrapping them in an HTTP response. root_path: Path prefix for requests. backend: The async backend to use, options are "asyncio" or "trio". backend_options: ``anyio`` options. session_config: Configuration for Session Middleware class to create raw session cookies for request to the route handlers. timeout: Request timeout cookies: Cookies to set on the client. """ BaseTestClient.__init__( self, app=app, base_url=base_url, backend=backend, backend_options=backend_options, session_config=session_config, cookies=cookies, ) Client.__init__( self, base_url=base_url, headers={"user-agent": "testclient"}, follow_redirects=True, cookies=cookies, transport=TestClientTransport( # type: ignore[arg-type] client=self, raise_server_exceptions=raise_server_exceptions, root_path=root_path, ), timeout=timeout, ) def __enter__(self) -> Self: with ExitStack() as stack: self.blocking_portal = portal = stack.enter_context(self.portal()) self.lifespan_handler = LifeSpanHandler(client=self) stack.enter_context(self.lifespan_handler) @stack.callback def reset_portal() -> None: delattr(self, "blocking_portal") @stack.callback def wait_shutdown() -> None: portal.call(self.lifespan_handler.wait_shutdown) self.exit_stack = stack.pop_all() return self def __exit__(self, *args: Any) -> None: self.exit_stack.close() def websocket_connect( self, url: str, subprotocols: Sequence[str] | None = None, params: QueryParamTypes | None = None, headers: HeaderTypes | None = None, cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: Mapping[str, Any] | None = None, ) -> WebSocketTestSession: """Sends a GET request to establish a websocket connection. Args: url: Request URL. subprotocols: Websocket subprotocols. params: Query parameters. headers: Request headers. cookies: Request cookies. auth: Auth headers. follow_redirects: Whether to follow redirects. timeout: Request timeout. extensions: Dictionary of ASGI extensions. Returns: A `WebSocketTestSession ` instance. """ try: self.send( self._prepare_ws_connect_request( url=url, subprotocols=subprotocols, params=params, headers=headers, cookies=cookies, extensions=extensions, timeout=timeout, ), auth=auth, follow_redirects=follow_redirects, ) except ConnectionUpgradeExceptionError as exc: return exc.session raise RuntimeError("Expected WebSocket upgrade") # pragma: no cover def set_session_data(self, data: dict[str, Any]) -> None: """Set session data. Args: data: Session data Returns: None Examples: .. code-block:: python from litestar import Litestar, get from litestar.middleware.session.memory_backend import MemoryBackendConfig session_config = MemoryBackendConfig() @get(path="/test") def get_session_data(request: Request) -> Dict[str, Any]: return request.session app = Litestar( route_handlers=[get_session_data], middleware=[session_config.middleware] ) with TestClient(app=app, session_config=session_config) as client: client.set_session_data({"foo": "bar"}) assert client.get("/test").json() == {"foo": "bar"} """ with self.portal() as portal: portal.call(self._set_session_data, data) def get_session_data(self) -> dict[str, Any]: """Get session data. Returns: A dictionary containing session data. Examples: .. code-block:: python from litestar import Litestar, post from litestar.middleware.session.memory_backend import MemoryBackendConfig session_config = MemoryBackendConfig() @post(path="/test") def set_session_data(request: Request) -> None: request.session["foo"] == "bar" app = Litestar( route_handlers=[set_session_data], middleware=[session_config.middleware] ) with TestClient(app=app, session_config=session_config) as client: client.post("/test") assert client.get_session_data() == {"foo": "bar"} """ with self.portal() as portal: return portal.call(self._get_session_data) litestar-2.16.0/litestar/testing/helpers.py000066400000000000000000000752651500564371300207720ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable, Literal, Mapping, Sequence from litestar.app import DEFAULT_OPENAPI_CONFIG, Litestar from litestar.controller import Controller from litestar.events import SimpleEventEmitter from litestar.testing.client import AsyncTestClient, TestClient from litestar.types import Empty from litestar.utils.predicates import is_class_and_subclass if TYPE_CHECKING: from contextlib import AbstractAsyncContextManager from litestar import Request, Response, WebSocket from litestar.config.allowed_hosts import AllowedHostsConfig from litestar.config.app import ExperimentalFeatures from litestar.config.compression import CompressionConfig from litestar.config.cors import CORSConfig from litestar.config.csrf import CSRFConfig from litestar.config.response_cache import ResponseCacheConfig from litestar.datastructures import CacheControlHeader, ETag, State from litestar.dto import AbstractDTO from litestar.events import BaseEventEmitterBackend, EventListener from litestar.logging.config import BaseLoggingConfig from litestar.middleware.session.base import BaseBackendConfig from litestar.openapi.config import OpenAPIConfig from litestar.openapi.spec import SecurityRequirement from litestar.plugins import PluginProtocol from litestar.static_files.config import StaticFilesConfig from litestar.stores.base import Store from litestar.stores.registry import StoreRegistry from litestar.template.config import TemplateConfig from litestar.types import ( AfterExceptionHookHandler, AfterRequestHookHandler, AfterResponseHookHandler, BeforeMessageSendHookHandler, BeforeRequestHookHandler, ControllerRouterHandler, Dependencies, EmptyType, ExceptionHandlersMap, Guard, LifespanHook, Middleware, OnAppInitHandler, ParametersMap, ResponseCookies, ResponseHeaders, TypeEncodersMap, ) def create_test_client( route_handlers: ControllerRouterHandler | Sequence[ControllerRouterHandler] | None = None, *, after_exception: Sequence[AfterExceptionHookHandler] | None = None, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, allowed_hosts: Sequence[str] | AllowedHostsConfig | None = None, backend: Literal["asyncio", "trio"] = "asyncio", backend_options: Mapping[str, Any] | None = None, base_url: str = "http://testserver.local", before_request: BeforeRequestHookHandler | None = None, before_send: Sequence[BeforeMessageSendHookHandler] | None = None, cache_control: CacheControlHeader | None = None, compression_config: CompressionConfig | None = None, cors_config: CORSConfig | None = None, csrf_config: CSRFConfig | None = None, debug: bool = True, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, event_emitter_backend: type[BaseEventEmitterBackend] = SimpleEventEmitter, exception_handlers: ExceptionHandlersMap | None = None, guards: Sequence[Guard] | None = None, include_in_schema: bool | EmptyType = Empty, listeners: Sequence[EventListener] | None = None, logging_config: BaseLoggingConfig | EmptyType | None = Empty, middleware: Sequence[Middleware] | None = None, multipart_form_part_limit: int = 1000, on_app_init: Sequence[OnAppInitHandler] | None = None, on_shutdown: Sequence[LifespanHook] | None = None, on_startup: Sequence[LifespanHook] | None = None, openapi_config: OpenAPIConfig | None = DEFAULT_OPENAPI_CONFIG, opt: Mapping[str, Any] | None = None, parameters: ParametersMap | None = None, path: str | None = None, plugins: Sequence[PluginProtocol] | None = None, lifespan: list[Callable[[Litestar], AbstractAsyncContextManager] | AbstractAsyncContextManager] | None = None, raise_server_exceptions: bool = True, pdb_on_exception: bool | None = None, request_class: type[Request] | None = None, response_cache_config: ResponseCacheConfig | None = None, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, root_path: str = "", security: Sequence[SecurityRequirement] | None = None, session_config: BaseBackendConfig | None = None, signature_namespace: Mapping[str, Any] | None = None, signature_types: Sequence[Any] | None = None, state: State | None = None, static_files_config: Sequence[StaticFilesConfig] | None = None, stores: StoreRegistry | dict[str, Store] | None = None, tags: Sequence[str] | None = None, template_config: TemplateConfig | None = None, timeout: float | None = None, type_encoders: TypeEncodersMap | None = None, websocket_class: type[WebSocket] | None = None, experimental_features: list[ExperimentalFeatures] | None = None, ) -> TestClient[Litestar]: """Create a Litestar app instance and initializes it. :class:`TestClient ` with it. Notes: - This function should be called as a context manager to ensure async startup and shutdown are handled correctly. Examples: .. code-block:: python from litestar import get from litestar.testing import create_test_client @get("/some-path") def my_handler() -> dict[str, str]: return {"hello": "world"} def test_my_handler() -> None: with create_test_client(my_handler) as client: response = client.get("/some-path") assert response.json() == {"hello": "world"} Args: route_handlers: A single handler or a sequence of route handlers, which can include instances of :class:`Router `, subclasses of :class:`Controller <.controller.Controller>` or any function decorated by the route handler decorators. backend: The async backend to use, options are "asyncio" or "trio". backend_options: ``anyio`` options. base_url: URL scheme and domain for test request paths, e.g. ``http://testserver``. raise_server_exceptions: Flag for underlying the test client to raise server exceptions instead of wrapping them in an HTTP response. root_path: Path prefix for requests. session_config: Configuration for Session Middleware class to create raw session cookies for request to the route handlers. after_exception: A sequence of :class:`exception hook handlers <.types.AfterExceptionHookHandler>`. This hook is called after an exception occurs. In difference to exception handlers, it is not meant to return a response - only to process the exception (e.g. log it, send it to Sentry etc.). after_request: A sync or async function executed after the route handler function returned and the response object has been resolved. Receives the response object. after_response: A sync or async function called after the response has been awaited. It receives the :class:`Request <.connection.Request>` object and should not return any values. allowed_hosts: A sequence of allowed hosts, or an :class:`AllowedHostsConfig <.config.allowed_hosts.AllowedHostsConfig>` instance. Enables the builtin allowed hosts middleware. before_request: A sync or async function called immediately before calling the route handler. Receives the :class:`Request <.connection.Request>` instance and any non-``None`` return value is used for the response, bypassing the route handler. before_send: A sequence of :class:`before send hook handlers <.types.BeforeMessageSendHookHandler>`. Called when the ASGI send function is called. cache_control: A ``cache-control`` header of type :class:`CacheControlHeader ` to add to route handlers of this app. Can be overridden by route handlers. compression_config: Configures compression behaviour of the application, this enabled a builtin or user defined Compression middleware. cors_config: If set, configures CORS handling for the application. csrf_config: If set, configures :class:`CSRFMiddleware <.middleware.csrf.CSRFMiddleware>`. debug: If ``True``, app errors rendered as HTML with a stack trace. dependencies: A string keyed mapping of dependency :class:`Providers <.di.Provide>`. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` to add to route handlers of this app. Can be overridden by route handlers. event_emitter_backend: A subclass of :class:`BaseEventEmitterBackend <.events.emitter.BaseEventEmitterBackend>`. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. lifespan: A list of callables returning async context managers, wrapping the lifespan of the ASGI application listeners: A sequence of :class:`EventListener <.events.listener.EventListener>`. logging_config: A subclass of :class:`BaseLoggingConfig <.logging.config.BaseLoggingConfig>`. middleware: A sequence of :class:`Middleware <.types.Middleware>`. multipart_form_part_limit: The maximal number of allowed parts in a multipart/formdata request. This limit is intended to protect from DoS attacks. on_app_init: A sequence of :class:`OnAppInitHandler <.types.OnAppInitHandler>` instances. Handlers receive an instance of :class:`AppConfig <.config.app.AppConfig>` that will have been initially populated with the parameters passed to :class:`Litestar `, and must return an instance of same. If more than one handler is registered they are called in the order they are provided. on_shutdown: A sequence of :class:`LifespanHook <.types.LifespanHook>` called during application shutdown. on_startup: A sequence of :class:`LifespanHook ` called during application startup. openapi_config: Defaults to :attr:`DEFAULT_OPENAPI_CONFIG` opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request ` or :class:`ASGI Scope <.types.Scope>`. parameters: A mapping of :class:`Parameter <.params.Parameter>` definitions available to all application paths. path: A path fragment that is prefixed to all route handlers, controllers and routers associated with the application instance. .. versionadded:: 2.8.0 pdb_on_exception: Drop into the PDB when an exception occurs. plugins: Sequence of plugins. request_class: An optional subclass of :class:`Request <.connection.Request>` to use for http connections. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as the app's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>`. response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` response_cache_config: Configures caching behavior of the application. return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. route_handlers: A sequence of route handlers, which can include instances of :class:`Router <.router.Router>`, subclasses of :class:`Controller <.controller.Controller>` or any callable decorated by the route handler decorators. security: A sequence of dicts that will be added to the schema of all route handlers in the application. See :data:`SecurityRequirement <.openapi.spec.SecurityRequirement>` for details. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. signature_types: A sequence of types for use in forward reference resolution during signature modelling. These types will be added to the signature namespace using their ``__name__`` attribute. state: An optional :class:`State <.datastructures.State>` for application state. static_files_config: A sequence of :class:`StaticFilesConfig <.static_files.StaticFilesConfig>` stores: Central registry of :class:`Store <.stores.base.Store>` that will be available throughout the application. If this is a dictionary to it will be passed to a :class:`StoreRegistry <.stores.registry.StoreRegistry>`. If it is a :class:`StoreRegistry <.stores.registry.StoreRegistry>`, this instance will be used directly. tags: A sequence of string tags that will be appended to the schema of all route handlers under the application. template_config: An instance of :class:`TemplateConfig <.template.TemplateConfig>` timeout: Request timeout type_encoders: A mapping of types to callables that transform them into types supported for serialization. websocket_class: An optional subclass of :class:`WebSocket <.connection.WebSocket>` to use for websocket connections. experimental_features: An iterable of experimental features to enable Returns: An instance of :class:`TestClient <.testing.TestClient>` with a created app instance. """ route_handlers = () if route_handlers is None else route_handlers if is_class_and_subclass(route_handlers, Controller) or not isinstance(route_handlers, Sequence): route_handlers = (route_handlers,) app = Litestar( after_exception=after_exception, after_request=after_request, after_response=after_response, allowed_hosts=allowed_hosts, before_request=before_request, before_send=before_send, cache_control=cache_control, compression_config=compression_config, cors_config=cors_config, csrf_config=csrf_config, debug=debug, dependencies=dependencies, dto=dto, etag=etag, lifespan=lifespan, event_emitter_backend=event_emitter_backend, exception_handlers=exception_handlers, guards=guards, include_in_schema=include_in_schema, listeners=listeners, logging_config=logging_config, middleware=middleware, multipart_form_part_limit=multipart_form_part_limit, on_app_init=on_app_init, on_shutdown=on_shutdown, on_startup=on_startup, openapi_config=openapi_config, opt=opt, parameters=parameters, path=path, pdb_on_exception=pdb_on_exception, plugins=plugins, request_class=request_class, response_cache_config=response_cache_config, response_class=response_class, response_cookies=response_cookies, response_headers=response_headers, return_dto=return_dto, route_handlers=route_handlers, security=security, signature_namespace=signature_namespace, signature_types=signature_types, state=state, static_files_config=static_files_config, stores=stores, tags=tags, template_config=template_config, type_encoders=type_encoders, websocket_class=websocket_class, experimental_features=experimental_features, ) return TestClient[Litestar]( app=app, backend=backend, backend_options=backend_options, base_url=base_url, raise_server_exceptions=raise_server_exceptions, root_path=root_path, session_config=session_config, timeout=timeout, ) def create_async_test_client( route_handlers: ControllerRouterHandler | Sequence[ControllerRouterHandler] | None = None, *, after_exception: Sequence[AfterExceptionHookHandler] | None = None, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, allowed_hosts: Sequence[str] | AllowedHostsConfig | None = None, backend: Literal["asyncio", "trio"] = "asyncio", backend_options: Mapping[str, Any] | None = None, base_url: str = "http://testserver.local", before_request: BeforeRequestHookHandler | None = None, before_send: Sequence[BeforeMessageSendHookHandler] | None = None, cache_control: CacheControlHeader | None = None, compression_config: CompressionConfig | None = None, cors_config: CORSConfig | None = None, csrf_config: CSRFConfig | None = None, debug: bool = True, dependencies: Dependencies | None = None, dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, event_emitter_backend: type[BaseEventEmitterBackend] = SimpleEventEmitter, exception_handlers: ExceptionHandlersMap | None = None, guards: Sequence[Guard] | None = None, include_in_schema: bool | EmptyType = Empty, lifespan: list[Callable[[Litestar], AbstractAsyncContextManager] | AbstractAsyncContextManager] | None = None, listeners: Sequence[EventListener] | None = None, logging_config: BaseLoggingConfig | EmptyType | None = Empty, middleware: Sequence[Middleware] | None = None, multipart_form_part_limit: int = 1000, on_app_init: Sequence[OnAppInitHandler] | None = None, on_shutdown: Sequence[LifespanHook] | None = None, on_startup: Sequence[LifespanHook] | None = None, openapi_config: OpenAPIConfig | None = DEFAULT_OPENAPI_CONFIG, opt: Mapping[str, Any] | None = None, parameters: ParametersMap | None = None, pdb_on_exception: bool | None = None, path: str | None = None, plugins: Sequence[PluginProtocol] | None = None, raise_server_exceptions: bool = True, request_class: type[Request] | None = None, response_cache_config: ResponseCacheConfig | None = None, response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, root_path: str = "", security: Sequence[SecurityRequirement] | None = None, session_config: BaseBackendConfig | None = None, signature_namespace: Mapping[str, Any] | None = None, signature_types: Sequence[Any] | None = None, state: State | None = None, static_files_config: Sequence[StaticFilesConfig] | None = None, stores: StoreRegistry | dict[str, Store] | None = None, tags: Sequence[str] | None = None, template_config: TemplateConfig | None = None, timeout: float | None = None, type_encoders: TypeEncodersMap | None = None, websocket_class: type[WebSocket] | None = None, experimental_features: list[ExperimentalFeatures] | None = None, ) -> AsyncTestClient[Litestar]: """Create a Litestar app instance and initializes it. :class:`AsyncTestClient ` with it. Notes: - This function should be called as a context manager to ensure async startup and shutdown are handled correctly. Examples: .. code-block:: python from litestar import get from litestar.testing import create_async_test_client @get("/some-path") def my_handler() -> dict[str, str]: return {"hello": "world"} async def test_my_handler() -> None: async with create_async_test_client(my_handler) as client: response = await client.get("/some-path") assert response.json() == {"hello": "world"} Args: route_handlers: A single handler or a sequence of route handlers, which can include instances of :class:`Router `, subclasses of :class:`Controller <.controller.Controller>` or any function decorated by the route handler decorators. backend: The async backend to use, options are "asyncio" or "trio". backend_options: ``anyio`` options. base_url: URL scheme and domain for test request paths, e.g. ``http://testserver``. raise_server_exceptions: Flag for underlying the test client to raise server exceptions instead of wrapping them in an HTTP response. root_path: Path prefix for requests. session_config: Configuration for Session Middleware class to create raw session cookies for request to the route handlers. after_exception: A sequence of :class:`exception hook handlers <.types.AfterExceptionHookHandler>`. This hook is called after an exception occurs. In difference to exception handlers, it is not meant to return a response - only to process the exception (e.g. log it, send it to Sentry etc.). after_request: A sync or async function executed after the route handler function returned and the response object has been resolved. Receives the response object. after_response: A sync or async function called after the response has been awaited. It receives the :class:`Request <.connection.Request>` object and should not return any values. allowed_hosts: A sequence of allowed hosts, or an :class:`AllowedHostsConfig <.config.allowed_hosts.AllowedHostsConfig>` instance. Enables the builtin allowed hosts middleware. before_request: A sync or async function called immediately before calling the route handler. Receives the :class:`Request <.connection.Request>` instance and any non-``None`` return value is used for the response, bypassing the route handler. before_send: A sequence of :class:`before send hook handlers <.types.BeforeMessageSendHookHandler>`. Called when the ASGI send function is called. cache_control: A ``cache-control`` header of type :class:`CacheControlHeader ` to add to route handlers of this app. Can be overridden by route handlers. compression_config: Configures compression behaviour of the application, this enabled a builtin or user defined Compression middleware. cors_config: If set, configures CORS handling for the application. csrf_config: If set, configures :class:`CSRFMiddleware <.middleware.csrf.CSRFMiddleware>`. debug: If ``True``, app errors rendered as HTML with a stack trace. dependencies: A string keyed mapping of dependency :class:`Providers <.di.Provide>`. dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and validation of request data. etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` to add to route handlers of this app. Can be overridden by route handlers. event_emitter_backend: A subclass of :class:`BaseEventEmitterBackend <.events.emitter.BaseEventEmitterBackend>`. exception_handlers: A mapping of status codes and/or exception types to handler functions. guards: A sequence of :class:`Guard <.types.Guard>` callables. include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema. lifespan: A list of callables returning async context managers, wrapping the lifespan of the ASGI application listeners: A sequence of :class:`EventListener <.events.listener.EventListener>`. logging_config: A subclass of :class:`BaseLoggingConfig <.logging.config.BaseLoggingConfig>`. middleware: A sequence of :class:`Middleware <.types.Middleware>`. multipart_form_part_limit: The maximal number of allowed parts in a multipart/formdata request. This limit is intended to protect from DoS attacks. on_app_init: A sequence of :class:`OnAppInitHandler <.types.OnAppInitHandler>` instances. Handlers receive an instance of :class:`AppConfig <.config.app.AppConfig>` that will have been initially populated with the parameters passed to :class:`Litestar `, and must return an instance of same. If more than one handler is registered they are called in the order they are provided. on_shutdown: A sequence of :class:`LifespanHook <.types.LifespanHook>` called during application shutdown. on_startup: A sequence of :class:`LifespanHook ` called during application startup. openapi_config: Defaults to :attr:`DEFAULT_OPENAPI_CONFIG` opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or wherever you have access to :class:`Request ` or :class:`ASGI Scope <.types.Scope>`. parameters: A mapping of :class:`Parameter <.params.Parameter>` definitions available to all application paths. path: A path fragment that is prefixed to all route handlers, controllers and routers associated with the application instance. .. versionadded:: 2.8.0 pdb_on_exception: Drop into the PDB when an exception occurs. plugins: Sequence of plugins. request_class: An optional subclass of :class:`Request <.connection.Request>` to use for http connections. response_class: A custom subclass of :class:`Response <.response.Response>` to be used as the app's default response. response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>`. response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>` response_cache_config: Configures caching behavior of the application. return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing outbound response data. route_handlers: A sequence of route handlers, which can include instances of :class:`Router <.router.Router>`, subclasses of :class:`Controller <.controller.Controller>` or any callable decorated by the route handler decorators. security: A sequence of dicts that will be added to the schema of all route handlers in the application. See :data:`SecurityRequirement <.openapi.spec.SecurityRequirement>` for details. signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling. signature_types: A sequence of types for use in forward reference resolution during signature modelling. These types will be added to the signature namespace using their ``__name__`` attribute. state: An optional :class:`State <.datastructures.State>` for application state. static_files_config: A sequence of :class:`StaticFilesConfig <.static_files.StaticFilesConfig>` stores: Central registry of :class:`Store <.stores.base.Store>` that will be available throughout the application. If this is a dictionary to it will be passed to a :class:`StoreRegistry <.stores.registry.StoreRegistry>`. If it is a :class:`StoreRegistry <.stores.registry.StoreRegistry>`, this instance will be used directly. tags: A sequence of string tags that will be appended to the schema of all route handlers under the application. template_config: An instance of :class:`TemplateConfig <.template.TemplateConfig>` timeout: Request timeout type_encoders: A mapping of types to callables that transform them into types supported for serialization. websocket_class: An optional subclass of :class:`WebSocket <.connection.WebSocket>` to use for websocket connections. experimental_features: An iterable of experimental features to enable Returns: An instance of :class:`AsyncTestClient ` with a created app instance. """ route_handlers = () if route_handlers is None else route_handlers if is_class_and_subclass(route_handlers, Controller) or not isinstance(route_handlers, Sequence): route_handlers = (route_handlers,) app = Litestar( after_exception=after_exception, after_request=after_request, after_response=after_response, allowed_hosts=allowed_hosts, before_request=before_request, before_send=before_send, cache_control=cache_control, compression_config=compression_config, cors_config=cors_config, csrf_config=csrf_config, debug=debug, dependencies=dependencies, dto=dto, etag=etag, event_emitter_backend=event_emitter_backend, exception_handlers=exception_handlers, guards=guards, include_in_schema=include_in_schema, lifespan=lifespan, listeners=listeners, logging_config=logging_config, middleware=middleware, multipart_form_part_limit=multipart_form_part_limit, on_app_init=on_app_init, on_shutdown=on_shutdown, on_startup=on_startup, openapi_config=openapi_config, opt=opt, parameters=parameters, path=path, pdb_on_exception=pdb_on_exception, plugins=plugins, request_class=request_class, response_cache_config=response_cache_config, response_class=response_class, response_cookies=response_cookies, response_headers=response_headers, return_dto=return_dto, route_handlers=route_handlers, security=security, signature_namespace=signature_namespace, signature_types=signature_types, state=state, static_files_config=static_files_config, stores=stores, tags=tags, template_config=template_config, type_encoders=type_encoders, websocket_class=websocket_class, experimental_features=experimental_features, ) return AsyncTestClient[Litestar]( app=app, backend=backend, backend_options=backend_options, base_url=base_url, raise_server_exceptions=raise_server_exceptions, root_path=root_path, session_config=session_config, timeout=timeout, ) litestar-2.16.0/litestar/testing/life_span_handler.py000066400000000000000000000103021500564371300227420ustar00rootroot00000000000000from __future__ import annotations import warnings from math import inf from typing import TYPE_CHECKING, Generic, Optional, TypeVar, cast from anyio import create_memory_object_stream from anyio.streams.stapled import StapledObjectStream from litestar.testing.client.base import BaseTestClient if TYPE_CHECKING: from types import TracebackType from litestar.types import ( LifeSpanReceiveMessage, # noqa: F401 LifeSpanSendMessage, LifeSpanShutdownEvent, LifeSpanStartupEvent, ) T = TypeVar("T", bound=BaseTestClient) class LifeSpanHandler(Generic[T]): __slots__ = ( "_startup_done", "client", "stream_receive", "stream_send", "task", ) def __init__(self, client: T) -> None: self.client = client self.stream_send = StapledObjectStream[Optional["LifeSpanSendMessage"]](*create_memory_object_stream(inf)) # type: ignore[arg-type] self.stream_receive = StapledObjectStream["LifeSpanReceiveMessage"](*create_memory_object_stream(inf)) # type: ignore[arg-type] self._startup_done = False def _ensure_setup(self, is_safe: bool = False) -> None: if self._startup_done: return if not is_safe: warnings.warn( "LifeSpanHandler used with implicit startup; Use LifeSpanHandler as a context manager instead. " "Implicit startup will be deprecated in version 3.0.", category=DeprecationWarning, stacklevel=2, ) self._startup_done = True with self.client.portal() as portal: self.task = portal.start_task_soon(self.lifespan) portal.call(self.wait_startup) def close(self) -> None: with self.client.portal() as portal: portal.call(self.stream_send.aclose) portal.call(self.stream_receive.aclose) def __enter__(self) -> LifeSpanHandler: try: self._ensure_setup(is_safe=True) except Exception as exc: self.close() raise exc return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: self.close() async def receive(self) -> LifeSpanSendMessage: self._ensure_setup() message = await self.stream_send.receive() if message is None: self.task.result() return cast("LifeSpanSendMessage", message) async def wait_startup(self) -> None: self._ensure_setup() event: LifeSpanStartupEvent = {"type": "lifespan.startup"} await self.stream_receive.send(event) message = await self.receive() if message["type"] not in ( "lifespan.startup.complete", "lifespan.startup.failed", ): raise RuntimeError( "Received unexpected ASGI message type. Expected 'lifespan.startup.complete' or " f"'lifespan.startup.failed'. Got {message['type']!r}", ) if message["type"] == "lifespan.startup.failed": await self.receive() async def wait_shutdown(self) -> None: self._ensure_setup() async with self.stream_send: lifespan_shutdown_event: LifeSpanShutdownEvent = {"type": "lifespan.shutdown"} await self.stream_receive.send(lifespan_shutdown_event) message = await self.receive() if message["type"] not in ( "lifespan.shutdown.complete", "lifespan.shutdown.failed", ): raise RuntimeError( "Received unexpected ASGI message type. Expected 'lifespan.shutdown.complete' or " f"'lifespan.shutdown.failed'. Got {message['type']!r}", ) if message["type"] == "lifespan.shutdown.failed": await self.receive() async def lifespan(self) -> None: self._ensure_setup() scope = {"type": "lifespan"} try: await self.client.app(scope, self.stream_receive.receive, self.stream_send.send) finally: await self.stream_send.send(None) litestar-2.16.0/litestar/testing/request_factory.py000066400000000000000000000551331500564371300225370ustar00rootroot00000000000000from __future__ import annotations import json from functools import partial from typing import TYPE_CHECKING, Any, cast from urllib.parse import urlencode from httpx._content import encode_json as httpx_encode_json from httpx._content import encode_multipart_data, encode_urlencoded_data from litestar import delete, patch, post, put from litestar.app import Litestar from litestar.connection import Request from litestar.enums import HttpMethod, ParamType, RequestEncodingType, ScopeType from litestar.handlers.http_handlers import get from litestar.serialization import decode_json, default_serializer, encode_json from litestar.types import DataContainerType, HTTPScope, RouteHandlerType from litestar.types.asgi_types import ASGIVersion from litestar.utils import get_serializer_from_scope from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from httpx._types import FileTypes from litestar.datastructures.cookie import Cookie from litestar.handlers.http_handlers import HTTPRouteHandler _decorator_http_method_map: dict[HttpMethod, type[HTTPRouteHandler]] = { HttpMethod.GET: get, HttpMethod.POST: post, HttpMethod.DELETE: delete, HttpMethod.PATCH: patch, HttpMethod.PUT: put, } def _create_default_route_handler( http_method: HttpMethod, handler_kwargs: dict[str, Any] | None, app: Litestar ) -> HTTPRouteHandler: handler_decorator = _decorator_http_method_map[http_method] def _default_route_handler() -> None: ... handler = handler_decorator("/", sync_to_thread=False, **(handler_kwargs or {}))(_default_route_handler) handler.owner = app return handler def _create_default_app() -> Litestar: return Litestar(route_handlers=[]) class RequestFactory: """Factory to create :class:`Request ` instances.""" __slots__ = ( "app", "handler_kwargs", "port", "root_path", "scheme", "serializer", "server", ) def __init__( self, app: Litestar | None = None, server: str = "test.org", port: int = 3000, root_path: str = "", scheme: str = "http", handler_kwargs: dict[str, Any] | None = None, ) -> None: """Initialize ``RequestFactory`` Args: app: An instance of :class:`Litestar ` to set as ``request.scope["litestar_app"]``. server: The server's domain. port: The server's port. root_path: Root path for the server. scheme: Scheme for the server. handler_kwargs: Kwargs to pass to the route handler created for the request Examples: .. code-block:: python from litestar import Litestar from litestar.enums import RequestEncodingType from litestar.testing import RequestFactory from tests import PersonFactory my_app = Litestar(route_handlers=[]) my_server = "litestar.org" # Create a GET request query_params = {"id": 1} get_user_request = RequestFactory(app=my_app, server=my_server).get( "/person", query_params=query_params ) # Create a POST request new_person = PersonFactory.build() create_user_request = RequestFactory(app=my_app, server=my_server).post( "/person", data=person ) # Create a request with a special header headers = {"header1": "value1"} request_with_header = RequestFactory(app=my_app, server=my_server).get( "/person", query_params=query_params, headers=headers ) # Create a request with a media type request_with_media_type = RequestFactory(app=my_app, server=my_server).post( "/person", data=person, request_media_type=RequestEncodingType.MULTI_PART ) """ self.app = app if app is not None else _create_default_app() self.server = server self.port = port self.root_path = root_path self.scheme = scheme self.handler_kwargs = handler_kwargs self.serializer = partial(default_serializer, type_encoders=self.app.type_encoders) def _create_scope( self, path: str, http_method: HttpMethod, session: dict[str, Any] | None = None, user: Any = None, auth: Any = None, query_params: dict[str, str | list[str]] | None = None, state: dict[str, Any] | None = None, path_params: dict[str, str] | None = None, http_version: str | None = "1.1", route_handler: RouteHandlerType | None = None, ) -> HTTPScope: """Create the scope for the :class:`Request `. Args: path: The request's path. http_method: The request's HTTP method. session: A dictionary of session data. user: A value for `request.scope["user"]`. auth: A value for `request.scope["auth"]`. query_params: A dictionary of values from which the request's query will be generated. state: Arbitrary request state. path_params: A string keyed dictionary of path parameter values. http_version: HTTP version. Defaults to "1.1". route_handler: A route handler instance or method. If not provided a default handler is set. Returns: A dictionary that can be passed as a scope to the :class:`Request ` ctor. """ if session is None: session = {} if state is None: state = {} if path_params is None: path_params = {} return HTTPScope( type=ScopeType.HTTP, method=http_method.value, scheme=self.scheme, server=(self.server, self.port), root_path=self.root_path.rstrip("/"), path=path, headers=[], app=self.app, litestar_app=self.app, session=session, user=user, auth=auth, query_string=urlencode(query_params, doseq=True).encode() if query_params else b"", path_params=path_params, client=(self.server, self.port), state=state, asgi=ASGIVersion(spec_version="3.0", version="3.0"), http_version=http_version or "1.1", raw_path=path.encode("ascii"), route_handler=route_handler or _create_default_route_handler(http_method, self.handler_kwargs, app=self.app), extensions={}, path_template="", ) @classmethod def _create_cookie_header(cls, headers: dict[str, str], cookies: list[Cookie] | str | None = None) -> None: """Create the cookie header and add it to the ``headers`` dictionary. Args: headers: A dictionary of headers, the cookie header will be added to it. cookies: A string representing the cookie header or a list of "Cookie" instances. This value can include multiple cookies. """ if not cookies: return if isinstance(cookies, list): cookie_header = "; ".join(cookie.to_header(header="") for cookie in cookies) headers[ParamType.COOKIE] = cookie_header elif isinstance(cookies, str): headers[ParamType.COOKIE] = cookies def _build_headers( self, headers: dict[str, str] | None = None, cookies: list[Cookie] | str | None = None, ) -> list[tuple[bytes, bytes]]: """Build a list of encoded headers that can be passed to the request scope. Args: headers: A dictionary of headers. cookies: A string representing the cookie header or a list of "Cookie" instances. This value can include multiple cookies. Returns: A list of encoded headers that can be passed to the request scope. """ headers = headers or {} self._create_cookie_header(headers, cookies) return [ ((key.lower()).encode("latin-1", errors="ignore"), value.encode("latin-1", errors="ignore")) for key, value in headers.items() ] def _create_request_with_data( self, http_method: HttpMethod, path: str, headers: dict[str, str] | None = None, cookies: list[Cookie] | str | None = None, session: dict[str, Any] | None = None, user: Any = None, auth: Any = None, request_media_type: RequestEncodingType = RequestEncodingType.JSON, data: dict[str, Any] | DataContainerType | None = None, # pyright: ignore files: dict[str, FileTypes] | list[tuple[str, FileTypes]] | None = None, query_params: dict[str, str | list[str]] | None = None, state: dict[str, Any] | None = None, path_params: dict[str, str] | None = None, http_version: str | None = "1.1", route_handler: RouteHandlerType | None = None, ) -> Request[Any, Any, Any]: """Create a :class:`Request ` instance that has body (data) Args: http_method: The request's HTTP method. path: The request's path. headers: A dictionary of headers. cookies: A string representing the cookie header or a list of "Cookie" instances. This value can include multiple cookies. session: A dictionary of session data. user: A value for `request.scope["user"]` auth: A value for `request.scope["auth"]` request_media_type: The 'Content-Type' header of the request. data: A value for the request's body. Can be any supported serializable type. files: A dictionary of files to be sent with the request. query_params: A dictionary of values from which the request's query will be generated. state: Arbitrary request state. path_params: A string keyed dictionary of path parameter values. http_version: HTTP version. Defaults to "1.1". route_handler: A route handler instance or method. If not provided a default handler is set. Returns: A :class:`Request ` instance """ scope = self._create_scope( path=path, http_method=http_method, session=session, user=user, auth=auth, query_params=query_params, state=state, path_params=path_params, http_version=http_version, route_handler=route_handler, ) headers = headers or {} body = b"" if data: data = json.loads(encode_json(data, serializer=get_serializer_from_scope(scope))) if request_media_type == RequestEncodingType.JSON: encoding_headers, stream = httpx_encode_json(data) elif request_media_type == RequestEncodingType.MULTI_PART: encoding_headers, stream = encode_multipart_data( # type: ignore[assignment] cast("dict[str, Any]", data), files=files or [], boundary=None ) else: encoding_headers, stream = encode_urlencoded_data(decode_json(value=encode_json(data))) headers.update(encoding_headers) for chunk in stream: body += chunk scope_state = ScopeState.from_scope(scope) scope_state.body = body scope_state.exception_handlers = scope["route_handler"].resolve_exception_handlers() self._create_cookie_header(headers, cookies) scope["headers"] = self._build_headers(headers) return Request(scope=scope) def get( self, path: str = "/", headers: dict[str, str] | None = None, cookies: list[Cookie] | str | None = None, session: dict[str, Any] | None = None, user: Any = None, auth: Any = None, query_params: dict[str, str | list[str]] | None = None, state: dict[str, Any] | None = None, path_params: dict[str, str] | None = None, http_version: str | None = "1.1", route_handler: RouteHandlerType | None = None, ) -> Request[Any, Any, Any]: """Create a GET :class:`Request ` instance. Args: path: The request's path. headers: A dictionary of headers. cookies: A string representing the cookie header or a list of "Cookie" instances. This value can include multiple cookies. session: A dictionary of session data. user: A value for `request.scope["user"]`. auth: A value for `request.scope["auth"]`. query_params: A dictionary of values from which the request's query will be generated. state: Arbitrary request state. path_params: A string keyed dictionary of path parameter values. http_version: HTTP version. Defaults to "1.1". route_handler: A route handler instance or method. If not provided a default handler is set. Returns: A :class:`Request ` instance """ scope = self._create_scope( path=path, http_method=HttpMethod.GET, session=session, user=user, auth=auth, query_params=query_params, state=state, path_params=path_params, http_version=http_version, route_handler=route_handler, ) scope["headers"] = self._build_headers(headers, cookies) return Request(scope=scope) def post( self, path: str = "/", headers: dict[str, str] | None = None, cookies: list[Cookie] | str | None = None, session: dict[str, Any] | None = None, user: Any = None, auth: Any = None, request_media_type: RequestEncodingType = RequestEncodingType.JSON, data: dict[str, Any] | DataContainerType | None = None, # pyright: ignore query_params: dict[str, str | list[str]] | None = None, state: dict[str, Any] | None = None, path_params: dict[str, str] | None = None, http_version: str | None = "1.1", route_handler: RouteHandlerType | None = None, ) -> Request[Any, Any, Any]: """Create a POST :class:`Request ` instance. Args: path: The request's path. headers: A dictionary of headers. cookies: A string representing the cookie header or a list of "Cookie" instances. This value can include multiple cookies. session: A dictionary of session data. user: A value for `request.scope["user"]`. auth: A value for `request.scope["auth"]`. request_media_type: The 'Content-Type' header of the request. data: A value for the request's body. Can be any supported serializable type. query_params: A dictionary of values from which the request's query will be generated. state: Arbitrary request state. path_params: A string keyed dictionary of path parameter values. http_version: HTTP version. Defaults to "1.1". route_handler: A route handler instance or method. If not provided a default handler is set. Returns: A :class:`Request ` instance """ return self._create_request_with_data( auth=auth, cookies=cookies, data=data, headers=headers, http_method=HttpMethod.POST, path=path, query_params=query_params, request_media_type=request_media_type, session=session, user=user, state=state, path_params=path_params, http_version=http_version, route_handler=route_handler, ) def put( self, path: str = "/", headers: dict[str, str] | None = None, cookies: list[Cookie] | str | None = None, session: dict[str, Any] | None = None, user: Any = None, auth: Any = None, request_media_type: RequestEncodingType = RequestEncodingType.JSON, data: dict[str, Any] | DataContainerType | None = None, # pyright: ignore query_params: dict[str, str | list[str]] | None = None, state: dict[str, Any] | None = None, path_params: dict[str, str] | None = None, http_version: str | None = "1.1", route_handler: RouteHandlerType | None = None, ) -> Request[Any, Any, Any]: """Create a PUT :class:`Request ` instance. Args: path: The request's path. headers: A dictionary of headers. cookies: A string representing the cookie header or a list of "Cookie" instances. This value can include multiple cookies. session: A dictionary of session data. user: A value for `request.scope["user"]`. auth: A value for `request.scope["auth"]`. request_media_type: The 'Content-Type' header of the request. data: A value for the request's body. Can be any supported serializable type. query_params: A dictionary of values from which the request's query will be generated. state: Arbitrary request state. path_params: A string keyed dictionary of path parameter values. http_version: HTTP version. Defaults to "1.1". route_handler: A route handler instance or method. If not provided a default handler is set. Returns: A :class:`Request ` instance """ return self._create_request_with_data( auth=auth, cookies=cookies, data=data, headers=headers, http_method=HttpMethod.PUT, path=path, query_params=query_params, request_media_type=request_media_type, session=session, user=user, state=state, path_params=path_params, http_version=http_version, route_handler=route_handler, ) def patch( self, path: str = "/", headers: dict[str, str] | None = None, cookies: list[Cookie] | str | None = None, session: dict[str, Any] | None = None, user: Any = None, auth: Any = None, request_media_type: RequestEncodingType = RequestEncodingType.JSON, data: dict[str, Any] | DataContainerType | None = None, # pyright: ignore query_params: dict[str, str | list[str]] | None = None, state: dict[str, Any] | None = None, path_params: dict[str, str] | None = None, http_version: str | None = "1.1", route_handler: RouteHandlerType | None = None, ) -> Request[Any, Any, Any]: """Create a PATCH :class:`Request ` instance. Args: path: The request's path. headers: A dictionary of headers. cookies: A string representing the cookie header or a list of "Cookie" instances. This value can include multiple cookies. session: A dictionary of session data. user: A value for `request.scope["user"]`. auth: A value for `request.scope["auth"]`. request_media_type: The 'Content-Type' header of the request. data: A value for the request's body. Can be any supported serializable type. query_params: A dictionary of values from which the request's query will be generated. state: Arbitrary request state. path_params: A string keyed dictionary of path parameter values. http_version: HTTP version. Defaults to "1.1". route_handler: A route handler instance or method. If not provided a default handler is set. Returns: A :class:`Request ` instance """ return self._create_request_with_data( auth=auth, cookies=cookies, data=data, headers=headers, http_method=HttpMethod.PATCH, path=path, query_params=query_params, request_media_type=request_media_type, session=session, user=user, state=state, path_params=path_params, http_version=http_version, route_handler=route_handler, ) def delete( self, path: str = "/", headers: dict[str, str] | None = None, cookies: list[Cookie] | str | None = None, session: dict[str, Any] | None = None, user: Any = None, auth: Any = None, query_params: dict[str, str | list[str]] | None = None, state: dict[str, Any] | None = None, path_params: dict[str, str] | None = None, http_version: str | None = "1.1", route_handler: RouteHandlerType | None = None, ) -> Request[Any, Any, Any]: """Create a POST :class:`Request ` instance. Args: path: The request's path. headers: A dictionary of headers. cookies: A string representing the cookie header or a list of "Cookie" instances. This value can include multiple cookies. session: A dictionary of session data. user: A value for `request.scope["user"]`. auth: A value for `request.scope["auth"]`. query_params: A dictionary of values from which the request's query will be generated. state: Arbitrary request state. path_params: A string keyed dictionary of path parameter values. http_version: HTTP version. Defaults to "1.1". route_handler: A route handler instance or method. If not provided a default handler is set. Returns: A :class:`Request ` instance """ scope = self._create_scope( path=path, http_method=HttpMethod.DELETE, session=session, user=user, auth=auth, query_params=query_params, state=state, path_params=path_params, http_version=http_version, route_handler=route_handler, ) scope["headers"] = self._build_headers(headers, cookies) return Request(scope=scope) litestar-2.16.0/litestar/testing/transport.py000066400000000000000000000174341500564371300213560ustar00rootroot00000000000000from __future__ import annotations from io import BytesIO from types import GeneratorType from typing import TYPE_CHECKING, Any, Generic, TypedDict, TypeVar, Union, cast from urllib.parse import unquote from anyio import Event from httpx import ByteStream, Response from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR from litestar.testing.websocket_test_session import WebSocketTestSession if TYPE_CHECKING: from httpx import Request from litestar.testing.client import AsyncTestClient, TestClient from litestar.types import ( HTTPDisconnectEvent, HTTPRequestEvent, Message, Receive, ReceiveMessage, Send, WebSocketScope, ) T = TypeVar("T", bound=Union["AsyncTestClient", "TestClient"]) class ConnectionUpgradeExceptionError(Exception): def __init__(self, session: WebSocketTestSession) -> None: self.session = session class SendReceiveContext(TypedDict): request_complete: bool response_complete: Event raw_kwargs: dict[str, Any] response_started: bool template: str | None context: Any | None class TestClientTransport(Generic[T]): def __init__( self, client: T, raise_server_exceptions: bool = True, root_path: str = "", ) -> None: self.client = client self.raise_server_exceptions = raise_server_exceptions self.root_path = root_path @staticmethod def create_receive(request: Request, context: SendReceiveContext) -> Receive: async def receive() -> ReceiveMessage: if context["request_complete"]: if not context["response_complete"].is_set(): await context["response_complete"].wait() disconnect_event: HTTPDisconnectEvent = {"type": "http.disconnect"} return disconnect_event body = cast("Union[bytes, str, GeneratorType]", (request.read() or b"")) request_event: HTTPRequestEvent = {"type": "http.request", "body": b"", "more_body": False} if isinstance(body, GeneratorType): # pragma: no cover try: chunk = body.send(None) request_event["body"] = chunk if isinstance(chunk, bytes) else chunk.encode("utf-8") request_event["more_body"] = True except StopIteration: context["request_complete"] = True else: context["request_complete"] = True request_event["body"] = body if isinstance(body, bytes) else body.encode("utf-8") return request_event return receive @staticmethod def create_send(request: Request, context: SendReceiveContext) -> Send: async def send(message: Message) -> None: if message["type"] == "http.response.start": assert not context["response_started"], 'Received multiple "http.response.start" messages.' # noqa: S101 context["raw_kwargs"]["status_code"] = message["status"] context["raw_kwargs"]["headers"] = [ (k.decode("utf-8"), v.decode("utf-8")) for k, v in message.get("headers", []) ] context["response_started"] = True elif message["type"] == "http.response.body": assert context["response_started"], 'Received "http.response.body" without "http.response.start".' # noqa: S101 assert not context[ # noqa: S101 "response_complete" ].is_set(), 'Received "http.response.body" after response completed.' body = message.get("body", b"") more_body = message.get("more_body", False) if request.method != "HEAD": context["raw_kwargs"]["stream"].write(body) if not more_body: context["raw_kwargs"]["stream"].seek(0) context["response_complete"].set() elif message["type"] == "http.response.template": # type: ignore[comparison-overlap] # pragma: no cover context["template"] = message["template"] # type: ignore[unreachable] context["context"] = message["context"] return send def parse_request(self, request: Request) -> dict[str, Any]: scheme = request.url.scheme netloc = unquote(request.url.netloc.decode(encoding="ascii")) path = request.url.path raw_path = request.url.raw_path query = request.url.query.decode(encoding="ascii") default_port = 433 if scheme in {"https", "wss"} else 80 if ":" in netloc: host, port_string = netloc.split(":", 1) port = int(port_string) else: host = netloc port = default_port host_header = request.headers.pop("host", host if port == default_port else f"{host}:{port}") headers = [(k.lower().encode(), v.encode()) for k, v in (("host", host_header), *request.headers.items())] return { "type": "websocket" if scheme in {"ws", "wss"} else "http", "path": unquote(path), "raw_path": raw_path, "root_path": self.root_path, "scheme": scheme, "query_string": query.encode(), "headers": headers, "client": ("testclient", 50000), "server": (host, port), } def handle_request(self, request: Request) -> Response: scope = self.parse_request(request=request) if scope["type"] == "websocket": scope.update( subprotocols=[value.strip() for value in request.headers.get("sec-websocket-protocol", "").split(",")] ) session = WebSocketTestSession(client=self.client, scope=cast("WebSocketScope", scope)) # type: ignore[arg-type] raise ConnectionUpgradeExceptionError(session) scope.update(method=request.method, http_version="1.1", extensions={"http.response.template": {}}) raw_kwargs: dict[str, Any] = {"stream": BytesIO()} try: with self.client.portal() as portal: response_complete = portal.call(Event) context: SendReceiveContext = { "response_complete": response_complete, "request_complete": False, "raw_kwargs": raw_kwargs, "response_started": False, "template": None, "context": None, } portal.call( self.client.app, scope, self.create_receive(request=request, context=context), self.create_send(request=request, context=context), ) except BaseException as exc: if self.raise_server_exceptions: raise exc return Response( status_code=HTTP_500_INTERNAL_SERVER_ERROR, headers=[], stream=ByteStream(b""), request=request ) else: if not context["response_started"]: # pragma: no cover if self.raise_server_exceptions: assert context["response_started"], "TestClient did not receive any response." # noqa: S101 return Response( status_code=HTTP_500_INTERNAL_SERVER_ERROR, headers=[], stream=ByteStream(b""), request=request ) stream = ByteStream(raw_kwargs.pop("stream", BytesIO()).read()) response = Response(**raw_kwargs, stream=stream, request=request) setattr(response, "template", context["template"]) setattr(response, "context", context["context"]) return response async def handle_async_request(self, request: Request) -> Response: return self.handle_request(request=request) litestar-2.16.0/litestar/testing/websocket_test_session.py000066400000000000000000000206241500564371300241050ustar00rootroot00000000000000from __future__ import annotations from contextlib import ExitStack from queue import Queue from typing import TYPE_CHECKING, Any, Literal, cast from anyio import sleep from litestar.exceptions import WebSocketDisconnect from litestar.serialization import decode_json, decode_msgpack, encode_json, encode_msgpack from litestar.status_codes import WS_1000_NORMAL_CLOSURE if TYPE_CHECKING: from litestar.testing.client.sync_client import TestClient from litestar.types import ( WebSocketConnectEvent, WebSocketDisconnectEvent, WebSocketReceiveMessage, WebSocketScope, WebSocketSendMessage, ) __all__ = ("WebSocketTestSession",) class WebSocketTestSession: exit_stack: ExitStack def __init__( self, client: TestClient[Any], scope: WebSocketScope, ) -> None: self.client = client self.scope = scope self.accepted_subprotocol: str | None = None self.receive_queue: Queue[WebSocketReceiveMessage] = Queue() self.send_queue: Queue[WebSocketSendMessage | BaseException] = Queue() self.extra_headers: list[tuple[bytes, bytes]] | None = None def __enter__(self) -> WebSocketTestSession: self.exit_stack = ExitStack() portal = self.exit_stack.enter_context(self.client.portal()) try: portal.start_task_soon(self.do_asgi_call) event: WebSocketConnectEvent = {"type": "websocket.connect"} self.receive_queue.put(event) message = self.receive(timeout=self.client.timeout.read) self.accepted_subprotocol = cast("str | None", message.get("subprotocol", None)) self.extra_headers = cast("list[tuple[bytes, bytes]] | None", message.get("headers", None)) return self except Exception: self.exit_stack.close() raise def __exit__(self, *args: Any) -> None: try: self.close() finally: self.exit_stack.close() while not self.send_queue.empty(): message = self.send_queue.get() if isinstance(message, BaseException): raise message async def do_asgi_call(self) -> None: """The sub-thread in which the websocket session runs.""" async def receive() -> WebSocketReceiveMessage: while self.receive_queue.empty(): await sleep(0) return self.receive_queue.get() async def send(message: WebSocketSendMessage) -> None: if message["type"] == "websocket.accept": headers = message.get("headers", []) if headers: # type: ignore[truthy-iterable] headers_list = list(self.scope["headers"]) headers_list.extend(headers) self.scope["headers"] = headers_list subprotocols = cast("str | None", message.get("subprotocols")) if subprotocols: # pragma: no cover self.scope["subprotocols"].append(subprotocols) self.send_queue.put(message) try: await self.client.app(self.scope, receive, send) except BaseException as exc: self.send_queue.put(exc) raise def send(self, data: str | bytes, mode: Literal["text", "binary"] = "text", encoding: str = "utf-8") -> None: """Sends a "receive" event. This is the inverse of the ASGI send method. Args: data: Either a string or a byte string. mode: The key to use - ``text`` or ``bytes`` encoding: The encoding to use when encoding or decoding data. Returns: None. """ if mode == "text": data = data.decode(encoding) if isinstance(data, bytes) else data text_event: WebSocketReceiveMessage = {"type": "websocket.receive", "text": data} # type: ignore[assignment] self.receive_queue.put(text_event) else: data = data if isinstance(data, bytes) else data.encode(encoding) binary_event: WebSocketReceiveMessage = {"type": "websocket.receive", "bytes": data} # type: ignore[assignment] self.receive_queue.put(binary_event) def send_text(self, data: str, encoding: str = "utf-8") -> None: """Sends the data using the ``text`` key. Args: data: Data to send. encoding: Encoding to use. Returns: None """ self.send(data=data, encoding=encoding) def send_bytes(self, data: bytes, encoding: str = "utf-8") -> None: """Sends the data using the ``bytes`` key. Args: data: Data to send. encoding: Encoding to use. Returns: None """ self.send(data=data, mode="binary", encoding=encoding) def send_json(self, data: Any, mode: Literal["text", "binary"] = "text") -> None: """Sends the given data as JSON. Args: data: The data to send. mode: Either ``text`` or ``binary`` Returns: None """ self.send(encode_json(data), mode=mode) def send_msgpack(self, data: Any) -> None: """Sends the given data as MessagePack. Args: data: The data to send. Returns: None """ self.send(encode_msgpack(data), mode="binary") def close(self, code: int = WS_1000_NORMAL_CLOSURE) -> None: """Sends an 'websocket.disconnect' event. Args: code: status code for closing the connection. Returns: None. """ event: WebSocketDisconnectEvent = {"type": "websocket.disconnect", "code": code} self.receive_queue.put(event) def receive(self, block: bool = True, timeout: float | None = None) -> WebSocketSendMessage: """This is the base receive method. Args: block: Block until a message is received timeout: If ``block`` is ``True``, block at most ``timeout`` seconds Notes: - you can use one of the other receive methods to extract the data from the message. Returns: A websocket message. """ message = cast("WebSocketSendMessage", self.send_queue.get(block=block, timeout=timeout)) if isinstance(message, BaseException): raise message if message["type"] == "websocket.close": raise WebSocketDisconnect( detail=cast("str", message.get("reason", "")), code=message.get("code", WS_1000_NORMAL_CLOSURE), ) return message def receive_text(self, block: bool = True, timeout: float | None = None) -> str: """Receive data in ``text`` mode and return a string Args: block: Block until a message is received timeout: If ``block`` is ``True``, block at most ``timeout`` seconds Returns: A string value. """ message = self.receive(block=block, timeout=timeout) return cast("str", message.get("text", "")) def receive_bytes(self, block: bool = True, timeout: float | None = None) -> bytes: """Receive data in ``binary`` mode and return bytes Args: block: Block until a message is received timeout: If ``block`` is ``True``, block at most ``timeout`` seconds Returns: A string value. """ message = self.receive(block=block, timeout=timeout) return cast("bytes", message.get("bytes", b"")) def receive_json( self, mode: Literal["text", "binary"] = "text", block: bool = True, timeout: float | None = None ) -> Any: """Receive data in either ``text`` or ``binary`` mode and decode it as JSON. Args: mode: Either ``text`` or ``binary`` block: Block until a message is received timeout: If ``block`` is ``True``, block at most ``timeout`` seconds Returns: An arbitrary value """ message = self.receive(block=block, timeout=timeout) if mode == "text": return decode_json(cast("str", message.get("text", ""))) return decode_json(cast("bytes", message.get("bytes", b""))) def receive_msgpack(self, block: bool = True, timeout: float | None = None) -> Any: message = self.receive(block=block, timeout=timeout) return decode_msgpack(cast("bytes", message.get("bytes", b""))) litestar-2.16.0/litestar/types/000077500000000000000000000000001500564371300164265ustar00rootroot00000000000000litestar-2.16.0/litestar/types/__init__.py000066400000000000000000000101351500564371300205370ustar00rootroot00000000000000from .asgi_types import ( ASGIApp, ASGIVersion, BaseScope, HTTPDisconnectEvent, HTTPReceiveMessage, HTTPRequestEvent, HTTPResponseBodyEvent, HTTPResponseStartEvent, HTTPScope, HTTPSendMessage, HTTPServerPushEvent, LifeSpanReceive, LifeSpanReceiveMessage, LifeSpanScope, LifeSpanSend, LifeSpanSendMessage, LifeSpanShutdownCompleteEvent, LifeSpanShutdownEvent, LifeSpanShutdownFailedEvent, LifeSpanStartupCompleteEvent, LifeSpanStartupEvent, LifeSpanStartupFailedEvent, Message, Method, Receive, ReceiveMessage, Scope, ScopeSession, Send, WebSocketAcceptEvent, WebSocketCloseEvent, WebSocketConnectEvent, WebSocketDisconnectEvent, WebSocketReceiveEvent, WebSocketReceiveMessage, WebSocketResponseBodyEvent, WebSocketResponseStartEvent, WebSocketScope, WebSocketSendEvent, WebSocketSendMessage, ) from .builtin_types import TypedDictClass from .callable_types import ( AfterExceptionHookHandler, AfterRequestHookHandler, AfterResponseHookHandler, AnyCallable, AnyGenerator, AsyncAnyCallable, BeforeMessageSendHookHandler, BeforeRequestHookHandler, CacheKeyBuilder, ExceptionHandler, GetLogger, Guard, LifespanHook, OnAppInitHandler, OperationIDCreator, Serializer, ) from .composite_types import ( Dependencies, ExceptionHandlersMap, Middleware, ParametersMap, PathType, ResponseCookies, ResponseHeaders, Scopes, TypeDecodersSequence, TypeEncodersMap, ) from .debugger_types import Debugger from .empty import Empty, EmptyType from .file_types import FileInfo, FileSystemProtocol from .helper_types import AnyIOBackend, MaybePartial, OptionalSequence, SSEData, StreamType, SyncOrAsyncUnion from .internal_types import ControllerRouterHandler, ReservedKwargs, RouteHandlerMapItem, RouteHandlerType from .protocols import DataclassProtocol, Logger from .serialization import DataContainerType, LitestarEncodableType __all__ = ( "ASGIApp", "ASGIVersion", "AfterExceptionHookHandler", "AfterRequestHookHandler", "AfterResponseHookHandler", "AnyCallable", "AnyGenerator", "AnyIOBackend", "AsyncAnyCallable", "BaseScope", "BeforeMessageSendHookHandler", "BeforeRequestHookHandler", "CacheKeyBuilder", "ControllerRouterHandler", "DataContainerType", "DataclassProtocol", "Debugger", "Dependencies", "Empty", "EmptyType", "ExceptionHandler", "ExceptionHandlersMap", "FileInfo", "FileSystemProtocol", "GetLogger", "Guard", "HTTPDisconnectEvent", "HTTPReceiveMessage", "HTTPRequestEvent", "HTTPResponseBodyEvent", "HTTPResponseStartEvent", "HTTPScope", "HTTPSendMessage", "HTTPServerPushEvent", "LifeSpanReceive", "LifeSpanReceiveMessage", "LifeSpanScope", "LifeSpanSend", "LifeSpanSendMessage", "LifeSpanShutdownCompleteEvent", "LifeSpanShutdownEvent", "LifeSpanShutdownFailedEvent", "LifeSpanStartupCompleteEvent", "LifeSpanStartupEvent", "LifeSpanStartupFailedEvent", "LifespanHook", "LitestarEncodableType", "Logger", "MaybePartial", "Message", "Method", "Middleware", "OnAppInitHandler", "OperationIDCreator", "OptionalSequence", "ParametersMap", "PathType", "Receive", "ReceiveMessage", "ReservedKwargs", "ResponseCookies", "ResponseHeaders", "RouteHandlerMapItem", "RouteHandlerType", "SSEData", "Scope", "ScopeSession", "Scopes", "Send", "Serializer", "StreamType", "SyncOrAsyncUnion", "TypeDecodersSequence", "TypeEncodersMap", "TypedDictClass", "WebSocketAcceptEvent", "WebSocketCloseEvent", "WebSocketConnectEvent", "WebSocketDisconnectEvent", "WebSocketReceiveEvent", "WebSocketReceiveMessage", "WebSocketReceiveMessage", "WebSocketResponseBodyEvent", "WebSocketResponseStartEvent", "WebSocketScope", "WebSocketSendEvent", "WebSocketSendMessage", "WebSocketSendMessage", ) litestar-2.16.0/litestar/types/asgi_types.py000066400000000000000000000215221500564371300211510ustar00rootroot00000000000000"""Includes code adapted from https://github.com/django/asgiref/blob/main/asgiref/typing.py. Copyright (c) Django Software Foundation and individual contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of Django nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ from __future__ import annotations from typing import ( TYPE_CHECKING, Any, Awaitable, Callable, Dict, Iterable, List, Literal, Tuple, TypedDict, Union, ) from litestar.enums import HttpMethod __all__ = ( "ASGIApp", "ASGIVersion", "BaseScope", "HTTPDisconnectEvent", "HTTPReceiveMessage", "HTTPRequestEvent", "HTTPResponseBodyEvent", "HTTPResponseStartEvent", "HTTPScope", "HTTPSendMessage", "HTTPServerPushEvent", "HeaderScope", "LifeSpanReceive", "LifeSpanReceiveMessage", "LifeSpanScope", "LifeSpanSend", "LifeSpanSendMessage", "LifeSpanShutdownCompleteEvent", "LifeSpanShutdownEvent", "LifeSpanShutdownFailedEvent", "LifeSpanStartupCompleteEvent", "LifeSpanStartupEvent", "LifeSpanStartupFailedEvent", "Message", "Method", "RawHeaders", "RawHeadersList", "Receive", "ReceiveMessage", "Scope", "ScopeSession", "Send", "WebSocketAcceptEvent", "WebSocketCloseEvent", "WebSocketConnectEvent", "WebSocketDisconnectEvent", "WebSocketMode", "WebSocketReceiveEvent", "WebSocketReceiveMessage", "WebSocketResponseBodyEvent", "WebSocketResponseStartEvent", "WebSocketScope", "WebSocketSendEvent", "WebSocketSendMessage", ) if TYPE_CHECKING: from typing_extensions import TypeAlias from litestar.app import Litestar from litestar.enums import ScopeType from litestar.types.empty import EmptyType from .internal_types import RouteHandlerType from .serialization import DataContainerType Method: TypeAlias = Union[Literal["GET", "POST", "DELETE", "PATCH", "PUT", "HEAD", "TRACE", "OPTIONS"], HttpMethod] ScopeSession: TypeAlias = "EmptyType | Dict[str, Any] | DataContainerType | None" class ASGIVersion(TypedDict): """ASGI spec version.""" spec_version: str version: Literal["3.0"] class HeaderScope(TypedDict): """Base class for ASGI-scopes that supports headers.""" headers: RawHeaders class BaseScope(HeaderScope): """Base ASGI-scope.""" app: Litestar # deprecated litestar_app: Litestar asgi: ASGIVersion auth: Any client: tuple[str, int] | None extensions: dict[str, dict[object, object]] | None http_version: str path: str path_params: dict[str, str] path_template: str query_string: bytes raw_path: bytes root_path: str route_handler: RouteHandlerType scheme: str server: tuple[str, int | None] | None session: ScopeSession state: dict[str, Any] user: Any class HTTPScope(BaseScope): """HTTP-ASGI-scope.""" method: Method type: Literal[ScopeType.HTTP] class WebSocketScope(BaseScope): """WebSocket-ASGI-scope.""" subprotocols: list[str] type: Literal[ScopeType.WEBSOCKET] class LifeSpanScope(TypedDict): """Lifespan-ASGI-scope.""" app: Litestar asgi: ASGIVersion type: Literal["lifespan"] class HTTPRequestEvent(TypedDict): """ASGI `http.request` event.""" type: Literal["http.request"] body: bytes more_body: bool class HTTPResponseStartEvent(HeaderScope): """ASGI `http.response.start` event.""" type: Literal["http.response.start"] status: int class HTTPResponseBodyEvent(TypedDict): """ASGI `http.response.body` event.""" type: Literal["http.response.body"] body: bytes more_body: bool class HTTPServerPushEvent(HeaderScope): """ASGI `http.response.push` event.""" type: Literal["http.response.push"] path: str class HTTPDisconnectEvent(TypedDict): """ASGI `http.disconnect` event.""" type: Literal["http.disconnect"] class WebSocketConnectEvent(TypedDict): """ASGI `websocket.connect` event.""" type: Literal["websocket.connect"] class WebSocketAcceptEvent(HeaderScope): """ASGI `websocket.accept` event.""" type: Literal["websocket.accept"] subprotocol: str | None class WebSocketReceiveEvent(TypedDict): """ASGI `websocket.receive` event.""" type: Literal["websocket.receive"] bytes: bytes | None text: str | None class WebSocketSendEvent(TypedDict): """ASGI `websocket.send` event.""" type: Literal["websocket.send"] bytes: bytes | None text: str | None class WebSocketResponseStartEvent(HeaderScope): """ASGI `websocket.http.response.start` event.""" type: Literal["websocket.http.response.start"] status: int class WebSocketResponseBodyEvent(TypedDict): """ASGI `websocket.http.response.body` event.""" type: Literal["websocket.http.response.body"] body: bytes more_body: bool class WebSocketDisconnectEvent(TypedDict): """ASGI `websocket.disconnect` event.""" type: Literal["websocket.disconnect"] code: int class WebSocketCloseEvent(TypedDict): """ASGI `websocket.close` event.""" type: Literal["websocket.close"] code: int reason: str | None class LifeSpanStartupEvent(TypedDict): """ASGI `lifespan.startup` event.""" type: Literal["lifespan.startup"] class LifeSpanShutdownEvent(TypedDict): """ASGI `lifespan.shutdown` event.""" type: Literal["lifespan.shutdown"] class LifeSpanStartupCompleteEvent(TypedDict): """ASGI `lifespan.startup.complete` event.""" type: Literal["lifespan.startup.complete"] class LifeSpanStartupFailedEvent(TypedDict): """ASGI `lifespan.startup.failed` event.""" type: Literal["lifespan.startup.failed"] message: str class LifeSpanShutdownCompleteEvent(TypedDict): """ASGI `lifespan.shutdown.complete` event.""" type: Literal["lifespan.shutdown.complete"] class LifeSpanShutdownFailedEvent(TypedDict): """ASGI `lifespan.shutdown.failed` event.""" type: Literal["lifespan.shutdown.failed"] message: str HTTPReceiveMessage: TypeAlias = Union[ HTTPRequestEvent, HTTPDisconnectEvent, ] WebSocketReceiveMessage: TypeAlias = Union[ WebSocketConnectEvent, WebSocketReceiveEvent, WebSocketDisconnectEvent, ] LifeSpanReceiveMessage: TypeAlias = Union[ LifeSpanStartupEvent, LifeSpanShutdownEvent, ] HTTPSendMessage: TypeAlias = Union[ HTTPResponseStartEvent, HTTPResponseBodyEvent, HTTPServerPushEvent, HTTPDisconnectEvent, ] WebSocketSendMessage: TypeAlias = Union[ WebSocketAcceptEvent, WebSocketSendEvent, WebSocketResponseStartEvent, WebSocketResponseBodyEvent, WebSocketCloseEvent, ] LifeSpanSendMessage: TypeAlias = Union[ LifeSpanStartupCompleteEvent, LifeSpanStartupFailedEvent, LifeSpanShutdownCompleteEvent, LifeSpanShutdownFailedEvent, ] LifeSpanReceive: TypeAlias = Callable[..., Awaitable[LifeSpanReceiveMessage]] LifeSpanSend: TypeAlias = Callable[[LifeSpanSendMessage], Awaitable[None]] Message: TypeAlias = Union[HTTPSendMessage, WebSocketSendMessage] ReceiveMessage: TypeAlias = Union[HTTPReceiveMessage, WebSocketReceiveMessage] Scope: TypeAlias = Union[HTTPScope, WebSocketScope] Receive: TypeAlias = Callable[..., Awaitable[Union[HTTPReceiveMessage, WebSocketReceiveMessage]]] Send: TypeAlias = Callable[[Message], Awaitable[None]] ASGIApp: TypeAlias = Callable[[Scope, Receive, Send], Awaitable[None]] RawHeaders: TypeAlias = Iterable[Tuple[bytes, bytes]] RawHeadersList: TypeAlias = List[Tuple[bytes, bytes]] WebSocketMode: TypeAlias = Literal["text", "binary"] litestar-2.16.0/litestar/types/builtin_types.py000066400000000000000000000010751500564371300216750ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Type, Union from typing_extensions import _TypedDictMeta # type: ignore[attr-defined] if TYPE_CHECKING: from typing_extensions import TypeAlias __all__ = ( "NoneType", "TypedDictClass", "UnionType", "UnionTypes", ) NoneType: type[None] = type(None) try: from types import UnionType # type: ignore[attr-defined] except ImportError: UnionType: TypeAlias = Union # type: ignore[no-redef] UnionTypes = {UnionType, Union} TypedDictClass: TypeAlias = Type[_TypedDictMeta] litestar-2.16.0/litestar/types/callable_types.py000066400000000000000000000042661500564371300217730ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Generator, TypeVar if TYPE_CHECKING: from typing_extensions import TypeAlias from litestar.app import Litestar from litestar.config.app import AppConfig from litestar.connection.base import ASGIConnection from litestar.connection.request import Request from litestar.handlers.base import BaseRouteHandler from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.response.base import Response from litestar.types.asgi_types import ASGIApp, Message, Method, Scope from litestar.types.helper_types import SyncOrAsyncUnion from litestar.types.internal_types import PathParameterDefinition from litestar.types.protocols import Logger ExceptionT = TypeVar("ExceptionT", bound=Exception) AfterExceptionHookHandler: TypeAlias = "Callable[[ExceptionT, Scope], SyncOrAsyncUnion[None]]" AfterRequestHookHandler: TypeAlias = ( "Callable[[ASGIApp], SyncOrAsyncUnion[ASGIApp]] | Callable[[Response], SyncOrAsyncUnion[Response]]" ) AfterResponseHookHandler: TypeAlias = "Callable[[Request], SyncOrAsyncUnion[None]]" AsyncAnyCallable: TypeAlias = Callable[..., Awaitable[Any]] AnyCallable: TypeAlias = Callable[..., Any] AnyGenerator: TypeAlias = "Generator[Any, Any, Any] | AsyncGenerator[Any, Any]" BeforeMessageSendHookHandler: TypeAlias = "Callable[[Message, Scope], SyncOrAsyncUnion[None]]" BeforeRequestHookHandler: TypeAlias = "Callable[[Request], Any | Awaitable[Any]]" CacheKeyBuilder: TypeAlias = "Callable[[Request], str]" ExceptionHandler: TypeAlias = "Callable[[Request, ExceptionT], Response]" ExceptionLoggingHandler: TypeAlias = "Callable[[Logger, Scope, list[str]], None]" GetLogger: TypeAlias = "Callable[..., Logger]" Guard: TypeAlias = "Callable[[ASGIConnection, BaseRouteHandler], SyncOrAsyncUnion[None]]" LifespanHook: TypeAlias = "Callable[[Litestar], SyncOrAsyncUnion[Any]] | Callable[[], SyncOrAsyncUnion[Any]]" OnAppInitHandler: TypeAlias = "Callable[[AppConfig], AppConfig]" OperationIDCreator: TypeAlias = "Callable[[HTTPRouteHandler, Method, list[str | PathParameterDefinition]], str]" Serializer: TypeAlias = Callable[[Any], Any] litestar-2.16.0/litestar/types/composite_types.py000066400000000000000000000033671500564371300222370ustar00rootroot00000000000000from __future__ import annotations from typing import ( TYPE_CHECKING, Any, Callable, Dict, Iterator, Literal, Mapping, MutableMapping, Sequence, Tuple, Type, Union, ) __all__ = ( "Dependencies", "ExceptionHandlersMap", "Middleware", "ParametersMap", "PathType", "ResponseCookies", "ResponseHeaders", "Scopes", "TypeEncodersMap", ) if TYPE_CHECKING: from os import PathLike from pathlib import Path from typing_extensions import TypeAlias from litestar.datastructures.cookie import Cookie from litestar.datastructures.response_header import ResponseHeader from litestar.di import Provide from litestar.enums import ScopeType from litestar.middleware.base import DefineMiddleware, MiddlewareProtocol from litestar.params import ParameterKwarg from .asgi_types import ASGIApp from .callable_types import AnyCallable, ExceptionHandler Dependencies: TypeAlias = "Mapping[str, Union[Provide, AnyCallable]]" ExceptionHandlersMap: TypeAlias = "MutableMapping[Union[int, Type[Exception]], ExceptionHandler]" Middleware: TypeAlias = "Union[Callable[..., ASGIApp], DefineMiddleware, Iterator[Tuple[ASGIApp, Dict[str, Any]]], Type[MiddlewareProtocol]]" ParametersMap: TypeAlias = "Mapping[str, ParameterKwarg]" PathType: TypeAlias = "Union[Path, PathLike, str]" ResponseCookies: TypeAlias = "Union[Sequence[Cookie], Mapping[str, str]]" ResponseHeaders: TypeAlias = "Union[Sequence[ResponseHeader], Mapping[str, str]]" Scopes: TypeAlias = "set[Literal[ScopeType.HTTP, ScopeType.WEBSOCKET]]" TypeDecodersSequence: TypeAlias = "Sequence[tuple[Callable[[Any], bool], Callable[[Any, Any], Any]]]" TypeEncodersMap: TypeAlias = "Mapping[Any, Callable[[Any], Any]]" litestar-2.16.0/litestar/types/debugger_types.py000066400000000000000000000005641500564371300220150ustar00rootroot00000000000000from types import ModuleType, TracebackType from typing import Any, Optional, Protocol, Union from typing_extensions import TypeAlias class PDBProtocol(Protocol): @staticmethod def post_mortem( traceback: Optional[TracebackType] = None, *args: Any, **kwargs: Any, ) -> Any: ... Debugger: TypeAlias = Union[ModuleType, PDBProtocol] litestar-2.16.0/litestar/types/empty.py000066400000000000000000000004361500564371300201410ustar00rootroot00000000000000from __future__ import annotations __all__ = ("Empty", "EmptyType") from enum import Enum from typing import Final, Literal class _EmptyEnum(Enum): """A sentinel enum used as placeholder.""" EMPTY = 0 EmptyType = Literal[_EmptyEnum.EMPTY] Empty: Final = _EmptyEnum.EMPTY litestar-2.16.0/litestar/types/file_types.py000066400000000000000000000050731500564371300211500ustar00rootroot00000000000000from __future__ import annotations from typing import ( IO, TYPE_CHECKING, Any, AnyStr, Awaitable, Literal, Protocol, TypedDict, overload, ) __all__ = ("FileInfo", "FileSystemProtocol") if TYPE_CHECKING: from _typeshed import OpenBinaryMode, OpenTextMode from anyio import AsyncFile from typing_extensions import NotRequired from litestar.types.composite_types import PathType class FileInfo(TypedDict): """File information gathered from a file system.""" created: float """Created time stamp, equal to 'stat_result.st_ctime'.""" destination: NotRequired[bytes | None] """Output of loading a symbolic link.""" gid: int """Group ID of owner.""" ino: int """inode value.""" islink: bool """True if the file is a symbolic link.""" mode: int """Protection mode.""" mtime: float """Modified time stamp.""" name: str """The path of the file.""" nlink: int """Number of hard links.""" size: int """Total size, in bytes.""" type: Literal["file", "directory", "other"] """The type of the file system object.""" uid: int """User ID of owner.""" class FileSystemProtocol(Protocol): """Base protocol used to interact with a file-system. This protocol is commensurable with the file systems exported by the `fsspec ` library. """ def info(self, path: PathType, **kwargs: Any) -> FileInfo | Awaitable[FileInfo]: """Retrieve information about a given file path. Args: path: A file path. **kwargs: Any additional kwargs. Returns: A dictionary of file info. """ ... @overload def open( self, file: PathType, mode: OpenBinaryMode, buffering: int = -1, ) -> IO[bytes] | Awaitable[AsyncFile[bytes]]: ... @overload def open( self, file: PathType, mode: OpenTextMode, buffering: int = -1, ) -> IO[str] | Awaitable[AsyncFile[str]]: ... def open( # pyright: ignore self, file: PathType, mode: str, buffering: int = -1, ) -> IO[AnyStr] | Awaitable[AsyncFile[AnyStr]]: """Return a file-like object from the filesystem. Notes: - The return value must function correctly in a context ``with`` block. Args: file: Path to the target file. mode: Mode, similar to the built ``open``. buffering: Buffer size. """ ... litestar-2.16.0/litestar/types/helper_types.py000066400000000000000000000021531500564371300215040ustar00rootroot00000000000000from __future__ import annotations from functools import partial from typing import ( TYPE_CHECKING, Any, AsyncIterable, AsyncIterator, Awaitable, Dict, Iterable, Iterator, Literal, Optional, Sequence, TypeVar, Union, ) if TYPE_CHECKING: from typing_extensions import TypeAlias from litestar.response.sse import ServerSentEventMessage T = TypeVar("T") __all__ = ("AnyIOBackend", "MaybePartial", "OptionalSequence", "SSEData", "StreamType", "SyncOrAsyncUnion") OptionalSequence: TypeAlias = Optional[Sequence[T]] """Types 'T' as union of Sequence[T] and None.""" SyncOrAsyncUnion: TypeAlias = Union[T, Awaitable[T]] """Types 'T' as a union of T and awaitable T.""" AnyIOBackend: TypeAlias = Literal["asyncio", "trio"] """Anyio backend names.""" StreamType: TypeAlias = Union[Iterable[T], Iterator[T], AsyncIterable[T], AsyncIterator[T]] """A stream type.""" MaybePartial: TypeAlias = Union[T, partial] """A potentially partial callable.""" SSEData: TypeAlias = Union[int, str, bytes, Dict[str, Any], "ServerSentEventMessage"] """A type alias for SSE data.""" litestar-2.16.0/litestar/types/internal_types.py000066400000000000000000000035501500564371300220430ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable, Literal, NamedTuple from litestar.utils.deprecation import warn_deprecation __all__ = ( "ControllerRouterHandler", "PathParameterDefinition", "PathParameterDefinition", "ReservedKwargs", "RouteHandlerMapItem", "RouteHandlerType", ) if TYPE_CHECKING: from typing_extensions import TypeAlias from litestar.app import Litestar from litestar.controller import Controller from litestar.handlers.asgi_handlers import ASGIRouteHandler from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.handlers.websocket_handlers import WebsocketRouteHandler from litestar.router import Router from litestar.template import TemplateConfig from litestar.template.config import EngineType from litestar.types import Method ReservedKwargs: TypeAlias = Literal["request", "socket", "headers", "query", "cookies", "state", "data"] RouteHandlerType: TypeAlias = "HTTPRouteHandler | WebsocketRouteHandler | ASGIRouteHandler" ControllerRouterHandler: TypeAlias = "type[Controller] | RouteHandlerType | Router | Callable[..., Any]" RouteHandlerMapItem: TypeAlias = 'dict[Method | Literal["websocket", "asgi"], RouteHandlerType]' TemplateConfigType: TypeAlias = "TemplateConfig[EngineType]" # deprecated _LitestarType: TypeAlias = "Litestar" class PathParameterDefinition(NamedTuple): """Path parameter tuple.""" name: str full: str type: type parser: Callable[[str], Any] | None def __getattr__(name: str) -> Any: if name == "LitestarType": warn_deprecation( "2.2.1", "LitestarType", "import", removal_in="3.0.0", alternative="Litestar", ) return _LitestarType raise AttributeError(f"module {__name__!r} has no attribute {name!r}") litestar-2.16.0/litestar/types/protocols.py000066400000000000000000000056141500564371300210320ustar00rootroot00000000000000from __future__ import annotations from typing import Any, ClassVar, Collection, Iterable, Protocol, TypeVar, runtime_checkable __all__ = ( "DataclassProtocol", "InstantiableCollection", "Logger", ) class Logger(Protocol): """Logger protocol.""" def debug(self, event: str, *args: Any, **kwargs: Any) -> Any: """Output a log message at 'DEBUG' level. Args: event: Log message. *args: Any args. **kwargs: Any kwargs. """ def info(self, event: str, *args: Any, **kwargs: Any) -> Any: """Output a log message at 'INFO' level. Args: event: Log message. *args: Any args. **kwargs: Any kwargs. """ def warning(self, event: str, *args: Any, **kwargs: Any) -> Any: """Output a log message at 'WARNING' level. Args: event: Log message. *args: Any args. **kwargs: Any kwargs. """ def warn(self, event: str, *args: Any, **kwargs: Any) -> Any: """Output a log message at 'WARN' level. Args: event: Log message. *args: Any args. **kwargs: Any kwargs. """ def error(self, event: str, *args: Any, **kwargs: Any) -> Any: """Output a log message at 'ERROR' level. Args: event: Log message. *args: Any args. **kwargs: Any kwargs. """ def fatal(self, event: str, *args: Any, **kwargs: Any) -> Any: """Output a log message at 'FATAL' level. Args: event: Log message. *args: Any args. **kwargs: Any kwargs. """ def exception(self, event: str, *args: Any, **kwargs: Any) -> Any: """Log a message with level 'ERROR' on this logger. The arguments are interpreted as for debug(). Exception info is added to the logging message. Args: event: Log message. *args: Any args. **kwargs: Any kwargs. """ def critical(self, event: str, *args: Any, **kwargs: Any) -> Any: """Output a log message at 'INFO' level. Args: event: Log message. *args: Any args. **kwargs: Any kwargs. """ def setLevel(self, level: int) -> None: # noqa: N802 """Set the log level Args: level: Log level to set as an integer Returns: None """ @runtime_checkable class DataclassProtocol(Protocol): """Protocol for instance checking dataclasses""" __dataclass_fields__: ClassVar[dict[str, Any]] T_co = TypeVar("T_co", covariant=True) @runtime_checkable class InstantiableCollection(Collection[T_co], Protocol[T_co]): # pyright: ignore """A protocol for instantiable collection types.""" def __init__(self, iterable: Iterable[T_co], /) -> None: ... litestar-2.16.0/litestar/types/serialization.py000066400000000000000000000046751500564371300216710ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Dict, Set if TYPE_CHECKING: from collections import deque from collections.abc import Collection from datetime import date, datetime, time from decimal import Decimal from enum import Enum, IntEnum from ipaddress import ( IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network, ) from pathlib import Path, PurePath from re import Pattern from uuid import UUID from msgspec import Raw, Struct from msgspec.msgpack import Ext from typing_extensions import TypeAlias from litestar.types import DataclassProtocol, TypedDictClass try: from pydantic import BaseModel from pydantic.main import IncEx from pydantic.typing import AbstractSetIntStr, MappingIntStrAny except ImportError: BaseModel = Any # type: ignore[assignment, misc] IncEx = Any # type: ignore[misc] AbstractSetIntStr = Any MappingIntStrAny = Any try: from attrs import AttrsInstance except ImportError: AttrsInstance = Any # type: ignore[assignment, misc] __all__ = ( "DataContainerType", "EncodableBuiltinCollectionType", "EncodableBuiltinType", "EncodableMsgSpecType", "EncodableStdLibIPType", "EncodableStdLibType", "LitestarEncodableType", ) EncodableBuiltinType: TypeAlias = "None | bool | int | float | str | bytes | bytearray" EncodableBuiltinCollectionType: TypeAlias = "list | tuple | set | frozenset | dict | Collection" EncodableStdLibType: TypeAlias = ( "date | datetime | deque | time | UUID | Decimal | Enum | IntEnum | DataclassProtocol | Path | PurePath | Pattern" ) EncodableStdLibIPType: TypeAlias = ( "IPv4Address | IPv4Interface | IPv4Network | IPv6Address | IPv6Interface | IPv6Network" ) EncodableMsgSpecType: TypeAlias = "Ext | Raw | Struct" LitestarEncodableType: TypeAlias = "EncodableBuiltinType | EncodableBuiltinCollectionType | EncodableStdLibType | EncodableStdLibIPType | EncodableMsgSpecType | BaseModel | AttrsInstance" # pyright: ignore DataContainerType: TypeAlias = "Struct | BaseModel | AttrsInstance | TypedDictClass | DataclassProtocol" # pyright: ignore PydanticV2FieldsListType: TypeAlias = "Set[int] | Set[str] | Dict[int, Any] | Dict[str, Any]" PydanticV1FieldsListType: TypeAlias = "IncEx | AbstractSetIntStr | MappingIntStrAny" # pyright: ignore litestar-2.16.0/litestar/typing.py000066400000000000000000000543211500564371300171530ustar00rootroot00000000000000from __future__ import annotations import dataclasses import warnings from collections import abc from copy import deepcopy from dataclasses import dataclass, is_dataclass, replace from enum import Enum from inspect import Parameter, Signature from typing import Any, AnyStr, Callable, Collection, ForwardRef, Literal, Mapping, TypeVar, cast from litestar.types import Empty try: import annotated_types except ImportError: annotated_types = Empty # type: ignore[assignment] from msgspec import UnsetType from typing_extensions import ( NewType, NotRequired, Required, Self, get_args, get_origin, get_type_hints, is_typeddict, ) from typing_extensions import ( TypeAliasType as TeTypeAliasType, ) try: from typing import TypeAliasType # type: ignore[attr-defined] TypeAliasTypes = (TypeAliasType, TeTypeAliasType) except ImportError: TypeAliasTypes = (TeTypeAliasType,) # type: ignore[assignment] from litestar.exceptions import ImproperlyConfiguredException, LitestarWarning from litestar.params import BodyKwarg, DependencyKwarg, KwargDefinition, ParameterKwarg from litestar.types.builtin_types import NoneType, UnionTypes from litestar.utils.predicates import ( is_any, is_class_and_subclass, is_generic, is_non_string_iterable, is_non_string_sequence, is_union, ) from litestar.utils.typing import ( get_instantiable_origin, get_safe_generic_origin, get_type_hints_with_generics_resolved, make_non_optional_union, unwrap_annotation, ) __all__ = ("FieldDefinition",) T = TypeVar("T", bound=KwargDefinition) def _annotated_types_extractor(meta: Any, is_sequence_container: bool) -> dict[str, Any]: # noqa: C901 if annotated_types is Empty: # type: ignore[comparison-overlap] # pragma: no branch return {} # type: ignore[unreachable] # pragma: no cover kwargs = {} if isinstance(meta, annotated_types.GroupedMetadata): for sub_meta in meta: kwargs.update(_annotated_types_extractor(sub_meta, is_sequence_container=is_sequence_container)) return kwargs if isinstance(meta, annotated_types.Gt): kwargs["gt"] = meta.gt elif isinstance(meta, annotated_types.Ge): kwargs["ge"] = meta.ge elif isinstance(meta, annotated_types.Lt): kwargs["lt"] = meta.lt elif isinstance(meta, annotated_types.Le): kwargs["le"] = meta.le elif isinstance(meta, annotated_types.MultipleOf): kwargs["multiple_of"] = meta.multiple_of elif isinstance(meta, annotated_types.MinLen): if is_sequence_container: kwargs["min_items"] = meta.min_length else: kwargs["min_length"] = meta.min_length elif isinstance(meta, annotated_types.MaxLen): if is_sequence_container: kwargs["max_items"] = meta.max_length else: kwargs["max_length"] = meta.max_length elif isinstance(meta, annotated_types.Predicate): if meta.func == str.islower: kwargs["lower_case"] = True elif meta.func == str.isupper: kwargs["upper_case"] = True elif meta.func == str.isascii: kwargs["pattern"] = "[[:ascii:]]" elif meta.func == str.isdigit: # pragma: no cover # coverage quirk: It expects a jump here for branch coverage kwargs["pattern"] = "[[:digit:]]" return kwargs @dataclass(frozen=True) class FieldDefinition: """Represents a function parameter or type annotation.""" __slots__ = ( "annotation", "args", "default", "extra", "inner_types", "instantiable_origin", "kwarg_definition", "metadata", "name", "origin", "raw", "safe_generic_origin", "type_wrappers", ) raw: Any """The annotation exactly as received.""" annotation: Any """The annotation with any "wrapper" types removed, e.g. Annotated.""" type_wrappers: tuple[type, ...] """A set of all "wrapper" types, e.g. Annotated.""" origin: Any """The result of calling ``get_origin(annotation)`` after unwrapping Annotated, e.g. list.""" args: tuple[Any, ...] """The result of calling ``get_args(annotation)`` after unwrapping Annotated, e.g. (int,).""" metadata: tuple[Any, ...] """Any metadata associated with the annotation via ``Annotated``.""" instantiable_origin: Any """An equivalent type to ``origin`` that can be safely instantiated. E.g., ``Sequence`` -> ``list``.""" safe_generic_origin: Any """An equivalent type to ``origin`` that can be safely used as a generic type across all supported Python versions. This is to serve safely rebuilding a generic outer type with different args at runtime. """ inner_types: tuple[FieldDefinition, ...] """The type's generic args parsed as ``FieldDefinition``, if applicable.""" default: Any """Default value of the field.""" extra: dict[str, Any] """A mapping of extra values.""" kwarg_definition: KwargDefinition | DependencyKwarg | None """Kwarg Parameter.""" name: str """Field name.""" def __deepcopy__(self, memo: dict[str, Any]) -> Self: return type(self)(**{attr: deepcopy(getattr(self, attr)) for attr in self.__slots__}) def __eq__(self, other: Any) -> bool: if not isinstance(other, FieldDefinition): return False if self.origin: return self.origin == other.origin and self.inner_types == other.inner_types return self.annotation == other.annotation # type: ignore[no-any-return] def __hash__(self) -> int: return hash((self.name, self.raw, self.annotation, self.origin, self.inner_types)) @property def has_default(self) -> bool: """Check if the field has a default value. Returns: True if the default is not Empty or Ellipsis otherwise False. """ return self.default is not Empty and self.default is not Ellipsis @property def is_non_string_iterable(self) -> bool: """Check if the field type is an Iterable. If ``self.annotation`` is an optional union, only the non-optional members of the union are evaluated. See: https://github.com/litestar-org/litestar/issues/1106 """ annotation = self.annotation if self.is_optional: annotation = make_non_optional_union(annotation) return is_non_string_iterable(annotation) @property def is_non_string_sequence(self) -> bool: """Check if the field type is a non-string Sequence. If ``self.annotation`` is an optional union, only the non-optional members of the union are evaluated. See: https://github.com/litestar-org/litestar/issues/1106 """ annotation = self.annotation if self.is_optional: annotation = make_non_optional_union(annotation) return is_non_string_sequence(annotation) @property def is_any(self) -> bool: """Check if the field type is Any.""" return is_any(self.annotation) @property def is_generic(self) -> bool: """Check if the field type is a custom class extending Generic.""" return is_generic(self.annotation) @property def is_simple_type(self) -> bool: """Check if the field type is a singleton value (e.g. int, str etc.).""" return not ( self.is_generic or self.is_optional or self.is_union or self.is_mapping or self.is_non_string_iterable or self.is_new_type ) @property def is_parameter_field(self) -> bool: """Check if the field type is a parameter kwarg value.""" return isinstance(self.kwarg_definition, ParameterKwarg) @property def is_const(self) -> bool: """Check if the field is defined as constant value.""" return bool(self.kwarg_definition and getattr(self.kwarg_definition, "const", False)) @property def is_required(self) -> bool: """Check if the field should be marked as a required parameter.""" if Required in self.type_wrappers: # type: ignore[comparison-overlap] return True if NotRequired in self.type_wrappers or UnsetType in self.args: # type: ignore[comparison-overlap] return False if isinstance(self.kwarg_definition, ParameterKwarg) and self.kwarg_definition.required is not None: return self.kwarg_definition.required return not self.is_optional and not self.is_any and (not self.has_default or self.default is None) @property def is_annotated(self) -> bool: """Check if the field type is Annotated.""" return bool(self.metadata) @property def is_literal(self) -> bool: """Check if the field type is Literal.""" return self.origin is Literal @property def is_forward_ref(self) -> bool: """Whether the annotation is a forward reference or not.""" return isinstance(self.annotation, (str, ForwardRef)) @property def is_mapping(self) -> bool: """Whether the annotation is a mapping or not.""" return self.is_subclass_of(Mapping) @property def is_tuple(self) -> bool: """Whether the annotation is a ``tuple`` or not.""" return self.is_subclass_of(tuple) @property def is_new_type(self) -> bool: return isinstance(self.annotation, NewType) @property def is_type_alias_type(self) -> bool: """Whether the annotation is a ``TypeAliasType``""" return isinstance(self.annotation, TypeAliasTypes) @property def is_type_var(self) -> bool: """Whether the annotation is a TypeVar or not.""" return isinstance(self.annotation, TypeVar) @property def is_union(self) -> bool: """Whether the annotation is a union type or not.""" return self.origin in UnionTypes @property def is_optional(self) -> bool: """Whether the annotation is Optional or not.""" return bool(self.is_union and NoneType in self.args) @property def is_none_type(self) -> bool: """Whether the annotation is NoneType or not.""" return self.annotation is NoneType @property def is_collection(self) -> bool: """Whether the annotation is a collection type or not.""" return self.is_subclass_of(Collection) @property def is_non_string_collection(self) -> bool: """Whether the annotation is a non-string collection type or not.""" return self.is_collection and not self.is_subclass_of((str, bytes)) @property def bound_types(self) -> tuple[FieldDefinition, ...] | None: """A tuple of bound types - if the annotation is a TypeVar with bound types, otherwise None.""" if self.is_type_var and (bound := getattr(self.annotation, "__bound__", None)): if is_union(bound): return tuple(FieldDefinition.from_annotation(t) for t in get_args(bound)) return (FieldDefinition.from_annotation(bound),) return None @property def generic_types(self) -> tuple[FieldDefinition, ...] | None: """A tuple of generic types passed into the annotation - if its generic.""" if not (bases := getattr(self.annotation, "__orig_bases__", None)): return None args: list[FieldDefinition] = [] for base_args in [getattr(base, "__args__", ()) for base in bases]: for arg in base_args: field_definition = FieldDefinition.from_annotation(arg) if field_definition.generic_types: args.extend(field_definition.generic_types) else: args.append(field_definition) return tuple(args) @property def is_dataclass_type(self) -> bool: """Whether the annotation is a dataclass type or not.""" return is_dataclass(cast("type", self.origin or self.annotation)) @property def is_typeddict_type(self) -> bool: """Whether the type is TypedDict or not.""" return is_typeddict(self.origin or self.annotation) @property def is_enum(self) -> bool: return self.is_subclass_of(Enum) @property def type_(self) -> Any: """The type of the annotation with all the wrappers removed, including the generic types.""" return self.origin or self.annotation def is_subclass_of(self, cl: type[Any] | tuple[type[Any], ...]) -> bool: """Whether the annotation is a subclass of the given type. Where ``self.annotation`` is a union type, this method will return ``True`` when all members of the union are a subtype of ``cl``, otherwise, ``False``. Args: cl: The type to check, or tuple of types. Passed as 2nd argument to ``issubclass()``. Returns: Whether the annotation is a subtype of the given type(s). """ if self.origin: if self.origin in UnionTypes: return all(t.is_subclass_of(cl) for t in self.inner_types) return self.origin not in UnionTypes and is_class_and_subclass(self.origin, cl) if self.annotation is AnyStr: return is_class_and_subclass(str, cl) or is_class_and_subclass(bytes, cl) return self.annotation is not Any and not self.is_type_var and is_class_and_subclass(self.annotation, cl) def has_inner_subclass_of(self, cl: type[Any] | tuple[type[Any], ...]) -> bool: """Whether any generic args are a subclass of the given type. Args: cl: The type to check, or tuple of types. Passed as 2nd argument to ``issubclass()``. Returns: Whether any of the type's generic args are a subclass of the given type. """ return any(t.is_subclass_of(cl) for t in self.inner_types) def get_type_hints(self, *, include_extras: bool = False, resolve_generics: bool = False) -> dict[str, Any]: """Get the type hints for the annotation. Args: include_extras: Flag to indicate whether to include ``Annotated[T, ...]`` or not. resolve_generics: Flag to indicate whether to resolve the generic types in the type hints or not. Returns: The type hints. """ if self.origin is not None or self.is_generic: if resolve_generics: return get_type_hints_with_generics_resolved(self.annotation, include_extras=include_extras) return get_type_hints(self.origin or self.annotation, include_extras=include_extras) return get_type_hints(self.annotation, include_extras=include_extras) @classmethod def from_annotation(cls, annotation: Any, **kwargs: Any) -> FieldDefinition: """Initialize FieldDefinition. Args: annotation: The type annotation. This should be extracted from the return of ``get_type_hints(..., include_extras=True)`` so that forward references are resolved and recursive ``Annotated`` types are flattened. **kwargs: Additional keyword arguments to pass to the ``FieldDefinition`` constructor. Returns: FieldDefinition """ unwrapped, metadata, wrappers = unwrap_annotation(annotation if annotation is not Empty else Any) origin = get_origin(unwrapped) annotation_args = () if origin is abc.Callable else get_args(unwrapped) if not kwargs.get("kwarg_definition"): if isinstance(kwargs.get("default"), (KwargDefinition, DependencyKwarg)): kwargs["kwarg_definition"] = kwargs.pop("default") elif kwarg_definition := next( (v for v in metadata if isinstance(v, (KwargDefinition, DependencyKwarg))), None ): kwargs["kwarg_definition"] = kwarg_definition if kwarg_definition.default is not Empty: warnings.warn( f"Deprecated default value specification for annotation '{annotation}'. Setting defaults " f"inside 'typing.Annotated' is discouraged and support for this will be removed in a future " f"version. Defaults should be set with regular parameter default values. Use " "'param: Annotated[, Parameter(...)] = ' instead of " "'param: Annotated[, Parameter(..., default=)].", category=DeprecationWarning, stacklevel=2, ) if kwargs.get("default", Empty) is not Empty and kwarg_definition.default != kwargs["default"]: warnings.warn( f"Ambiguous default values for annotation '{annotation}'. The default value " f"'{kwarg_definition.default!r}' set inside the parameter annotation differs from the " f"parameter default value '{kwargs['default']!r}'", category=LitestarWarning, stacklevel=2, ) metadata = tuple(v for v in metadata if not isinstance(v, (KwargDefinition, DependencyKwarg))) elif (extra := kwargs.get("extra", {})) and "kwarg_definition" in extra: kwargs["kwarg_definition"] = extra.pop("kwarg_definition") # there might be additional metadata if metadata: kwarg_definition_merge_args = {} is_sequence_container = is_non_string_sequence(annotation) # extract metadata into KwargDefinition attributes for meta in metadata: kwarg_definition_merge_args.update( _annotated_types_extractor(meta, is_sequence_container=is_sequence_container) ) # if we already have a KwargDefinition, merge it with the additional metadata if existing_kwargs_definition := kwargs.get("kwarg_definition"): kwargs["kwarg_definition"] = dataclasses.replace( existing_kwargs_definition, **kwarg_definition_merge_args ) # if not, create a new KwargDefinition else: model = BodyKwarg if kwargs.get("name") == "data" else ParameterKwarg kwargs["kwarg_definition"] = model(**kwarg_definition_merge_args) kwargs.setdefault("annotation", unwrapped) kwargs.setdefault("args", annotation_args) kwargs.setdefault("default", Empty) kwargs.setdefault("extra", {}) kwargs.setdefault("inner_types", tuple(FieldDefinition.from_annotation(arg) for arg in annotation_args)) kwargs.setdefault("instantiable_origin", get_instantiable_origin(origin, unwrapped)) kwargs.setdefault("kwarg_definition", None) kwargs.setdefault("metadata", metadata) kwargs.setdefault("name", "") kwargs.setdefault("origin", origin) kwargs.setdefault("raw", annotation) kwargs.setdefault("safe_generic_origin", get_safe_generic_origin(origin, unwrapped)) kwargs.setdefault("type_wrappers", wrappers) instance = FieldDefinition(**kwargs) if not instance.has_default and instance.kwarg_definition: return replace(instance, default=instance.kwarg_definition.default) return instance @classmethod def from_kwarg( cls, annotation: Any, name: str, default: Any = Empty, inner_types: tuple[FieldDefinition, ...] | None = None, kwarg_definition: KwargDefinition | DependencyKwarg | None = None, extra: dict[str, Any] | None = None, ) -> FieldDefinition: """Create a new FieldDefinition instance. Args: annotation: The type of the kwarg. name: Field name. default: A default value. inner_types: A tuple of FieldDefinition instances representing the inner types, if any. kwarg_definition: Kwarg Parameter. extra: A mapping of extra values. Returns: FieldDefinition instance. """ return cls.from_annotation( annotation, name=name, default=default, **{ k: v for k, v in { "inner_types": inner_types, "kwarg_definition": kwarg_definition, "extra": extra, }.items() if v is not None }, ) @classmethod def from_parameter(cls, parameter: Parameter, fn_type_hints: dict[str, Any]) -> FieldDefinition: """Initialize ParsedSignatureParameter. Args: parameter: inspect.Parameter fn_type_hints: mapping of names to types. Should be result of ``get_type_hints()``, preferably via the :attr:``get_fn_type_hints() <.utils.signature_parsing.get_fn_type_hints>`` helper. Returns: ParsedSignatureParameter. """ from litestar.datastructures import ImmutableState try: annotation = fn_type_hints[parameter.name] except KeyError as e: raise ImproperlyConfiguredException( f"'{parameter.name}' does not have a type annotation. If it should receive any value, use 'Any'." ) from e if parameter.name == "state" and not issubclass(annotation, ImmutableState): raise ImproperlyConfiguredException( f"The type annotation `{annotation}` is an invalid type for the 'state' reserved kwarg. " "It must be typed to a subclass of `litestar.datastructures.ImmutableState` or " "`litestar.datastructures.State`." ) return FieldDefinition.from_kwarg( annotation=annotation, name=parameter.name, default=Empty if parameter.default is Signature.empty else parameter.default, ) def match_predicate_recursively(self, predicate: Callable[[FieldDefinition], bool]) -> bool: """Recursively test the passed in predicate against the field and any of its inner fields. Args: predicate: A callable that receives a field definition instance as an arg and returns a boolean. Returns: A boolean. """ return predicate(self) or any(t.match_predicate_recursively(predicate) for t in self.inner_types) litestar-2.16.0/litestar/utils/000077500000000000000000000000001500564371300164225ustar00rootroot00000000000000litestar-2.16.0/litestar/utils/__init__.py000066400000000000000000000045071500564371300205410ustar00rootroot00000000000000from typing import Any from litestar.utils.deprecation import deprecated, warn_deprecation from .helpers import get_enum_string_value, get_name, unique_name_for_scope, url_quote from .path import join_paths, normalize_path from .predicates import ( _is_sync_or_async_generator, is_annotated_type, is_any, is_async_callable, is_class_and_subclass, is_class_var, is_dataclass_class, is_dataclass_instance, is_generic, is_mapping, is_non_string_iterable, is_non_string_sequence, is_optional_union, is_undefined_sentinel, is_union, ) from .scope import ( # type: ignore[attr-defined] _delete_litestar_scope_state, _get_litestar_scope_state, _set_litestar_scope_state, get_serializer_from_scope, ) from .sequence import find_index, unique from .sync import AsyncIteratorWrapper, ensure_async_callable from .typing import get_origin_or_inner_type, make_non_optional_union __all__ = ( "AsyncIteratorWrapper", "deprecated", "ensure_async_callable", "find_index", "get_enum_string_value", "get_name", "get_origin_or_inner_type", "get_serializer_from_scope", "is_annotated_type", "is_any", "is_async_callable", "is_class_and_subclass", "is_class_var", "is_dataclass_class", "is_dataclass_instance", "is_generic", "is_mapping", "is_non_string_iterable", "is_non_string_sequence", "is_optional_union", "is_undefined_sentinel", "is_union", "join_paths", "make_non_optional_union", "normalize_path", "unique", "unique_name_for_scope", "url_quote", "warn_deprecation", ) _deprecated_names = { "get_litestar_scope_state": _get_litestar_scope_state, "set_litestar_scope_state": _set_litestar_scope_state, "delete_litestar_scope_state": _delete_litestar_scope_state, "is_sync_or_async_generator": _is_sync_or_async_generator, } def __getattr__(name: str) -> Any: if name in _deprecated_names: warn_deprecation( deprecated_name=f"litestar.utils.{name}", version="2.4", kind="import", removal_in="3.0", info=f"'litestar.utils.{name}' is deprecated.", ) return globals()["_deprecated_names"][name] raise AttributeError(f"module {__name__!r} has no attribute {name!r}") # pragma: no cover litestar-2.16.0/litestar/utils/compat.py000066400000000000000000000012341500564371300202570ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, TypeVar from litestar.types import Empty, EmptyType __all__ = ("async_next",) if TYPE_CHECKING: from typing import Any, AsyncGenerator T = TypeVar("T") D = TypeVar("D") try: async_next = anext # type: ignore[name-defined] except NameError: async def async_next(gen: AsyncGenerator[T, Any], default: D | EmptyType = Empty) -> T | D: """Backwards compatibility shim for Python<3.10.""" try: return await gen.__anext__() except StopAsyncIteration as exc: if default is not Empty: return default raise exc litestar-2.16.0/litestar/utils/dataclass.py000066400000000000000000000072631500564371300207430ustar00rootroot00000000000000from __future__ import annotations from dataclasses import Field, fields from typing import TYPE_CHECKING from litestar.types import Empty from litestar.utils.predicates import is_dataclass_instance if TYPE_CHECKING: from typing import AbstractSet, Any, Iterable from litestar.types.protocols import DataclassProtocol __all__ = ( "extract_dataclass_fields", "extract_dataclass_items", "simple_asdict", ) def extract_dataclass_fields( dt: DataclassProtocol, exclude_none: bool = False, exclude_empty: bool = False, include: AbstractSet[str] | None = None, exclude: AbstractSet[str] | None = None, ) -> tuple[Field[Any], ...]: """Extract dataclass fields. Args: dt: A dataclass instance. exclude_none: Whether to exclude None values. exclude_empty: Whether to exclude Empty values. include: An iterable of fields to include. exclude: An iterable of fields to exclude. Returns: A tuple of dataclass fields. """ include = include or set() exclude = exclude or set() if common := (include & exclude): raise ValueError(f"Fields {common} are both included and excluded.") dataclass_fields: Iterable[Field[Any]] = fields(dt) if exclude_none: dataclass_fields = (field for field in dataclass_fields if getattr(dt, field.name) is not None) if exclude_empty: dataclass_fields = (field for field in dataclass_fields if getattr(dt, field.name) is not Empty) if include: dataclass_fields = (field for field in dataclass_fields if field.name in include) if exclude: dataclass_fields = (field for field in dataclass_fields if field.name not in exclude) return tuple(dataclass_fields) def extract_dataclass_items( dt: DataclassProtocol, exclude_none: bool = False, exclude_empty: bool = False, include: AbstractSet[str] | None = None, exclude: AbstractSet[str] | None = None, ) -> tuple[tuple[str, Any], ...]: """Extract dataclass name, value pairs. Unlike the 'asdict' method exports by the stlib, this function does not pickle values. Args: dt: A dataclass instance. exclude_none: Whether to exclude None values. exclude_empty: Whether to exclude Empty values. include: An iterable of fields to include. exclude: An iterable of fields to exclude. Returns: A tuple of key/value pairs. """ dataclass_fields = extract_dataclass_fields(dt, exclude_none, exclude_empty, include, exclude) return tuple((field.name, getattr(dt, field.name)) for field in dataclass_fields) def simple_asdict( obj: DataclassProtocol, exclude_none: bool = False, exclude_empty: bool = False, convert_nested: bool = True, exclude: set[str] | None = None, ) -> dict[str, Any]: """Convert a dataclass to a dictionary. This method has important differences to the standard library version: - it does not deepcopy values - it does not recurse into collections Args: obj: A dataclass instance. exclude_none: Whether to exclude None values. exclude_empty: Whether to exclude Empty values. convert_nested: Whether to recursively convert nested dataclasses. exclude: An iterable of fields to exclude. Returns: A dictionary of key/value pairs. """ ret = {} for field in extract_dataclass_fields(obj, exclude_none, exclude_empty, exclude=exclude): value = getattr(obj, field.name) if is_dataclass_instance(value) and convert_nested: ret[field.name] = simple_asdict(value, exclude_none, exclude_empty) else: ret[field.name] = getattr(obj, field.name) return ret litestar-2.16.0/litestar/utils/deprecation.py000066400000000000000000000067411500564371300213010ustar00rootroot00000000000000from __future__ import annotations import inspect from functools import wraps from typing import Callable, Literal, TypeVar from warnings import warn from typing_extensions import ParamSpec __all__ = ("deprecated", "warn_deprecation") T = TypeVar("T") P = ParamSpec("P") DeprecatedKind = Literal["function", "method", "classmethod", "attribute", "property", "class", "parameter", "import"] def warn_deprecation( version: str, deprecated_name: str, kind: DeprecatedKind, *, removal_in: str | None = None, alternative: str | None = None, info: str | None = None, pending: bool = False, ) -> None: """Warn about a call to a (soon to be) deprecated function. Args: version: Litestar version where the deprecation will occur deprecated_name: Name of the deprecated function removal_in: Litestar version where the deprecated function will be removed alternative: Name of a function that should be used instead info: Additional information pending: Use ``PendingDeprecationWarning`` instead of ``DeprecationWarning`` kind: Type of the deprecated thing """ parts = [] if kind == "import": access_type = "Import of" elif kind in {"function", "method"}: access_type = "Call to" else: access_type = "Use of" if pending: parts.append(f"{access_type} {kind} awaiting deprecation {deprecated_name!r}") else: parts.append(f"{access_type} deprecated {kind} {deprecated_name!r}") parts.extend( ( f"Deprecated in litestar {version}", f"This {kind} will be removed in {removal_in or 'the next major version'}", ) ) if alternative: parts.append(f"Use {alternative!r} instead") if info: parts.append(info) text = ". ".join(parts) warning_class = PendingDeprecationWarning if pending else DeprecationWarning warn(text, warning_class, stacklevel=2) def deprecated( version: str, *, removal_in: str | None = None, alternative: str | None = None, info: str | None = None, pending: bool = False, kind: Literal["function", "method", "classmethod", "property"] | None = None, ) -> Callable[[Callable[P, T]], Callable[P, T]]: """Create a decorator wrapping a function, method or property with a warning call about a (pending) deprecation. Args: version: Litestar version where the deprecation will occur removal_in: Litestar version where the deprecated function will be removed alternative: Name of a function that should be used instead info: Additional information pending: Use ``PendingDeprecationWarning`` instead of ``DeprecationWarning`` kind: Type of the deprecated callable. If ``None``, will use ``inspect`` to figure out if it's a function or method Returns: A decorator wrapping the function call with a warning """ def decorator(func: Callable[P, T]) -> Callable[P, T]: @wraps(func) def wrapped(*args: P.args, **kwargs: P.kwargs) -> T: warn_deprecation( version=version, deprecated_name=func.__name__, info=info, alternative=alternative, pending=pending, removal_in=removal_in, kind=kind or ("method" if inspect.ismethod(func) else "function"), ) return func(*args, **kwargs) return wrapped return decorator litestar-2.16.0/litestar/utils/empty.py000066400000000000000000000021351500564371300201330ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, TypeVar from litestar.types.empty import Empty if TYPE_CHECKING: from litestar.types.empty import EmptyType ValueT = TypeVar("ValueT") DefaultT = TypeVar("DefaultT") def value_or_default(value: ValueT | EmptyType, default: DefaultT) -> ValueT | DefaultT: """Return `value` handling the case where it is empty. If `value` is `Empty`, `default` is returned. Args: value: The value to check. default: The default value to return if `value` is `Empty`. Returns: The value or default value. """ return default if value is Empty else value class EmptyValueError(ValueError): """Raised when an empty value is encountered.""" def value_or_raise(value: ValueT | EmptyType) -> ValueT: """Return `value` handling the case where it is empty. Args: value: The value to check. Returns: The value. Raises: EmptyValueError: If `value` is `Empty`. """ if value is Empty: raise EmptyValueError("Empty value encountered") return value litestar-2.16.0/litestar/utils/helpers.py000066400000000000000000000047461500564371300204510ustar00rootroot00000000000000from __future__ import annotations from enum import Enum from functools import partial from typing import TYPE_CHECKING, TypeVar, cast from urllib.parse import quote from litestar.utils.typing import get_origin_or_inner_type if TYPE_CHECKING: from collections.abc import Container from litestar.types import MaybePartial __all__ = ( "get_enum_string_value", "get_name", "unique_name_for_scope", "unwrap_partial", "url_quote", ) T = TypeVar("T") def get_name(value: object) -> str: """Get the ``__name__`` of an object. Args: value: An arbitrary object. Returns: A name string. """ name = getattr(value, "__name__", None) if name is not None: return cast("str", name) # On Python 3.8 and 3.9, Foo[int] does not have the __name__ attribute. if origin := get_origin_or_inner_type(value): return cast("str", origin.__name__) return type(value).__name__ def get_enum_string_value(value: Enum | str) -> str: """Return the string value of a string enum. See: https://github.com/litestar-org/litestar/pull/633#issuecomment-1286519267 Args: value: An enum or string. Returns: A string. """ return value.value if isinstance(value, Enum) else value # type: ignore[no-any-return] def unwrap_partial(value: MaybePartial[T]) -> T: """Unwraps a partial, returning the underlying callable. Args: value: A partial function. Returns: Callable """ from litestar.utils.sync import AsyncCallable return cast("T", value.func if isinstance(value, (partial, AsyncCallable)) else value) def url_quote(value: str | bytes) -> str: """Quote a URL. Args: value: A URL. Returns: A quoted URL. """ return quote(value, safe="/#%[]=:;$&()+,!?*@'~") def unique_name_for_scope(base_name: str, scope: Container[str]) -> str: """Create a name derived from ``base_name`` that's unique within ``scope``""" i = 0 while True: if (unique_name := f"{base_name}_{i}") not in scope: return unique_name i += 1 def get_exception_group() -> type[BaseException]: """Get the exception group class with version compatibility.""" try: return cast("type[BaseException]", ExceptionGroup) # type:ignore[name-defined] except NameError: from exceptiongroup import ExceptionGroup as _ExceptionGroup # pyright: ignore return cast("type[BaseException]", _ExceptionGroup) litestar-2.16.0/litestar/utils/module_loader.py000066400000000000000000000051701500564371300216120ustar00rootroot00000000000000"""General utility functions.""" from __future__ import annotations import sys from importlib import import_module from importlib.util import find_spec from pathlib import Path from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from types import ModuleType __all__ = ( "import_string", "module_to_os_path", ) def module_to_os_path(dotted_path: str = "app") -> Path: """Find Module to OS Path. Return a path to the base directory of the project or the module specified by `dotted_path`. Args: dotted_path: The path to the module. Defaults to "app". Raises: TypeError: The module could not be found. Returns: Path: The path to the module. """ try: if (src := find_spec(dotted_path)) is None: # pragma: no cover raise TypeError(f"Couldn't find the path for {dotted_path}") except ModuleNotFoundError as e: raise TypeError(f"Couldn't find the path for {dotted_path}") from e path = Path(str(src.origin)) return path.parent if path.is_file() else path def import_string(dotted_path: str) -> Any: """Dotted Path Import. Import a dotted module path and return the attribute/class designated by the last name in the path. Raise ImportError if the import failed. Args: dotted_path: The path of the module to import. Raises: ImportError: Could not import the module. Returns: object: The imported object. """ def _is_loaded(module: ModuleType | None) -> bool: spec = getattr(module, "__spec__", None) initializing = getattr(spec, "_initializing", False) return bool(module and spec and not initializing) def _cached_import(module_path: str, class_name: str) -> Any: """Import and cache a class from a module. Args: module_path: dotted path to module. class_name: Class or function name. Returns: object: The imported class or function """ # Check whether module is loaded and fully initialized. module = sys.modules.get(module_path) if not _is_loaded(module): module = import_module(module_path) return getattr(module, class_name) try: module_path, class_name = dotted_path.rsplit(".", 1) except ValueError as e: msg = "%s doesn't look like a module path" raise ImportError(msg, dotted_path) from e try: return _cached_import(module_path, class_name) except AttributeError as e: msg = "Module '%s' does not define a '%s' attribute/class" raise ImportError(msg, module_path, class_name) from e litestar-2.16.0/litestar/utils/path.py000066400000000000000000000013231500564371300177270ustar00rootroot00000000000000from __future__ import annotations import re from typing import Iterable __all__ = ("join_paths", "normalize_path") multi_slash_pattern = re.compile("//+") def normalize_path(path: str) -> str: """Normalize a given path by ensuring it starts with a slash and does not end with a slash. Args: path: Path string Returns: Path string """ path = path.strip("/") path = f"/{path}" return multi_slash_pattern.sub("/", path) def join_paths(paths: Iterable[str]) -> str: """Normalize and joins path fragments. Args: paths: An iterable of path fragments. Returns: A normalized joined path string. """ return normalize_path("/".join(paths)) litestar-2.16.0/litestar/utils/predicates.py000066400000000000000000000217131500564371300211230ustar00rootroot00000000000000from __future__ import annotations from asyncio import iscoroutinefunction from collections import defaultdict, deque from collections.abc import Iterable as CollectionsIterable from dataclasses import is_dataclass from inspect import isasyncgenfunction, isclass, isgeneratorfunction from typing import ( TYPE_CHECKING, Any, Awaitable, Callable, ClassVar, DefaultDict, Deque, Dict, FrozenSet, Generic, Iterable, List, Mapping, Optional, Sequence, Set, Tuple, TypeVar, get_origin, ) from typing_extensions import ( Annotated, ParamSpec, TypeGuard, get_args, ) from litestar.constants import UNDEFINED_SENTINELS from litestar.types.builtin_types import NoneType, UnionTypes from litestar.utils.deprecation import warn_deprecation from litestar.utils.helpers import unwrap_partial from litestar.utils.typing import get_origin_or_inner_type if TYPE_CHECKING: from litestar.types.callable_types import AnyGenerator from litestar.types.protocols import DataclassProtocol __all__ = ( "is_annotated_type", "is_any", "is_async_callable", "is_class_and_subclass", "is_class_var", "is_dataclass_class", "is_dataclass_instance", "is_generic", "is_mapping", "is_non_string_iterable", "is_non_string_sequence", "is_optional_union", "is_undefined_sentinel", "is_union", ) P = ParamSpec("P") T = TypeVar("T") def is_async_callable(value: Callable[P, T]) -> TypeGuard[Callable[P, Awaitable[T]]]: """Extend :func:`asyncio.iscoroutinefunction` to additionally detect async :func:`functools.partial` objects and class instances with ``async def __call__()`` defined. Args: value: Any Returns: Bool determining if type of ``value`` is an awaitable. """ value = unwrap_partial(value) return iscoroutinefunction(value) or ( callable(value) and iscoroutinefunction(value.__call__) # type: ignore[operator] ) def is_dataclass_instance(obj: Any) -> TypeGuard[DataclassProtocol]: """Check if an object is a dataclass instance. Args: obj: An object to check. Returns: True if the object is a dataclass instance. """ return hasattr(type(obj), "__dataclass_fields__") def is_dataclass_class(annotation: Any) -> TypeGuard[type[DataclassProtocol]]: """Wrap :func:`is_dataclass ` in a :data:`typing.TypeGuard`. Args: annotation: tested to determine if instance or type of :class:`dataclasses.dataclass`. Returns: ``True`` if instance or type of ``dataclass``. """ try: origin = get_origin_or_inner_type(annotation) annotation = origin or annotation return isclass(annotation) and is_dataclass(annotation) except TypeError: # pragma: no cover return False def is_class_and_subclass(annotation: Any, type_or_type_tuple: type[T] | tuple[type[T], ...]) -> TypeGuard[type[T]]: """Return ``True`` if ``value`` is a ``class`` and is a subtype of ``t_type``. See https://github.com/litestar-org/litestar/issues/367 Args: annotation: The value to check if is class and subclass of ``t_type``. type_or_type_tuple: Type used for :func:`issubclass` check of ``value`` Returns: bool """ origin = get_origin_or_inner_type(annotation) if not origin and not isclass(annotation): return False try: return issubclass(origin or annotation, type_or_type_tuple) except TypeError: # pragma: no cover return False def is_generic(annotation: Any) -> bool: """Given a type annotation determine if the annotation is a generic class. Args: annotation: A type. Returns: True if the annotation is a subclass of :data:`Generic ` otherwise ``False``. """ return is_class_and_subclass(annotation, Generic) # type: ignore[arg-type] def is_mapping(annotation: Any) -> TypeGuard[Mapping[Any, Any]]: """Given a type annotation determine if the annotation is a mapping type. Args: annotation: A type. Returns: A typeguard determining whether the type can be cast as :class:`Mapping `. """ _type = get_origin_or_inner_type(annotation) or annotation return isclass(_type) and issubclass(_type, (dict, defaultdict, DefaultDict, Mapping)) def is_non_string_iterable(annotation: Any) -> TypeGuard[Iterable[Any]]: """Given a type annotation determine if the annotation is an iterable. Args: annotation: A type. Returns: A typeguard determining whether the type can be cast as :class:`Iterable ` that is not a string. """ origin = get_origin_or_inner_type(annotation) if not origin and not isclass(annotation): return False try: return not issubclass(origin or annotation, (str, bytes)) and ( issubclass(origin or annotation, (Iterable, CollectionsIterable, Dict, dict, Mapping)) or is_non_string_sequence(annotation) ) except TypeError: # pragma: no cover return False def is_non_string_sequence(annotation: Any) -> TypeGuard[Sequence[Any]]: """Given a type annotation determine if the annotation is a sequence. Args: annotation: A type. Returns: A typeguard determining whether the type can be cast as :class`Sequence ` that is not a string. """ origin = get_origin_or_inner_type(annotation) if not origin and not isclass(annotation): return False try: return not issubclass(origin or annotation, (str, bytes)) and issubclass( origin or annotation, ( # type: ignore[arg-type] Tuple, List, Set, FrozenSet, Deque, Sequence, list, tuple, deque, set, frozenset, ), ) except TypeError: # pragma: no cover return False def is_any(annotation: Any) -> TypeGuard[Any]: """Given a type annotation determine if the annotation is Any. Args: annotation: A type. Returns: A typeguard determining whether the type is :data:`Any `. """ return ( annotation is Any or getattr(annotation, "_name", "") == "typing.Any" or (get_origin_or_inner_type(annotation) in UnionTypes and Any in get_args(annotation)) ) def is_union(annotation: Any) -> bool: """Given a type annotation determine if the annotation infers an optional union. Args: annotation: A type. Returns: A boolean determining whether the type is :data:`Union typing.Union>`. """ return get_origin_or_inner_type(annotation) in UnionTypes def is_optional_union(annotation: Any) -> TypeGuard[Any | None]: """Given a type annotation determine if the annotation infers an optional union. Args: annotation: A type. Returns: A typeguard determining whether the type is :data:`Union typing.Union>` with a None value or :data:`Optional ` which is equivalent. """ origin = get_origin_or_inner_type(annotation) return origin is Optional or ( get_origin_or_inner_type(annotation) in UnionTypes and NoneType in get_args(annotation) ) def is_class_var(annotation: Any) -> bool: """Check if the given annotation is a ClassVar. Args: annotation: A type annotation Returns: A boolean. """ annotation = get_origin_or_inner_type(annotation) or annotation return annotation is ClassVar def _is_sync_or_async_generator(obj: Any) -> TypeGuard[AnyGenerator]: """Check if the given annotation is a sync or async generator. Args: obj: type to be tested for sync or async generator. Returns: A boolean. """ return isgeneratorfunction(obj) or isasyncgenfunction(obj) def is_annotated_type(annotation: Any) -> bool: """Check if the given annotation is an Annotated. Args: annotation: A type annotation Returns: A boolean. """ return get_origin(annotation) is Annotated def is_undefined_sentinel(value: Any) -> bool: """Check if the given value is the undefined sentinel. Args: value: A value to be tested for undefined sentinel. Returns: A boolean. """ return any(v is value for v in UNDEFINED_SENTINELS) _deprecated_names = {"is_sync_or_async_generator": _is_sync_or_async_generator} def __getattr__(name: str) -> Any: if name in _deprecated_names: warn_deprecation( deprecated_name=f"litestar.utils.scope.{name}", version="2.4", kind="import", removal_in="3.0", info=f"'litestar.utils.predicates.{name}' is deprecated.", ) return globals()["_deprecated_names"][name] raise AttributeError(f"module {__name__!r} has no attribute {name!r}") # pragma: no cover litestar-2.16.0/litestar/utils/scope/000077500000000000000000000000001500564371300175335ustar00rootroot00000000000000litestar-2.16.0/litestar/utils/scope/__init__.py000066400000000000000000000041751500564371300216530ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from litestar.serialization import get_serializer from litestar.utils.deprecation import warn_deprecation from litestar.utils.scope.state import delete_litestar_scope_state as _delete_litestar_scope_state from litestar.utils.scope.state import get_litestar_scope_state as _get_litestar_scope_state from litestar.utils.scope.state import set_litestar_scope_state as _set_litestar_scope_state if TYPE_CHECKING: from litestar.types import Scope, Serializer __all__ = ("get_serializer_from_scope",) def get_serializer_from_scope(scope: Scope) -> Serializer: """Return a serializer given a scope object. Args: scope: The ASGI connection scope. Returns: A serializer function """ route_handler = scope["route_handler"] app = scope["litestar_app"] if hasattr(route_handler, "resolve_type_encoders"): type_encoders = route_handler.resolve_type_encoders() else: type_encoders = app.type_encoders or {} if response_class := ( route_handler.resolve_response_class() # pyright: ignore if hasattr(route_handler, "resolve_response_class") else app.response_class ): type_encoders = {**type_encoders, **(response_class.type_encoders or {})} return get_serializer(type_encoders) _deprecated_names = { "get_litestar_scope_state": _get_litestar_scope_state, "set_litestar_scope_state": _set_litestar_scope_state, "delete_litestar_scope_state": _delete_litestar_scope_state, } def __getattr__(name: str) -> Any: if name in _deprecated_names: warn_deprecation( deprecated_name=f"litestar.utils.scope.{name}", version="2.4", kind="import", removal_in="3.0", info=f"'litestar.utils.scope.{name}' is deprecated. The Litestar scope state is private and should not be " f"used. Plugin authors should maintain their own scope state namespace.", ) return globals()["_deprecated_names"][name] raise AttributeError(f"module {__name__!r} has no attribute {name!r}") # pragma: no cover litestar-2.16.0/litestar/utils/scope/state.py000066400000000000000000000120301500564371300212210ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Final from litestar.types import Empty, EmptyType from litestar.utils.empty import value_or_default if TYPE_CHECKING: from typing_extensions import Self from litestar.datastructures import URL, Accept, Headers, UploadFile from litestar.types.asgi_types import Scope from litestar.types.composite_types import ExceptionHandlersMap CONNECTION_STATE_KEY: Final = "_ls_connection_state" @dataclass class ScopeState: """An object for storing connection state. This is an internal API, and subject to change without notice. All types are a union with `EmptyType` and are seeded with the `Empty` value. """ __slots__ = ( "_compat_ns", "accept", "base_url", "body", "content_type", "cookies", "csrf_token", "dependency_cache", "do_cache", "exception_handlers", "flash_messages", "form", "headers", "is_cached", "json", "log_context", "msgpack", "parsed_query", "response_compressed", "response_started", "session_id", "url", ) def __init__(self) -> None: self.accept = Empty self.base_url = Empty self.body = Empty self.content_type = Empty self.cookies = Empty self.csrf_token = Empty self.dependency_cache = Empty self.do_cache = Empty self.exception_handlers = Empty self.form = Empty self.flash_messages = [] self.headers = Empty self.is_cached = Empty self.json = Empty self.log_context: dict[str, Any] = {} self.msgpack = Empty self.parsed_query = Empty self.response_compressed = Empty self.response_started = False self.session_id = Empty self.url = Empty self._compat_ns: dict[str, Any] = {} accept: Accept | EmptyType base_url: URL | EmptyType body: bytes | EmptyType content_type: tuple[str, dict[str, str]] | EmptyType cookies: dict[str, str] | EmptyType csrf_token: str | EmptyType dependency_cache: dict[str, Any] | EmptyType do_cache: bool | EmptyType exception_handlers: ExceptionHandlersMap | EmptyType form: dict[str, str | list[str] | UploadFile] | EmptyType flash_messages: list[dict[str, str]] headers: Headers | EmptyType is_cached: bool | EmptyType json: Any | EmptyType log_context: dict[str, Any] msgpack: Any | EmptyType parsed_query: tuple[tuple[str, str], ...] | EmptyType response_compressed: bool | EmptyType response_started: bool session_id: str | None | EmptyType url: URL | EmptyType _compat_ns: dict[str, Any] @classmethod def from_scope(cls, scope: Scope) -> Self: """Create a new `ConnectionState` object from a scope. Object is cached in the scope's state under the `SCOPE_STATE_NAMESPACE` key. Args: scope: The ASGI connection scope. Returns: A `ConnectionState` object. """ base_scope_state = scope.setdefault("state", {}) if (state := base_scope_state.get(CONNECTION_STATE_KEY)) is None: state = base_scope_state[CONNECTION_STATE_KEY] = cls() return state def get_litestar_scope_state(scope: Scope, key: str, default: Any = None, pop: bool = False) -> Any: """Get an internal value from connection scope state. Args: scope: The connection scope. key: Key to get from internal namespace in scope state. default: Default value to return. pop: Boolean flag dictating whether the value should be deleted from the state. Returns: Value mapped to ``key`` in internal connection scope namespace. """ scope_state = ScopeState.from_scope(scope) try: val = value_or_default(getattr(scope_state, key), default) if pop: setattr(scope_state, key, Empty) return val except AttributeError: if pop: return scope_state._compat_ns.pop(key, default) return scope_state._compat_ns.get(key, default) def set_litestar_scope_state(scope: Scope, key: str, value: Any) -> None: """Set an internal value in connection scope state. Args: scope: The connection scope. key: Key to set under internal namespace in scope state. value: Value for key. """ scope_state = ScopeState.from_scope(scope) if hasattr(scope_state, key): setattr(scope_state, key, value) else: scope_state._compat_ns[key] = value def delete_litestar_scope_state(scope: Scope, key: str) -> None: """Delete an internal value from connection scope state. Args: scope: The connection scope. key: Key to set under internal namespace in scope state. """ scope_state = ScopeState.from_scope(scope) if hasattr(scope_state, key): setattr(scope_state, key, Empty) else: del scope_state._compat_ns[key] litestar-2.16.0/litestar/utils/sequence.py000066400000000000000000000013511500564371300206040ustar00rootroot00000000000000from __future__ import annotations from typing import Callable, Sequence, TypeVar __all__ = ("find_index", "unique") T = TypeVar("T") def find_index(target_list: Sequence[T], predicate: Callable[[T], bool]) -> int: """Find element in list given a key and value. List elements can be dicts or classes """ return next((i for i, element in enumerate(target_list) if predicate(element)), -1) def unique(value: Sequence[T]) -> list[T]: """Return all unique values in a given sequence or iterator.""" try: return list(set(value)) except TypeError: output: list[T] = [] for element in value: if element not in output: output.append(element) return output litestar-2.16.0/litestar/utils/signature.py000066400000000000000000000243341500564371300210030ustar00rootroot00000000000000from __future__ import annotations import sys import typing from copy import deepcopy from dataclasses import dataclass, replace from inspect import Signature, getmembers, isclass, ismethod from itertools import chain from typing import TYPE_CHECKING, Any, Union from typing_extensions import Annotated, Self, get_args, get_origin, get_type_hints from litestar import connection, datastructures, types from litestar.types import Empty from litestar.typing import FieldDefinition from litestar.utils.typing import expand_type_var_in_type_hint, unwrap_annotation from litestar.utils.warnings import warn_signature_namespace_override if TYPE_CHECKING: from typing import Sequence from litestar.types import AnyCallable if sys.version_info < (3, 11): from typing import _get_defaults # type: ignore[attr-defined] else: def _get_defaults(_: Any) -> Any: ... __all__ = ( "ParsedSignature", "add_types_to_signature_namespace", "get_fn_type_hints", "merge_signature_namespaces", ) _GLOBAL_NAMES = { namespace: export for namespace, export in chain( tuple(getmembers(types)), tuple(getmembers(connection)), tuple(getmembers(datastructures)) ) if namespace[0].isupper() and namespace in chain(types.__all__, connection.__all__, datastructures.__all__) # pyright: ignore } """A mapping of names used for handler signature forward-ref resolution. This allows users to include these names within an `if TYPE_CHECKING:` block in their handler module. """ def _unwrap_implicit_optional_hints(defaults: dict[str, Any], hints: dict[str, Any]) -> dict[str, Any]: """Unwrap implicit optional hints. On Python<3.11, if a function parameter annotation has a ``None`` default, it is unconditionally wrapped in an ``Optional`` type. If the annotation is not annotated, then any nested unions are flattened, e.g.,: .. code-block:: python def foo(a: Optional[Union[str, int]] = None): ... ...will become `Union[str, int, NoneType]`. However, if the annotation is annotated, then we end up with an optional union around the annotated type, e.g.,: .. code-block:: python def foo(a: Annotated[Optional[Union[str, int]], ...] = None): ... ... becomes `Union[Annotated[Union[str, int, NoneType], ...], NoneType]` This function makes the latter case consistent with the former by either removing the outer union if it is redundant or flattening the union if it is not. The latter case would become `Annotated[Union[str, int, NoneType], ...]`. Args: defaults: Mapping of names to default values. hints: Mapping of names to types. Returns: Mapping of names to types. """ def _is_two_arg_optional(origin_: Any, args_: Any) -> bool: """Check if a type is a two-argument optional type. If the type has been wrapped in `Optional` by `get_type_hints()` it will always be a union of a type and `NoneType`. See: https://github.com/litestar-org/litestar/pull/2516 """ return origin_ is Union and len(args_) == 2 and args_[1] is type(None) def _is_any_optional(origin_: Any, args_: tuple[Any, ...]) -> bool: """Detect if a type is a union with `NoneType`. After detecting that a type is a two-argument optional type, this function can be used to detect if the inner type is a union with `NoneType` at all. We only want to perform the unwrapping of the optional union if the inner type is optional as well. """ return origin_ is Union and any(arg is type(None) for arg in args_) for name, default in defaults.items(): if default is not None: continue hint = hints[name] origin = get_origin(hint) args = get_args(hint) if _is_two_arg_optional(origin, args): unwrapped_inner, meta, wrappers = unwrap_annotation(args[0]) if Annotated not in wrappers: continue inner_args = get_args(unwrapped_inner) if not _is_any_optional(get_origin(unwrapped_inner), inner_args): # this is where hint is like `Union[Annotated[Union[str, int], ...], NoneType]`, we add the outer union # into the inner one, and re-wrap with Annotated union_args = (*(inner_args or (unwrapped_inner,)), type(None)) # calling `__class_getitem__` directly as in earlier py vers it is a syntax error to unpack into # the getitem brackets, e.g., Annotated[T, *meta]. hints[name] = Annotated.__class_getitem__((Union[union_args], *meta)) # type: ignore[attr-defined] continue # this is where hint is like `Union[Annotated[Union[str, NoneType], ...], NoneType]`, we remove the # redundant outer union hints[name] = args[0] return hints def get_fn_type_hints(fn: Any, namespace: dict[str, Any] | None = None) -> dict[str, Any]: """Resolve type hints for ``fn``. Args: fn: Callable that is being inspected namespace: Extra names for resolution of forward references. Returns: Mapping of names to types. """ fn_to_inspect: Any = fn module_name = fn_to_inspect.__module__ if isclass(fn_to_inspect): fn_to_inspect = fn_to_inspect.__init__ # detect objects that are not functions and that have a `__call__` method if callable(fn_to_inspect) and ismethod(fn_to_inspect.__call__): fn_to_inspect = fn_to_inspect.__call__ # inspect the underlying function for methods if hasattr(fn_to_inspect, "__func__"): fn_to_inspect = fn_to_inspect.__func__ # Order important. If a litestar name has been overridden in the function module, we want # to use that instead of the litestar one. namespace = { **_GLOBAL_NAMES, **vars(typing), **vars(sys.modules[module_name]), **(namespace or {}), } hints = get_type_hints(fn_to_inspect, globalns=namespace, include_extras=True) if sys.version_info < (3, 11): # see https://github.com/litestar-org/litestar/pull/2516 defaults = _get_defaults(fn_to_inspect) hints = _unwrap_implicit_optional_hints(defaults, hints) return hints @dataclass(frozen=True) class ParsedSignature: """Parsed signature. This object is the primary source of handler/dependency signature information. The only post-processing that occurs is the conversion of any forward referenced type annotations. """ __slots__ = ("original_signature", "parameters", "return_type") parameters: dict[str, FieldDefinition] """A mapping of parameter names to ParsedSignatureParameter instances.""" return_type: FieldDefinition """The return annotation of the callable.""" original_signature: Signature """The raw signature as returned by :func:`inspect.signature`""" def __deepcopy__(self, memo: dict[str, Any]) -> Self: return type(self)( parameters={k: deepcopy(v) for k, v in self.parameters.items()}, return_type=deepcopy(self.return_type), original_signature=deepcopy(self.original_signature), ) @classmethod def from_fn(cls, fn: AnyCallable, signature_namespace: dict[str, Any]) -> Self: """Parse a function signature. Args: fn: Any callable. signature_namespace: mapping of names to types for forward reference resolution Returns: ParsedSignature """ signature = Signature.from_callable(fn) fn_type_hints = get_fn_type_hints(fn, namespace=signature_namespace) expanded_type_hints = expand_type_var_in_type_hint(fn_type_hints, signature_namespace) return cls.from_signature(signature, expanded_type_hints) @classmethod def from_signature(cls, signature: Signature, fn_type_hints: dict[str, type]) -> Self: """Parse an :class:`inspect.Signature` instance. Args: signature: An :class:`inspect.Signature` instance. fn_type_hints: mapping of types Returns: ParsedSignature """ parameters = tuple( FieldDefinition.from_parameter(parameter=parameter, fn_type_hints=fn_type_hints) for name, parameter in signature.parameters.items() if name not in ("self", "cls") ) return_type = FieldDefinition.from_annotation(fn_type_hints.get("return", Any)) return cls( parameters={p.name: p for p in parameters}, return_type=return_type if "return" in fn_type_hints else replace(return_type, annotation=Empty), original_signature=signature, ) def add_types_to_signature_namespace( signature_types: Sequence[Any], signature_namespace: dict[str, Any] ) -> dict[str, Any]: """Add types to ith signature namespace mapping. Types are added mapped to their `__name__` attribute. Args: signature_types: A list of types to add to the signature namespace. signature_namespace: The signature namespace to add types to. Raises: AttributeError: If a type does not have a `__name__` attribute. Returns: The updated signature namespace. """ return merge_signature_namespaces( signature_namespace=signature_namespace, additional_signature_namespace={signature_type.__name__: signature_type for signature_type in signature_types}, ) def merge_signature_namespaces( signature_namespace: dict[str, Any], additional_signature_namespace: dict[str, Any] ) -> dict[str, Any]: """Add types to ith signature namespace mapping. Types are added mapped to their `__name__` attribute. Args: signature_namespace: The signature namespace to add types to. additional_signature_namespace: The signature namespace to merge Raises: AttributeError: If a type does not have a `__name__` attribute. Returns: The updated signature namespace. """ for signature_key, signature_type in additional_signature_namespace.items(): if signature_key in signature_namespace and signature_namespace.get(signature_key) != signature_type: warn_signature_namespace_override(signature_key) signature_namespace.update(additional_signature_namespace) return signature_namespace litestar-2.16.0/litestar/utils/sync.py000066400000000000000000000043771500564371300177630ustar00rootroot00000000000000from __future__ import annotations from typing import ( AsyncGenerator, Awaitable, Callable, Generic, Iterable, Iterator, TypeVar, ) from typing_extensions import ParamSpec from litestar.concurrency import sync_to_thread from litestar.utils.predicates import is_async_callable __all__ = ("AsyncCallable", "AsyncIteratorWrapper", "ensure_async_callable", "is_async_callable") P = ParamSpec("P") T = TypeVar("T") def ensure_async_callable(fn: Callable[P, T]) -> Callable[P, Awaitable[T]]: """Ensure that ``fn`` is an asynchronous callable. If it is an asynchronous, return the original object, else wrap it in an ``AsyncCallable`` """ if is_async_callable(fn): return fn return AsyncCallable(fn) # pyright: ignore class AsyncCallable: """Wrap a given callable to be called in a thread pool using ``anyio.to_thread.run_sync``, keeping a reference to the original callable as :attr:`func` """ def __init__(self, fn: Callable[P, T]) -> None: # pyright: ignore self.func = fn def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Awaitable[T]: # pyright: ignore return sync_to_thread(self.func, *args, **kwargs) # type: ignore[arg-type] class AsyncIteratorWrapper(Generic[T]): """Asynchronous generator, wrapping an iterable or iterator.""" __slots__ = ("generator", "iterator") def __init__(self, iterator: Iterator[T] | Iterable[T]) -> None: """Take a sync iterator or iterable and yields values from it asynchronously. Args: iterator: A sync iterator or iterable. """ self.iterator = iterator if isinstance(iterator, Iterator) else iter(iterator) self.generator = self._async_generator() def _call_next(self) -> T: try: return next(self.iterator) except StopIteration as e: raise ValueError from e async def _async_generator(self) -> AsyncGenerator[T, None]: while True: try: yield await sync_to_thread(self._call_next) except ValueError: return def __aiter__(self) -> AsyncIteratorWrapper[T]: return self async def __anext__(self) -> T: return await self.generator.__anext__() litestar-2.16.0/litestar/utils/typing.py000066400000000000000000000237251500564371300203170ustar00rootroot00000000000000from __future__ import annotations import re from collections import abc, defaultdict, deque from typing import ( AbstractSet, Any, AsyncGenerator, AsyncIterable, AsyncIterator, Awaitable, Collection, Container, Coroutine, DefaultDict, Deque, Dict, FrozenSet, Generator, ItemsView, Iterable, Iterator, KeysView, List, Mapping, MappingView, MutableMapping, MutableSequence, MutableSet, Reversible, Sequence, Set, Tuple, TypeVar, Union, ValuesView, cast, ) from typing_extensions import Annotated, NewType, NotRequired, Required, get_args, get_origin, get_type_hints from litestar.types.builtin_types import NoneType, UnionTypes __all__ = ( "get_instantiable_origin", "get_origin_or_inner_type", "get_safe_generic_origin", "instantiable_type_mapping", "make_non_optional_union", "safe_generic_origin_map", "unwrap_annotation", ) T = TypeVar("T") UnionT = TypeVar("UnionT", bound="Union") tuple_types_regex = re.compile( "^" + "|".join( [*[repr(x) for x in (List, Sequence, Iterable, Iterator, Tuple, Deque)], "tuple", "list", "collections.deque"] ) ) instantiable_type_mapping = { AbstractSet: set, DefaultDict: defaultdict, Deque: deque, Dict: dict, FrozenSet: frozenset, List: list, Mapping: dict, MutableMapping: dict, MutableSequence: list, MutableSet: set, Sequence: list, Set: set, Tuple: tuple, abc.Mapping: dict, abc.MutableMapping: dict, abc.MutableSequence: list, abc.MutableSet: set, abc.Sequence: list, abc.Set: set, defaultdict: defaultdict, deque: deque, dict: dict, frozenset: frozenset, list: list, set: set, tuple: tuple, } safe_generic_origin_map = { set: AbstractSet, defaultdict: DefaultDict, deque: Deque, dict: Dict, frozenset: FrozenSet, list: List, tuple: Tuple, abc.Mapping: Mapping, abc.MutableMapping: MutableMapping, abc.MutableSequence: MutableSequence, abc.MutableSet: MutableSet, abc.Sequence: Sequence, abc.Set: AbstractSet, abc.Collection: Collection, abc.Container: Container, abc.ItemsView: ItemsView, abc.KeysView: KeysView, abc.MappingView: MappingView, abc.ValuesView: ValuesView, abc.Iterable: Iterable, abc.Iterator: Iterator, abc.Generator: Generator, abc.Reversible: Reversible, abc.Coroutine: Coroutine, abc.AsyncGenerator: AsyncGenerator, abc.AsyncIterable: AsyncIterable, abc.AsyncIterator: AsyncIterator, abc.Awaitable: Awaitable, **{union_t: Union for union_t in UnionTypes}, } """A mapping of types to equivalent types that are safe to be used as generics across all Python versions. This is necessary because occasionally we want to rebuild a generic outer type with different args, and types such as ``collections.abc.Mapping``, are not valid generic types in Python 3.8. """ wrapper_type_set = {Annotated, Required, NotRequired} """Types that always contain a wrapped type annotation as their first arg.""" def normalize_type_annotation(annotation: Any) -> Any: """Normalize a type annotation to a standard form.""" return instantiable_type_mapping.get(annotation, annotation) def make_non_optional_union(annotation: UnionT | None) -> UnionT: """Make a :data:`Union ` type that excludes ``NoneType``. Args: annotation: A type annotation. Returns: The union with all original members, except ``NoneType``. """ args = tuple(tp for tp in get_args(annotation) if tp is not NoneType) return cast("UnionT", Union[args]) # pyright: ignore def unwrap_annotation(annotation: Any) -> tuple[Any, tuple[Any, ...], set[Any]]: """Remove "wrapper" annotation types, such as ``Annotated``, ``Required``, and ``NotRequired``. Note: ``annotation`` should have been retrieved from :func:`get_type_hints()` with ``include_extras=True``. This ensures that any nested ``Annotated`` types are flattened according to the PEP 593 specification. Args: annotation: A type annotation. Returns: A tuple of the unwrapped annotation and any ``Annotated`` metadata, and a set of any wrapper types encountered. """ origin = get_origin(annotation) wrappers = set() metadata = [] while origin in wrapper_type_set: wrappers.add(origin) annotation, *meta = get_args(annotation) metadata.extend(meta) origin = get_origin(annotation) return annotation, tuple(metadata), wrappers def unwrap_new_type(new_type: Any) -> Any: """Unwrap a (nested) ``typing.NewType``""" inner = new_type while isinstance(inner, NewType): inner = inner.__supertype__ return inner def get_origin_or_inner_type(annotation: Any) -> Any: """Get origin or unwrap it. Returns None for non-generic types. Args: annotation: A type annotation. Returns: Any type. """ origin = get_origin(annotation) if origin in wrapper_type_set: inner, _, _ = unwrap_annotation(annotation) # we need to recursively call here 'get_origin_or_inner_type' because we might be dealing # with a generic type alias e.g. Annotated[dict[str, list[int]] origin = get_origin_or_inner_type(inner) return instantiable_type_mapping.get(origin, origin) def get_safe_generic_origin(origin_type: Any, annotation: Any) -> Any: """Get a type that is safe to use as a generic type across all supported Python versions. If a builtin collection type is annotated without generic args, e.g, ``a: dict``, then the origin type will be ``None``. In this case, we can use the annotation to determine the correct generic type, if one exists. Args: origin_type: A type - would be the return value of :func:`get_origin()`. annotation: Type annotation associated with the origin type. Should be unwrapped from any wrapper types, such as ``Annotated``. Returns: The ``typing`` module equivalent of the given type, if it exists. Otherwise, the original type is returned. """ if origin_type is None: return safe_generic_origin_map.get(annotation) return safe_generic_origin_map.get(origin_type, origin_type) def get_instantiable_origin(origin_type: Any, annotation: Any) -> Any: """Get a type that is safe to instantiate for the given origin type. If a builtin collection type is annotated without generic args, e.g, ``a: dict``, then the origin type will be ``None``. In this case, we can use the annotation to determine the correct instantiable type, if one exists. Args: origin_type: A type - would be the return value of :func:`get_origin()`. annotation: Type annotation associated with the origin type. Should be unwrapped from any wrapper types, such as ``Annotated``. Returns: A builtin type that is safe to instantiate for the given origin type. """ if origin_type is None: return instantiable_type_mapping.get(annotation) return instantiable_type_mapping.get(origin_type, origin_type) def get_type_hints_with_generics_resolved( annotation: Any, globalns: dict[str, Any] | None = None, localns: dict[str, Any] | None = None, include_extras: bool = False, type_hints: dict[str, Any] | None = None, ) -> dict[str, Any]: """Get the type hints for the given object after resolving the generic types as much as possible. Args: annotation: A type annotation. globalns: The global namespace. localns: The local namespace. include_extras: A flag indicating whether to include the ``Annotated[T, ...]`` or not. type_hints: Already resolved type hints """ origin = get_origin(annotation) if origin is None: # Implies the generic types have not been specified in the annotation if type_hints is None: # pragma: no cover type_hints = get_type_hints(annotation, globalns=globalns, localns=localns, include_extras=include_extras) typevar_map = {p: p for p in annotation.__parameters__} else: if type_hints is None: # pragma: no cover type_hints = get_type_hints(origin, globalns=globalns, localns=localns, include_extras=include_extras) # the __parameters__ is only available on the origin itself and not the annotation typevar_map = dict(zip(origin.__parameters__, get_args(annotation))) return {n: _substitute_typevars(type_, typevar_map) for n, type_ in type_hints.items()} def expand_type_var_in_type_hint(type_hint: dict[str, Any], namespace: dict[str, Any] | None) -> dict[str, Any]: """Expand TypeVar for any parameters in type_hint Args: type_hint: mapping of parameter to type obtained from calling `get_type_hints` or `get_fn_type_hints` namespace: mapping of TypeVar to concrete type Returns: type_hint with any TypeVar parameter expanded """ if namespace: return {name: _substitute_typevars(hint, namespace) for name, hint in type_hint.items()} return type_hint def _substitute_typevars(obj: Any, typevar_map: Mapping[Any, Any]) -> Any: if params := getattr(obj, "__parameters__", None): args = tuple(_substitute_typevars(typevar_map.get(p, p), typevar_map) for p in params) return obj[args] if isinstance(obj, TypeVar): # If there's a mapped type for the TypeVar already, then it should be returned instead # of considering __constraints__ or __bound__. For a generic `Foo[T]`, if Foo[int] is given # then int should be returned and if `Foo` is given then the __bounds__ and __constraints__ # should be considered. if (type_ := typevar_map.get(obj, None)) is not None and not isinstance(type_, TypeVar): return type_ if obj.__bound__ is not None: return obj.__bound__ if obj.__constraints__: return Union[obj.__constraints__] # pyright: ignore return obj litestar-2.16.0/litestar/utils/version.py000066400000000000000000000035551500564371300204710ustar00rootroot00000000000000from __future__ import annotations import re import sys from typing import Literal, NamedTuple __all__ = ("Version", "get_version", "parse_version") if sys.version_info >= (3, 10): import importlib.metadata as importlib_metadata else: import importlib_metadata _ReleaseLevel = Literal["alpha", "beta", "rc", "final"] _PRE_RELEASE_TAGS = {"alpha", "a", "beta", "b", "rc"} _PRE_RELEASE_TAGS_CONVERSIONS: dict[str, _ReleaseLevel] = {"a": "alpha", "b": "beta"} _VERSION_PARTS_RE = re.compile(r"(\d+|[a-z]+|\.)") class Version(NamedTuple): """Litestar version information""" major: int minor: int patch: int release_level: _ReleaseLevel serial: int def formatted(self, short: bool = False) -> str: version = f"{self.major}.{self.minor}.{self.patch}" if not short: version += f"{self.release_level}{self.serial}" return version def parse_version(raw_version: str) -> Version: """Parse a version string into a :class:`Version`""" parts = [p for p in _VERSION_PARTS_RE.split(raw_version) if p and p != "."] release_level: _ReleaseLevel = "final" serial = "0" if len(parts) == 3: major, minor, patch = parts elif len(parts) == 5: major, minor, patch, release_level, serial = parts # type: ignore[assignment] if release_level not in _PRE_RELEASE_TAGS: raise ValueError(f"Invalid release level: {release_level}") release_level = _PRE_RELEASE_TAGS_CONVERSIONS.get(release_level, release_level) else: raise ValueError(f"Invalid version: {raw_version}") return Version( major=int(major), minor=int(minor), patch=int(patch), release_level=release_level, serial=int(serial) ) def get_version() -> Version: """Get the version of the installed litestar package""" return parse_version(importlib_metadata.version("litestar")) litestar-2.16.0/litestar/utils/warnings.py000066400000000000000000000060511500564371300206260ustar00rootroot00000000000000from __future__ import annotations import os import warnings from typing import TYPE_CHECKING from litestar.exceptions import LitestarWarning if TYPE_CHECKING: import re from litestar.types import AnyCallable, AnyGenerator def warn_implicit_sync_to_thread(source: AnyCallable, stacklevel: int = 2) -> None: if os.getenv("LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD") == "0": return warnings.warn( f"Use of a synchronous callable {source} without setting sync_to_thread is " "discouraged since synchronous callables can block the main thread if they " "perform blocking operations. If the callable is guaranteed to be non-blocking, " "you can set sync_to_thread=False to skip this warning, or set the environment" "variable LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD=0 to disable warnings of this " "type entirely.", category=LitestarWarning, stacklevel=stacklevel, ) def warn_sync_to_thread_with_async_callable(source: AnyCallable, stacklevel: int = 2) -> None: if os.getenv("LITESTAR_WARN_SYNC_TO_THREAD_WITH_ASYNC") == "0": return warnings.warn( f"Use of an asynchronous callable {source} with sync_to_thread; sync_to_thread " "has no effect on async callable. You can disable this warning by setting " "LITESTAR_WARN_SYNC_TO_THREAD_WITH_ASYNC=0", category=LitestarWarning, stacklevel=stacklevel, ) def warn_sync_to_thread_with_generator(source: AnyGenerator, stacklevel: int = 2) -> None: if os.getenv("LITESTAR_WARN_SYNC_TO_THREAD_WITH_GENERATOR") == "0": return warnings.warn( f"Use of generator {source} with sync_to_thread; sync_to_thread has no effect " "on generators. You can disable this warning by setting " "LITESTAR_WARN_SYNC_TO_THREAD_WITH_GENERATOR=0", category=LitestarWarning, stacklevel=stacklevel, ) def warn_pdb_on_exception(stacklevel: int = 2) -> None: warnings.warn("Python Debugger on exception enabled", category=LitestarWarning, stacklevel=stacklevel) def warn_middleware_excluded_on_all_routes( pattern: re.Pattern, middleware_cls: type | None = None, ) -> None: middleware_name = f" {middleware_cls.__name__!r}" if middleware_cls else "" warnings.warn( f"Middleware{middleware_name} exclude pattern {pattern.pattern!r} greedily " "matches all paths, effectively disabling this middleware. If this was " "intentional, consider removing this middleware entirely", category=LitestarWarning, stacklevel=2, ) def warn_signature_namespace_override(signature_key: str, stacklevel: int = 2) -> None: if os.getenv("LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE") == "0": return warnings.warn( f"Type '{signature_key}' is already defined as a different type in the signature namespace" "If this is intentional, you can disable this warning by setting " "LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE=0", category=LitestarWarning, stacklevel=stacklevel, ) litestar-2.16.0/pyproject.toml000066400000000000000000000367001500564371300163550ustar00rootroot00000000000000[project] authors = [ { name = "Cody Fincher", email = "cody@litestar.dev" }, { name = "Jacob Coffee", email = "jacob@litestar.dev" }, { name = "Janek Nouvertné", email = "janek@litestar.dev" }, { name = "Na'aman Hirschfeld", email = "nhirschfeld@gmail.com" }, { name = "Peter Schutt", email = "peter@litestar.dev" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries", "Topic :: Software Development", "Typing :: Typed", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ "anyio>=3", "httpx>=0.22", "exceptiongroup; python_version < \"3.11\"", "importlib-metadata; python_version < \"3.10\"", "importlib-resources>=5.12.0; python_version < \"3.9\"", "msgspec>=0.18.2", "multidict>=6.0.2", "polyfactory>=2.6.3", "pyyaml", "typing-extensions", "click", "rich>=13.0.0", "rich-click", "multipart>=1.2.0", "exceptiongroup>=1.2.2; python_version < \"3.11\"", # default litestar plugins "litestar-htmx>=0.4.0", ] description = "Litestar - A production-ready, highly performant, extensible ASGI API Framework" keywords = ["api", "rest", "asgi", "litestar", "starlite"] license = { text = "MIT" } maintainers = [ { name = "Litestar Developers", email = "hello@litestar.dev" }, { name = "Cody Fincher", email = "cody@litestar.dev" }, { name = "Jacob Coffee", email = "jacob@litestar.dev" }, { name = "Janek Nouvertné", email = "janek@litestar.dev" }, { name = "Visakh Unnikrishnan", email = "guacs@litestar.dev" }, { name = "Julien Courtes", email = "julien@litestar.dev" }, ] name = "litestar" readme = "README.md" requires-python = ">=3.8,<4.0" version = "2.16.0" [project.urls] Blog = "https://blog.litestar.dev" Changelog = "https://github.com/litestar-org/litestar/releases/" Discord = "https://discord.gg/litestar" Discussions = "https://github.com/litestar-org/litestar/discussions" Documentation = "https://docs.litestar.dev/" Funding = "https://github.com/sponsors/litestar-org" Homepage = "https://litestar.dev/" "Issue Tracker" = "https://github.com/litestar-org/litestar/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" Reddit = "https://www.reddit.com/r/LitestarAPI" Repository = "https://github.com/litestar-org/litestar" Twitter = "https://twitter.com/LitestarAPI" [project.optional-dependencies] annotated-types = ["annotated-types"] attrs = ["attrs"] brotli = ["brotli"] cli = ["jsbeautifier", "uvicorn[standard]", "uvloop>=0.18.0; sys_platform != 'win32'"] cryptography = ["cryptography"] full = [ "litestar[annotated-types,attrs,brotli,cli,cryptography,jinja,jwt,mako,minijinja,opentelemetry,piccolo,picologging,prometheus,pydantic,redis,sqlalchemy,standard,structlog,valkey]; python_version < \"3.13\"", "litestar[annotated-types,attrs,brotli,cli,cryptography,jinja,jwt,mako,minijinja,opentelemetry,piccolo,prometheus,pydantic,redis,sqlalchemy,standard,structlog,valkey]; python_version >= \"3.13\"", ] jinja = ["jinja2>=3.1.2"] jwt = ["cryptography", "pyjwt>=2.9.0"] mako = ["mako>=1.2.4"] minijinja = ["minijinja>=1.0.0"] opentelemetry = ["opentelemetry-instrumentation-asgi"] piccolo = ["piccolo"] picologging = ["picologging; python_version < \"3.13\""] prometheus = ["prometheus-client"] pydantic = [ "pydantic", "email-validator", "pydantic-extra-types!=2.9.0; python_version < \"3.9\"", "pydantic-extra-types; python_version >= \"3.9\"", ] redis = ["redis[hiredis]>=4.4.4"] sqlalchemy = ["advanced-alchemy>=0.2.2"] standard = [ "jinja2", "jsbeautifier", "uvicorn[standard]", "uvloop>=0.18.0; sys_platform != 'win32'", "fast-query-parsers>=1.0.2", ] structlog = ["structlog"] valkey = ["valkey[libvalkey]>=6.0.2"] [project.scripts] litestar = "litestar.__main__:run_cli" [tool.hatch.build.targets.sdist] include = ['docs/PYPI_README.md', '/litestar'] [tool.uv] default-groups = ["dev", "linting", "test", "docs"] [dependency-groups] dev = [ "litestar[full]", "beanie>=1.21.0", "beautifulsoup4", "fsspec", "greenlet", "hypothesis", "python-dotenv", "starlette", "trio", "aiosqlite", "asyncpg>=0.29.0", "psycopg[pool,binary]>=3.1.10,<3.2; python_version < \"3.13\"", "psycopg[pool,c]; python_version >= \"3.13\" and sys_platform == 'linux'", "psycopg[pool]; python_version >= \"3.13\" and sys_platform != 'linux'", "psycopg2-binary", "psutil>=5.9.8", "hypercorn>=0.16.0", "daphne>=4.0.0", "opentelemetry-sdk", "httpx-sse", ] docs = [ "sphinx>=7.1.2", "sphinx-autobuild>=2021.3.14", "sphinx-copybutton>=0.5.2", "sphinx-toolbox>=3.5.0", "sphinx-design>=0.5.0", "sphinx-click>=4.4.0", "sphinxcontrib-mermaid>=0.9.2", "auto-pytabs[sphinx]>=0.5.0", "litestar-sphinx-theme @ git+https://github.com/litestar-org/litestar-sphinx-theme.git", "sphinx-paramlinks>=0.6.0", ] linting = [ "ruff>=0.2.1", "mypy", "pre-commit", "slotscheck", "codecov-cli", "pyright==1.1.344", "asyncpg-stubs", "types-beautifulsoup4", "types-pyyaml", "types-redis", "types-psutil", ] test = [ "covdefaults", "pytest", "pytest-asyncio<=0.24.0; python_version < \"3.9\"", "pytest-asyncio>0.24.0; python_version >= \"3.9\"", "pytest-cov", "pytest-lazy-fixtures", "pytest-mock", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist", "time-machine", ] [build-system] build-backend = "hatchling.build" requires = ["hatchling"] [tool.coverage.run] concurrency = ["multiprocessing", "thread"] omit = ["*/tests/*", "*/litestar/plugins/sqlalchemy.py", "*/litestar/_kwargs/types.py"] parallel = true plugins = ["covdefaults"] source = ["litestar"] [tool.coverage.report] exclude_lines = ['except ImportError\b', 'if VERSION.startswith("1"):', 'if pydantic.VERSION.startswith("1"):'] fail_under = 50 [tool.pytest.ini_options] addopts = "--strict-markers --strict-config --dist=loadgroup -m 'not server_integration'" asyncio_default_fixture_loop_scope = "function" asyncio_mode = "auto" filterwarnings = [ "error", # https://github.com/pytest-dev/pytest-asyncio/issues/724 "default:.*socket.socket:pytest.PytestUnraisableExceptionWarning", "ignore::trio.TrioDeprecationWarning:anyio._backends._trio*:", "ignore::DeprecationWarning:pkg_resources.*", "ignore::DeprecationWarning:google.rpc", "ignore::DeprecationWarning:google.gcloud", "ignore::DeprecationWarning:google.iam", "ignore::DeprecationWarning:google", "ignore::DeprecationWarning:sphinxcontrib", "ignore::DeprecationWarning:litestar.*", "ignore::pydantic.PydanticDeprecatedSince20::", "ignore:`general_plain_validator_function`:DeprecationWarning::", "ignore: 'RichMultiCommand':DeprecationWarning::", # this is coming from rich_click itself, nothing we can do about # that for now "ignore: Dropping max_length:litestar.exceptions.LitestarWarning:litestar.contrib.piccolo", "ignore: Python Debugger on exception enabled:litestar.exceptions.LitestarWarning:", "ignore: datetime.datetime.utcnow:DeprecationWarning:time_machine", ] markers = [ "sqlalchemy_integration: SQLAlchemy integration tests", "server_integration: Test integration with ASGI server", ] testpaths = ["tests", "docs/examples/testing"] xfail_strict = true [tool.mypy] enable_error_code = [ "truthy-bool", "truthy-iterable", "unused-awaitable", "ignore-without-code", "possibly-undefined", "redundant-self", ] packages = ["litestar", "tests"] plugins = ["pydantic.mypy"] python_version = "3.8" disallow_any_generics = false local_partial_types = true show_error_codes = true strict = true warn_unreachable = true [[tool.mypy.overrides]] ignore_errors = true module = ["tests.examples.*", "tests.docker_service_fixtures"] [[tool.mypy.overrides]] disable_error_code = ["truthy-bool"] module = ["tests.*"] [[tool.mypy.overrides]] disable_error_code = ["assignment"] module = ["tests.unit.test_logging.*"] [[tool.mypy.overrides]] disallow_untyped_decorators = false module = ["tests.unit.test_kwargs.test_reserved_kwargs_injection"] [[tool.mypy.overrides]] module = ["tests.unit.test_contrib.test_repository"] strict_equality = false [[tool.mypy.overrides]] disable_error_code = "index, union-attr" module = ["tests.unit.test_plugins.test_pydantic.test_openapi", "litestar._asgi.routing_trie.traversal"] [[tool.mypy.overrides]] disable_error_code = "arg-type, comparison-overlap, unreachable" module = ["tests.unit.test_channels.test_subscriber", "tests.unit.test_response.test_streaming_response"] [[tool.mypy.overrides]] ignore_missing_imports = true module = [ "brotli.*", "fsspec.*", "jsbeautifier.*", "pytimeparse.*", "importlib_resources", "exceptiongroup", "picologging", "picologging.*", ] [[tool.mypy.overrides]] module = [ "litestar.contrib.sqlalchemy.*", "litestar.plugins.pydantic.*", "tests.unit.test_contrib.test_sqlalchemy", "tests.unit.test_contrib.test_pydantic.*", "tests.unit.test_logging.test_logging_config", "litestar.openapi.spec.base", "litestar.utils.helpers", "litestar.channels.plugin", "litestar.handlers.http_handlers._utils", ] warn_unused_ignores = false [[tool.mypy.overrides]] disable_error_code = "arg-type" module = ["litestar.openapi.spec.base", "litestar._asgi.routin_trie.traversal", "litestar.plugins.pydantic.plugins.int"] warn_unused_ignores = false [tool.pydantic-mypy] init_forbid_extra = true init_typed = true warn_required_dynamic_aliases = true warn_untyped_fields = true [tool.pyright] disableBytesTypePromotions = true exclude = [ "test_apps", "tools", "docs", "tests/examples", "tests/docker_service_fixtures.py", "litestar/plugins/pydantic/plugins/di.py", "litestar/plugins/pydantic/plugins/init.py", "litestar/plugins/pydantic/plugins/schema.py", "litestar/plugins/pydantic/dto_factory.py", "tests/unit/test_contrib/test_sqlalchemy.py", ] include = ["litestar", "tests"] pythonVersion = "3.8" reportUnnecessaryTypeIgnoreComments = true [tool.slotscheck] exclude-classes = """ ( # github.com/python/cpython/pull/106771 (^litestar.events.emitter:BaseEventEmitterBackend) # review these as time permits |(^litestar.connection.base:ASGIConnection) |(^litestar.datastructures.state:ImmutableState) |(^litestar.datastructures.state:State) |(^litestar.dto.base_dto:AbstractDTO) |(^litestar.dto.data_structures:DTOData) |(^litestar.middleware.session.base:BaseSessionBackend) |(^litestar.pagination:ClassicPagination) |(^litestar.pagination:CursorPagination) |(^litestar.response.base:Response) |(^litestar.testing.client.base:BaseTestClient) |(^litestar.testing.life_span_handler:LifeSpanHandler) |(^litestar.utils.sync:AsyncIteratorWrapper) ) """ strict-imports = false [tool.ruff] include = ["{litestar,tests,docs,test_apps,tools}/**/*.{py,pyi}", "pyproject.toml"] lint.select = [ "A", # flake8-builtins "B", # flake8-bugbear "BLE", # flake8-blind-except "C4", # flake8-comprehensions "C90", # mccabe "D", # pydocstyle "DJ", # flake8-django "DTZ", # flake8-datetimez "E", # pycodestyle errors "ERA", # eradicate "EXE", # flake8-executable "F", # pyflakes "G", # flake8-logging-format "I", # isort "ICN", # flake8-import-conventions "ISC", # flake8-implicit-str-concat "N", # pep8-naming "PIE", # flake8-pie "PLC", # pylint - convention "PLE", # pylint - error "PLW", # pylint - warning "PTH", # flake8-use-pathlib "Q", # flake8-quotes "RET", # flake8-return "RUF", # Ruff-specific rules "S", # flake8-bandit "SIM", # flake8-simplify "T10", # flake8-debugger "T20", # flake8-print "TC", # flake8-type-checking "TID", # flake8-tidy-imports "UP", # pyupgrade "W", # pycodestyle - warning "YTT", # flake8-2020 ] lint.ignore = [ "A003", # flake8-builtins - class attribute {name} is shadowing a python builtin "B010", # flake8-bugbear - do not call setattr with a constant attribute value "D100", # pydocstyle - missing docstring in public module "D101", # pydocstyle - missing docstring in public class "D102", # pydocstyle - missing docstring in public method "D103", # pydocstyle - missing docstring in public function "D104", # pydocstyle - missing docstring in public package "D105", # pydocstyle - missing docstring in magic method "D106", # pydocstyle - missing docstring in public nested class "D107", # pydocstyle - missing docstring in __init__ "D202", # pydocstyle - no blank lines allowed after function docstring "D205", # pydocstyle - 1 blank line required between summary line and description "D415", # pydocstyle - first line should end with a period, question mark, or exclamation point "E501", # pycodestyle line too long, handled by ruff format "PLW2901", # pylint - for loop variable overwritten by assignment target "RUF012", # Ruff-specific rule - annotated with classvar "ISC001", # Ruff formatter incompatible "CPY001", # ruff - copyright notice at the top of the file ] line-length = 120 src = ["litestar", "tests", "docs/examples"] target-version = "py38" [tool.ruff.lint.pydocstyle] convention = "google" [tool.ruff.lint.mccabe] max-complexity = 12 [tool.ruff.lint.pep8-naming] classmethod-decorators = [ "classmethod", "pydantic.root_validator", "pydantic.validator", "sqlalchemy.ext.declarative.declared_attr", "sqlalchemy.orm.declared_attr.directive", "sqlalchemy.orm.declared_attr", ] [tool.ruff.lint.isort] known-first-party = ["litestar", "tests", "examples"] [tool.ruff.lint.per-file-ignores] "docs/**/*.*" = ["S", "B", "DTZ", "A", "TC", "ERA", "D", "RET"] "docs/examples/**" = ["T201"] "docs/examples/application_hooks/before_send_hook.py" = ["UP006"] "docs/examples/contrib/sqlalchemy/plugins/**/*.*" = ["UP006"] "docs/examples/contrib/sqlalchemy/sqlalchemy_declarative_models.py" = ["UP006"] "docs/examples/data_transfer_objects**/*.*" = ["UP006"] "litestar/_openapi/schema_generation/schema.py" = ["C901"] "litestar/exceptions/*.*" = ["N818"] "litestar/handlers/**/*.*" = ["N801"] "litestar/handlers/websocket_handlers/listener.py" = ["B027"] "litestar/params.py" = ["N802"] "test_apps/**/*.*" = ["D", "TRY", "EM", "S", "PTH"] "tests/**/*.*" = [ "A", "ARG", "B", "BLE", "C901", "D", "DTZ", "EM", "FBT", "G", "N", "PGH", "PIE", "PLR", "PLW", "PTH", "RSE", "S", "S101", "SIM", "TC", "TRY", "E721", ] "tests/unit/test_contrib/test_sqlalchemy/**/*.*" = ["UP006"] "tests/unit/test_openapi/test_typescript_converter/test_converter.py" = ["W293"] "tools/**/*.*" = ["D", "ARG", "EM", "TRY", "G", "FBT"] "tools/prepare_release.py" = ["S603", "S607"] [tool.ruff.format] docstring-code-format = true docstring-code-line-length = 88 [tool.unasyncd] add_editors_note = true ruff_fix = true [tool.unasyncd.files] "litestar/repository/abc/_async.py" = "litestar/repository/abc/_sync.py" [tool.unasyncd.per_file_add_replacements."litestar/repository/abc/_async.py"] "AbstractAsyncRepository" = "AbstractSyncRepository" "AsyncRepoT" = "SyncRepoT" litestar-2.16.0/test_apps/000077500000000000000000000000001500564371300154355ustar00rootroot00000000000000litestar-2.16.0/test_apps/__init__.py000066400000000000000000000000001500564371300175340ustar00rootroot00000000000000litestar-2.16.0/test_apps/asgi_with_starlette_test_app/000077500000000000000000000000001500564371300234015ustar00rootroot00000000000000litestar-2.16.0/test_apps/asgi_with_starlette_test_app/__init__.py000066400000000000000000000000001500564371300255000ustar00rootroot00000000000000litestar-2.16.0/test_apps/asgi_with_starlette_test_app/main.py000066400000000000000000000012731500564371300247020ustar00rootroot00000000000000from starlette.applications import Starlette from starlette.requests import Request from starlette.responses import PlainTextResponse from starlette.routing import Route from litestar import Litestar, asgi, get @get("/") async def litestar_index() -> str: return "Hello, world!" async def starlette_index(_: Request) -> PlainTextResponse: return PlainTextResponse({"app": "starlette"}) foo_app = asgi(path="/foo", is_mount=True)(Starlette(routes=[Route("/", starlette_index)])) bar_app = asgi(path="/bar", is_mount=True)(Litestar(route_handlers=[litestar_index])) app = Litestar(route_handlers=[foo_app, bar_app]) if __name__ == "__main__": import uvicorn uvicorn.run(app) litestar-2.16.0/test_apps/debugging/000077500000000000000000000000001500564371300173705ustar00rootroot00000000000000litestar-2.16.0/test_apps/debugging/__init__.py000066400000000000000000000000001500564371300214670ustar00rootroot00000000000000litestar-2.16.0/test_apps/debugging/main.py000066400000000000000000000031331500564371300206660ustar00rootroot00000000000000import asyncio import pdb # noqa: T100 from concurrent.futures import ThreadPoolExecutor from typing import Dict # Install this packages if you want to run this test-app import ipdb # pyright: ignore # noqa: T100 import pdbr # pyright: ignore import pudb # pyright: ignore # noqa: T100 import uvicorn from litestar import Litestar, get @get("/") async def zero_division_error() -> Dict[str, str]: """Handler function that returns a greeting dictionary.""" 1 / 0 # pyright: ignore # noqa: B018 return {"message": "ZeroDivisionError didn't occur."} pdb_app = Litestar(route_handlers=[zero_division_error], pdb_on_exception=True, debugger_module=pdb) ipdb_app = Litestar(route_handlers=[zero_division_error], pdb_on_exception=True, debugger_module=ipdb) pudb_app = Litestar(route_handlers=[zero_division_error], pdb_on_exception=True, debugger_module=pudb) pdbr_app = Litestar(route_handlers=[zero_division_error], pdb_on_exception=True, debugger_module=pdbr) def run_server(app, port): uvicorn.run(app, port=port) async def start_servers(): with ThreadPoolExecutor() as executor: # Run all the servers concurrently await asyncio.gather( asyncio.get_event_loop().run_in_executor(executor, run_server, pdb_app, 8000), asyncio.get_event_loop().run_in_executor(executor, run_server, ipdb_app, 8001), asyncio.get_event_loop().run_in_executor(executor, run_server, pudb_app, 8002), asyncio.get_event_loop().run_in_executor(executor, run_server, pdbr_app, 8003), ) if __name__ == "__main__": asyncio.run(start_servers()) litestar-2.16.0/test_apps/logging_test_app/000077500000000000000000000000001500564371300207625ustar00rootroot00000000000000litestar-2.16.0/test_apps/logging_test_app/__init__.py000066400000000000000000000000001500564371300230610ustar00rootroot00000000000000litestar-2.16.0/test_apps/logging_test_app/main.py000066400000000000000000000007601500564371300222630ustar00rootroot00000000000000from litestar import Litestar, get from litestar.logging.config import LoggingConfig from litestar.middleware.logging import LoggingMiddlewareConfig @get("/") async def handler() -> dict[str, str]: return {"hello": "world"} logging_middleware_config = LoggingMiddlewareConfig() app = Litestar( route_handlers=[handler], logging_config=LoggingConfig(), middleware=[logging_middleware_config.middleware], ) if __name__ == "__main__": import uvicorn uvicorn.run(app) litestar-2.16.0/test_apps/openapi_test_app/000077500000000000000000000000001500564371300207675ustar00rootroot00000000000000litestar-2.16.0/test_apps/openapi_test_app/__init__.py000066400000000000000000000000001500564371300230660ustar00rootroot00000000000000litestar-2.16.0/test_apps/openapi_test_app/main.py000066400000000000000000000013441500564371300222670ustar00rootroot00000000000000from __future__ import annotations import msgspec from litestar import Litestar, get from litestar.openapi.config import OpenAPIConfig from litestar.openapi.plugins import ScalarRenderPlugin from tests.unit.test_openapi.conftest import create_person_controller, create_pet_controller class Model(msgspec.Struct): hello: str = "world" @get("/", sync_to_thread=False) def greet() -> Model: return Model(hello="world") app = Litestar( route_handlers=[greet, create_person_controller(), create_pet_controller()], openapi_config=OpenAPIConfig( title="whatever", version="0.0.1", render_plugins=[ScalarRenderPlugin()], ), ) if __name__ == "__main__": import uvicorn uvicorn.run(app) litestar-2.16.0/test_apps/piccolo_admin_test_app/000077500000000000000000000000001500564371300221345ustar00rootroot00000000000000litestar-2.16.0/test_apps/piccolo_admin_test_app/__init__.py000066400000000000000000000000001500564371300242330ustar00rootroot00000000000000litestar-2.16.0/test_apps/piccolo_admin_test_app/home/000077500000000000000000000000001500564371300230645ustar00rootroot00000000000000litestar-2.16.0/test_apps/piccolo_admin_test_app/home/__init__.py000066400000000000000000000000001500564371300251630ustar00rootroot00000000000000litestar-2.16.0/test_apps/piccolo_admin_test_app/home/piccolo_app.py000066400000000000000000000006041500564371300257260ustar00rootroot00000000000000import os from piccolo.conf.apps import AppConfig, table_finder CURRENT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) APP_CONFIG = AppConfig( app_name="home", migrations_folder_path=os.path.join(CURRENT_DIRECTORY, "piccolo_migrations"), table_classes=table_finder(modules=["home.tables"], exclude_imported=True), migration_dependencies=[], commands=[], ) litestar-2.16.0/test_apps/piccolo_admin_test_app/home/piccolo_migrations/000077500000000000000000000000001500564371300267505ustar00rootroot00000000000000litestar-2.16.0/test_apps/piccolo_admin_test_app/home/piccolo_migrations/.gitkeep000066400000000000000000000000001500564371300303670ustar00rootroot00000000000000litestar-2.16.0/test_apps/piccolo_admin_test_app/home/schema.py000066400000000000000000000005121500564371300246740ustar00rootroot00000000000000from piccolo.utils.pydantic import create_pydantic_model from test_apps.piccolo_admin_test_app.home.tables import Task # task models TaskModelIn = create_pydantic_model(table=Task, model_name="TaskModelIn") TaskModelOut = create_pydantic_model( table=Task, include_default_columns=True, model_name="TaskModelOut", ) litestar-2.16.0/test_apps/piccolo_admin_test_app/home/tables.py000066400000000000000000000010101500564371300247000ustar00rootroot00000000000000from piccolo.apps.user.tables import BaseUser from piccolo.columns import Boolean, ForeignKey, Timestamp, Varchar from piccolo.columns.readable import Readable from piccolo.table import Table from piccolo_conf import DB class Task(Table, db=DB): """An example table.""" name = Varchar() completed = Boolean() created_at = Timestamp() task_user = ForeignKey(BaseUser) @classmethod def get_readable(cls) -> Readable: return Readable(template="%s", columns=[cls.task_user.username]) litestar-2.16.0/test_apps/piccolo_admin_test_app/home/templates/000077500000000000000000000000001500564371300250625ustar00rootroot00000000000000litestar-2.16.0/test_apps/piccolo_admin_test_app/home/templates/.gitkeep000066400000000000000000000000001500564371300265010ustar00rootroot00000000000000litestar-2.16.0/test_apps/piccolo_admin_test_app/main.py000066400000000000000000000045311500564371300234350ustar00rootroot00000000000000import asyncio from typing import TYPE_CHECKING, List from home.piccolo_app import APP_CONFIG from home.tables import Task from piccolo.apps.user.tables import BaseUser from piccolo_admin.endpoints import create_admin # pyright: ignore from piccolo_api.session_auth.tables import SessionsBase from litestar import Litestar, asgi, delete, get, patch, post from litestar.contrib.piccolo_orm import PiccoloORMPlugin from litestar.exceptions import NotFoundException if TYPE_CHECKING: from litestar.types import Receive, Scope, Send @asgi("/admin/", is_mount=True) async def admin(scope: "Scope", receive: "Receive", send: "Send") -> None: await create_admin(tables=APP_CONFIG.table_classes)(scope, receive, send) @get("/tasks", tags=["Task"]) async def tasks() -> List[Task]: return await Task.select().order_by(Task.id, ascending=False) @post("/tasks", tags=["Task"]) async def create_task(data: Task) -> Task: task = Task(**data.to_dict()) await task.save() return task @patch("/tasks/{task_id:int}", tags=["Task"]) async def update_task(task_id: int, data: Task) -> Task: task = await Task.objects().get(Task.id == task_id) if not task: raise NotFoundException("task does not exist") for key, value in data.to_dict().items(): task.id = task_id setattr(task, key, value) await task.save() return task @delete("/tasks/{task_id:int}", tags=["Task"]) async def delete_task(task_id: int) -> None: task = await Task.objects().get(Task.id == task_id) if task: await task.remove() async def main() -> None: # Creating tables await BaseUser.create_table(if_not_exists=True) await SessionsBase.create_table(if_not_exists=True) await Task.create_table(if_not_exists=True) # Creating admin user if not await BaseUser.exists().where(BaseUser.email == "admin@test.com"): user = BaseUser( username="piccolo", password="piccolo123", email="admin@test.com", admin=True, active=True, superuser=True, ) await user.save() app = Litestar( route_handlers=[ admin, tasks, create_task, update_task, delete_task, ], plugins=[PiccoloORMPlugin()], ) if __name__ == "__main__": asyncio.run(main()) import uvicorn uvicorn.run(app) litestar-2.16.0/test_apps/piccolo_admin_test_app/piccolo_conf.py000066400000000000000000000001171500564371300251420ustar00rootroot00000000000000from piccolo.engine import SQLiteEngine DB = SQLiteEngine(path="test.sqlite") litestar-2.16.0/test_apps/pydantic_1_app.py000066400000000000000000000011441500564371300207020ustar00rootroot00000000000000import unittest from typing import List import pydantic from litestar import post from litestar.testing import create_test_client class Foo(pydantic.BaseModel): bar: str baz: List[str] @post("/") async def handler(data: Foo) -> Foo: return data class TestApp(unittest.TestCase): def test_app(self) -> None: assert pydantic.__version__.startswith("1."), pydantic.__version__ with create_test_client([handler]) as client: data = {"bar": "baz", "baz": ["a", "b", "c"]} res = client.post("/", json=data) self.assertEqual(res.json(), data) litestar-2.16.0/test_apps/sse/000077500000000000000000000000001500564371300162275ustar00rootroot00000000000000litestar-2.16.0/test_apps/sse/__init__.py000066400000000000000000000000001500564371300203260ustar00rootroot00000000000000litestar-2.16.0/test_apps/sse/sse.html000066400000000000000000000017761500564371300177220ustar00rootroot00000000000000

Server-Sent Events

litestar-2.16.0/test_apps/sse/sse_empty.py000066400000000000000000000011471500564371300206140ustar00rootroot00000000000000from typing import AsyncIterator import uvicorn from litestar import Litestar, get from litestar.config.cors import CORSConfig from litestar.response import ServerSentEvent, ServerSentEventMessage from litestar.types import SSEData @get("/test_sse_empty") async def handler() -> ServerSentEvent: async def generate() -> AsyncIterator[SSEData]: event = ServerSentEventMessage(event="empty") yield event return ServerSentEvent(generate()) app = Litestar(route_handlers=[handler], cors_config=CORSConfig(allow_origins=["*"])) if __name__ == "__main__": uvicorn.run("sse_empty:app") litestar-2.16.0/test_apps/static_files_test_app/000077500000000000000000000000001500564371300220055ustar00rootroot00000000000000litestar-2.16.0/test_apps/static_files_test_app/__init__.py000066400000000000000000000000001500564371300241040ustar00rootroot00000000000000litestar-2.16.0/test_apps/static_files_test_app/main.py000066400000000000000000000007001500564371300233000ustar00rootroot00000000000000from pathlib import Path from litestar import Litestar, get from litestar.static_files.config import StaticFilesConfig @get("/") async def handler() -> dict[str, str]: return {"hello": "world"} app = Litestar( route_handlers=[], static_files_config=[ StaticFilesConfig(directories=[Path(__file__).parent / "public"], path="/", html_mode=True), ], ) if __name__ == "__main__": import uvicorn uvicorn.run(app) litestar-2.16.0/test_apps/static_files_test_app/public/000077500000000000000000000000001500564371300232635ustar00rootroot00000000000000litestar-2.16.0/test_apps/static_files_test_app/public/index.html000066400000000000000000000001331500564371300252550ustar00rootroot00000000000000
Hello World!
litestar-2.16.0/test_apps/structlog_app/000077500000000000000000000000001500564371300203235ustar00rootroot00000000000000litestar-2.16.0/test_apps/structlog_app/__init__.py000066400000000000000000000000001500564371300224220ustar00rootroot00000000000000litestar-2.16.0/test_apps/structlog_app/main.py000066400000000000000000000011671500564371300216260ustar00rootroot00000000000000from typing import Dict from litestar import Litestar, Request, get from litestar.logging.config import StructLoggingConfig from litestar.middleware.logging import LoggingMiddlewareConfig @get("/") async def handler(request: Request) -> Dict[str, str]: request.logger.info("Logging in the handler") return {"hello": "world"} logging_middleware_config = LoggingMiddlewareConfig() app = Litestar( route_handlers=[handler], logging_config=StructLoggingConfig(log_exceptions="always"), middleware=[logging_middleware_config.middleware], ) if __name__ == "__main__": import uvicorn uvicorn.run(app) litestar-2.16.0/tests/000077500000000000000000000000001500564371300145755ustar00rootroot00000000000000litestar-2.16.0/tests/__init__.py000066400000000000000000000000001500564371300166740ustar00rootroot00000000000000litestar-2.16.0/tests/conftest.py000066400000000000000000000260231500564371300167770ustar00rootroot00000000000000from __future__ import annotations import importlib.util import logging import os import random import shutil import string import sys from datetime import datetime from os import urandom from pathlib import Path from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Generator, Union, cast from unittest.mock import AsyncMock, MagicMock import pytest from pytest_lazy_fixtures import lf from redis.asyncio import Redis as AsyncRedis from redis.client import Redis from time_machine import travel from valkey.asyncio import Valkey as AsyncValkey from valkey.client import Valkey from litestar.logging import LoggingConfig from litestar.middleware.session import SessionMiddleware from litestar.middleware.session.base import BaseSessionBackend from litestar.middleware.session.client_side import ClientSideSessionBackend, CookieBackendConfig from litestar.middleware.session.server_side import ServerSideSessionBackend, ServerSideSessionConfig from litestar.openapi.config import OpenAPIConfig from litestar.stores.base import Store from litestar.stores.file import FileStore from litestar.stores.memory import MemoryStore from litestar.stores.redis import RedisStore from litestar.stores.valkey import ValkeyStore from litestar.testing import RequestFactory from tests.helpers import not_none if TYPE_CHECKING: from types import ModuleType from pytest import FixtureRequest, MonkeyPatch from time_machine import Coordinates from litestar import Litestar from litestar.types import ( AnyIOBackend, ASGIApp, ASGIVersion, GetLogger, Receive, RouteHandlerType, Scope, ScopeSession, Send, ) pytest_plugins = ["tests.docker_service_fixtures"] @pytest.fixture def mock() -> MagicMock: return MagicMock() @pytest.fixture() def async_mock() -> AsyncMock: return AsyncMock() @pytest.fixture(params=[pytest.param("asyncio", id="asyncio"), pytest.param("trio", id="trio")]) def anyio_backend(request: pytest.FixtureRequest) -> str: return request.param # type: ignore[no-any-return] @pytest.fixture() def mock_asgi_app() -> ASGIApp: async def asgi_app(scope: Scope, receive: Receive, send: Send) -> None: ... return asgi_app @pytest.fixture() def redis_store(redis_client: AsyncRedis) -> RedisStore: return RedisStore(redis=redis_client) @pytest.fixture() def valkey_store(valkey_client: AsyncValkey) -> ValkeyStore: return ValkeyStore(valkey=valkey_client) @pytest.fixture() def memory_store() -> MemoryStore: return MemoryStore() @pytest.fixture() def file_store(tmp_path: Path) -> FileStore: return FileStore(path=tmp_path) @pytest.fixture() def file_store_create_directories(tmp_path: Path) -> FileStore: path = tmp_path / "subdir1" / "subdir2" return FileStore(path=path, create_directories=True) @pytest.fixture() def file_store_create_directories_flag_false(tmp_path: Path) -> FileStore: shutil.rmtree(tmp_path, ignore_errors=True) # in case the path was already created by different tests - we clean it return FileStore(path=tmp_path.joinpath("subdir"), create_directories=False) @pytest.fixture( params=[ pytest.param("redis_store", marks=pytest.mark.xdist_group("redis")), pytest.param("valkey_store", marks=pytest.mark.xdist_group("valkey")), "memory_store", "file_store", ] ) def store(request: FixtureRequest) -> Store: return cast("Store", request.getfixturevalue(request.param)) @pytest.fixture def cookie_session_backend_config() -> CookieBackendConfig: return CookieBackendConfig(secret=urandom(16)) @pytest.fixture() def cookie_session_backend(cookie_session_backend_config: CookieBackendConfig) -> ClientSideSessionBackend: return ClientSideSessionBackend(config=cookie_session_backend_config) @pytest.fixture( params=[ pytest.param(lf("cookie_session_backend_config"), id="cookie"), pytest.param(lf("server_side_session_config"), id="server-side"), ] ) def session_backend_config(request: pytest.FixtureRequest) -> ServerSideSessionConfig | CookieBackendConfig: return cast("Union[ServerSideSessionConfig, CookieBackendConfig]", request.param) @pytest.fixture() def server_side_session_config() -> ServerSideSessionConfig: return ServerSideSessionConfig() @pytest.fixture() def server_side_session_backend(server_side_session_config: ServerSideSessionConfig) -> ServerSideSessionBackend: return ServerSideSessionBackend(config=server_side_session_config) @pytest.fixture( params=[ pytest.param("cookie_session_backend", id="cookie"), pytest.param("server_side_session_backend", id="server-side"), ] ) def session_backend(request: pytest.FixtureRequest) -> BaseSessionBackend: return cast("BaseSessionBackend", request.getfixturevalue(request.param)) @pytest.fixture() def session_backend_config_memory(memory_store: MemoryStore) -> ServerSideSessionConfig: return ServerSideSessionConfig() @pytest.fixture def session_middleware(session_backend: BaseSessionBackend, mock_asgi_app: ASGIApp) -> SessionMiddleware[Any]: return SessionMiddleware(app=mock_asgi_app, backend=session_backend) @pytest.fixture def cookie_session_middleware( cookie_session_backend: ClientSideSessionBackend, mock_asgi_app: ASGIApp ) -> SessionMiddleware[ClientSideSessionBackend]: return SessionMiddleware(app=mock_asgi_app, backend=cookie_session_backend) @pytest.fixture def test_client_backend(anyio_backend_name: str) -> AnyIOBackend: return cast("AnyIOBackend", anyio_backend_name) @pytest.fixture def create_scope() -> Callable[..., Scope]: def inner( *, type: str = "http", app: Litestar | None = None, asgi: ASGIVersion | None = None, auth: Any = None, client: tuple[str, int] | None = ("testclient", 50000), extensions: dict[str, dict[object, object]] | None = None, http_version: str = "1.1", path: str = "/", path_params: dict[str, str] | None = None, query_string: str = "", root_path: str = "", route_handler: RouteHandlerType | None = None, scheme: str = "http", server: tuple[str, int | None] | None = ("testserver", 80), session: ScopeSession | None = None, state: dict[str, Any] | None = None, user: Any = None, **kwargs: dict[str, Any], ) -> Scope: scope = { "app": app, "litestar_app": app, "asgi": asgi or {"spec_version": "2.0", "version": "3.0"}, "auth": auth, "type": type, "path": path, "raw_path": path.encode(), "root_path": root_path, "scheme": scheme, "query_string": query_string.encode(), "client": client, "server": server, "method": "GET", "http_version": http_version, "extensions": extensions or {"http.response.template": {}}, "state": state or {}, "path_params": path_params or {}, "route_handler": route_handler, "user": user, "session": session, "headers": [], **kwargs, } return cast("Scope", scope) return inner @pytest.fixture def scope(create_scope: Callable[..., Scope]) -> Scope: return create_scope() @pytest.fixture def create_module(tmp_path: Path, monkeypatch: MonkeyPatch) -> Callable[[str], ModuleType]: """Utility fixture for dynamic module creation.""" def wrapped(source: str) -> ModuleType: """ Args: source: Source code as a string. Returns: An imported module. """ def module_name_generator() -> str: letters = string.ascii_lowercase return "".join(random.choice(letters) for _ in range(10)) module_name = module_name_generator() path = tmp_path / f"{module_name}.py" path.write_text(source) # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly spec = not_none(importlib.util.spec_from_file_location(module_name, path)) module = not_none(importlib.util.module_from_spec(spec)) monkeypatch.setitem(sys.modules, module_name, module) not_none(spec.loader).exec_module(module) return module return wrapped @pytest.fixture() def frozen_datetime() -> Generator[Coordinates, None, None]: with travel(datetime.utcnow, tick=False) as frozen: yield frozen @pytest.fixture() def request_factory() -> RequestFactory: return RequestFactory() @pytest.fixture() def reset_httpx_logging() -> Generator[None, None, None]: # ensure that httpx logging is not interfering with our test client httpx_logger = logging.getLogger("httpx") initial_level = httpx_logger.level httpx_logger.setLevel(logging.WARNING) yield httpx_logger.setLevel(initial_level) # the monkeypatch fixture does not work with session scoped dependencies @pytest.fixture(autouse=True, scope="session") def disable_warn_implicit_sync_to_thread() -> Generator[None, None, None]: old_value = os.getenv("LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD") os.environ["LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD"] = "0" yield if old_value is not None: os.environ["LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD"] = old_value @pytest.fixture() def disable_warn_sync_to_thread_with_async(monkeypatch: MonkeyPatch) -> None: monkeypatch.setenv("LITESTAR_WARN_SYNC_TO_THREAD_WITH_ASYNC", "0") @pytest.fixture() def enable_warn_implicit_sync_to_thread(monkeypatch: MonkeyPatch) -> None: monkeypatch.setenv("LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD", "1") @pytest.fixture def get_logger() -> GetLogger: # due to the limitations of caplog we have to place this call here. # we also have to allow propagation. return LoggingConfig( logging_module="logging", loggers={ "litestar": {"level": "INFO", "handlers": ["queue_listener"], "propagate": True}, }, ).configure() @pytest.fixture() async def redis_client(docker_ip: str, redis_service: None) -> AsyncGenerator[AsyncRedis, None]: # this is to get around some weirdness with pytest-asyncio and redis interaction # on 3.8 and 3.9 Redis(host=docker_ip, port=6397).flushall() client: AsyncRedis = AsyncRedis(host=docker_ip, port=6397) yield client try: await client.aclose() # type: ignore[attr-defined] except RuntimeError: pass @pytest.fixture() async def valkey_client(docker_ip: str, valkey_service: None) -> AsyncGenerator[AsyncValkey, None]: # this is to get around some weirdness with pytest-asyncio and valkey interaction # on 3.8 and 3.9 Valkey(host=docker_ip, port=6381).flushall() client: AsyncValkey = AsyncValkey(host=docker_ip, port=6381) yield client try: await client.aclose() except RuntimeError: pass @pytest.fixture(autouse=True) def _patch_openapi_config(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr("litestar.app.DEFAULT_OPENAPI_CONFIG", OpenAPIConfig(title="Litestar API", version="1.0.0")) litestar-2.16.0/tests/docker-compose.yml000066400000000000000000000006371500564371300202400ustar00rootroot00000000000000version: "3" services: postgres: image: postgres:latest ports: - "5423:5432" # use a non-standard port here environment: POSTGRES_PASSWORD: super-secret redis: image: redis:latest restart: always ports: - "6397:6379" # use a non-standard port here valkey: image: valkey/valkey:latest restart: always ports: - "6381:6379" # also a non-standard port litestar-2.16.0/tests/docker_service_fixtures.py000066400000000000000000000112331500564371300220670ustar00rootroot00000000000000from __future__ import annotations import asyncio import os import re import subprocess import sys import timeit from pathlib import Path from typing import Any, Awaitable, Callable, Generator import asyncpg import pytest from redis.asyncio import Redis as AsyncRedis from redis.exceptions import ConnectionError as RedisConnectionError from valkey.asyncio import Valkey as AsyncValkey from valkey.exceptions import ConnectionError as ValkeyConnectionError from litestar.utils import ensure_async_callable async def wait_until_responsive( check: Callable[..., Awaitable], timeout: float, pause: float, **kwargs: Any, ) -> None: """Wait until a service is responsive. Args: check: Coroutine, return truthy value when waiting should stop. timeout: Maximum seconds to wait. pause: Seconds to wait between calls to `check`. **kwargs: Given as kwargs to `check`. """ ref = timeit.default_timer() now = ref while (now - ref) < timeout: if await check(**kwargs): return await asyncio.sleep(pause) now = timeit.default_timer() raise RuntimeError("Timeout reached while waiting on service!") class DockerServiceRegistry: def __init__(self, worker_id: str) -> None: self._running_services: set[str] = set() self.docker_ip = self._get_docker_ip() self._base_command = [ "docker", "compose", f"--file={Path(__file__).parent / 'docker-compose.yml'}", f"--project-name=litestar_pytest-{worker_id}", ] def _get_docker_ip(self) -> str: docker_host = os.environ.get("DOCKER_HOST", "").strip() if not docker_host or docker_host.startswith("unix://"): return "127.0.0.1" if match := re.match(r"^tcp://(.+?):\d+$", docker_host): return match[1] raise ValueError(f'Invalid value for DOCKER_HOST: "{docker_host}".') def run_command(self, *args: str) -> None: command = [*self._base_command, *args] subprocess.run(command, check=True, capture_output=True) async def start( self, name: str, *, check: Callable[..., Awaitable], timeout: float = 30, pause: float = 0.1, **kwargs: Any, ) -> None: if name not in self._running_services: self.run_command("up", "-d", name) self._running_services.add(name) await wait_until_responsive( check=ensure_async_callable(check), timeout=timeout, pause=pause, host=self.docker_ip, **kwargs, ) def stop(self, name: str) -> None: pass def down(self) -> None: self.run_command("down", "-t", "5") @pytest.fixture(scope="session") def docker_services(worker_id: str) -> Generator[DockerServiceRegistry, None, None]: if os.getenv("GITHUB_ACTIONS") == "true" and sys.platform != "linux": pytest.skip("Docker not available on this platform") registry = DockerServiceRegistry(worker_id) try: yield registry finally: registry.down() @pytest.fixture(scope="session") def docker_ip(docker_services: DockerServiceRegistry) -> str: return docker_services.docker_ip async def redis_responsive(host: str) -> bool: client: AsyncRedis = AsyncRedis(host=host, port=6397) try: return await client.ping() except (ConnectionError, RedisConnectionError): return False finally: await client.aclose() @pytest.fixture() async def redis_service(docker_services: DockerServiceRegistry) -> None: await docker_services.start("redis", check=redis_responsive) async def valkey_responsive(host: str) -> bool: client: AsyncValkey = AsyncValkey(host=host, port=6381) try: return await client.ping() except (ConnectionError, ValkeyConnectionError): return False finally: await client.aclose() @pytest.fixture() async def valkey_service(docker_services: DockerServiceRegistry) -> None: await docker_services.start("valkey", check=valkey_responsive) async def postgres_responsive(host: str) -> bool: try: conn = await asyncpg.connect( host=host, port=5423, user="postgres", database="postgres", password="super-secret" ) except (ConnectionError, asyncpg.CannotConnectNowError): return False try: return (await conn.fetchrow("SELECT 1"))[0] == 1 # type: ignore finally: await conn.close() @pytest.fixture() async def postgres_service(docker_services: DockerServiceRegistry) -> None: await docker_services.start("postgres", check=postgres_responsive) litestar-2.16.0/tests/e2e/000077500000000000000000000000001500564371300152505ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/__init__.py000066400000000000000000000000001500564371300173470ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_advanced_alchemy.py000066400000000000000000000015021500564371300221260ustar00rootroot00000000000000from litestar import get from litestar.contrib.sqlalchemy.plugins import SQLAlchemyInitPlugin, SQLAlchemySyncConfig from litestar.repository.filters import LimitOffset from litestar.testing import create_test_client def test_using_pagination() -> None: # https://github.com/litestar-org/litestar/issues/2358 async def provide_limit_offset_pagination() -> LimitOffset: return LimitOffset(limit=0, offset=0) @get( path="/", dependencies={"limit_offset": provide_limit_offset_pagination}, ) async def handler(limit_offset: LimitOffset) -> None: return None with create_test_client( route_handlers=[handler], plugins=[SQLAlchemyInitPlugin(SQLAlchemySyncConfig(connection_string="sqlite:///"))], ) as client: assert client.get("/").status_code == 200 litestar-2.16.0/tests/e2e/test_cors/000077500000000000000000000000001500564371300172555ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_cors/__init__.py000066400000000000000000000000001500564371300213540ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_cors/test_cors_allowed_headers.py000066400000000000000000000033641500564371300250440ustar00rootroot00000000000000from litestar import Litestar, get from litestar.config.cors import CORSConfig from litestar.status_codes import HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST from litestar.testing import TestClient @get("/headers-test") async def headers_handler() -> str: return "Test Successful!" cors_config = CORSConfig( allow_methods=["GET"], allow_origins=["https://allowed-origin.com"], allow_headers=["X-Custom-Header", "Content-Type"], ) app = Litestar(route_handlers=[headers_handler], cors_config=cors_config) def test_cors_with_specific_allowed_headers() -> None: with TestClient(app) as client: response = client.options( "/endpoint", headers={ "Origin": "https://allowed-origin.com", "Access-Control-Request-Method": "GET", "Access-Control-Request-Headers": "X-Custom-Header, Content-Type", }, ) assert response.status_code == HTTP_204_NO_CONTENT assert "x-custom-header" in response.headers["access-control-allow-headers"] assert "content-type" in response.headers["access-control-allow-headers"] def test_cors_with_unauthorized_headers() -> None: with TestClient(app) as client: response = client.options( "/endpoint", headers={ "Origin": "https://allowed-origin.com", "Access-Control-Request-Method": "GET", "Access-Control-Request-Headers": "X-Not-Allowed-Header", }, ) assert response.status_code == HTTP_400_BAD_REQUEST assert ( "access-control-allow-headers" not in response.headers or "x-not-allowed-header" not in response.headers.get("access-control-allow-headers", "") ) litestar-2.16.0/tests/e2e/test_cors/test_cors_allowed_methods.py000066400000000000000000000030541500564371300250700ustar00rootroot00000000000000from http import HTTPStatus from litestar import Litestar, get from litestar.config.cors import CORSConfig from litestar.testing import TestClient @get("/method-test") async def method_handler() -> str: return "Method Test Successful!" cors_config = CORSConfig(allow_methods=["GET", "POST"], allow_origins=["https://allowed-origin.com"]) app = Litestar(route_handlers=[method_handler], cors_config=cors_config) def test_cors_allowed_methods() -> None: with TestClient(app) as client: response = client.options( "/method-test", headers={"Origin": "https://allowed-origin.com", "Access-Control-Request-Method": "GET"} ) assert response.status_code == HTTPStatus.NO_CONTENT assert response.headers["access-control-allow-origin"] == "https://allowed-origin.com" assert "GET" in response.headers["access-control-allow-methods"] response = client.options( "/method-test", headers={"Origin": "https://allowed-origin.com", "Access-Control-Request-Method": "POST"} ) assert response.status_code == HTTPStatus.NO_CONTENT assert "POST" in response.headers["access-control-allow-methods"] def test_cors_disallowed_methods() -> None: with TestClient(app) as client: response = client.options( "/method-test", headers={"Origin": "https://allowed-origin.com", "Access-Control-Request-Method": "PUT"} ) assert response.status_code == HTTPStatus.BAD_REQUEST assert "PUT" not in response.headers.get("access-control-allow-methods", "") litestar-2.16.0/tests/e2e/test_cors/test_cors_credentials.py000066400000000000000000000027731500564371300242220ustar00rootroot00000000000000from litestar import Litestar, get from litestar.config.cors import CORSConfig from litestar.status_codes import HTTP_204_NO_CONTENT from litestar.testing import TestClient @get("/credentials-test") async def credentials_handler() -> str: return "Test Successful!" def test_cors_with_credentials_allowed() -> None: cors_config = CORSConfig( allow_methods=["GET"], allow_origins=["https://allowed-origin.com"], allow_credentials=True ) app = Litestar(route_handlers=[credentials_handler], cors_config=cors_config) with TestClient(app) as client: response = client.options( "/endpoint", headers={"Origin": "https://allowed-origin.com", "Access-Control-Request-Method": "GET"} ) assert response.status_code == HTTP_204_NO_CONTENT assert response.headers["access-control-allow-credentials"] == "true" def test_cors_with_credentials_disallowed() -> None: cors_config = CORSConfig( allow_methods=["GET"], allow_origins=["https://allowed-origin.com"], allow_credentials=False, # Credentials should not be allowed ) app = Litestar(route_handlers=[credentials_handler], cors_config=cors_config) with TestClient(app) as client: response = client.options( "/endpoint", headers={"Origin": "https://allowed-origin.com", "Access-Control-Request-Method": "GET"} ) assert response.status_code == HTTP_204_NO_CONTENT assert "access-control-allow-credentials" not in response.headers litestar-2.16.0/tests/e2e/test_cors/test_cors_for_middleware_exception.py000066400000000000000000000023511500564371300267560ustar00rootroot00000000000000from http import HTTPStatus from litestar import Litestar, get from litestar.config.cors import CORSConfig from litestar.exceptions import HTTPException from litestar.middleware import AbstractMiddleware from litestar.testing import TestClient from litestar.types.asgi_types import Receive, Scope, Send class ExceptionMiddleware(AbstractMiddleware): async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Intentional Error") @get("/test") async def handler() -> str: return "Should not reach this" cors_config = CORSConfig(allow_methods=["GET"], allow_origins=["https://allowed-origin.com"], allow_credentials=True) app = Litestar(route_handlers=[handler], cors_config=cors_config, middleware=[ExceptionMiddleware]) def test_cors_on_middleware_exception() -> None: with TestClient(app) as client: response = client.get("/test", headers={"Origin": "https://allowed-origin.com"}) assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR assert response.headers["access-control-allow-origin"] == "https://allowed-origin.com" assert response.headers["access-control-allow-credentials"] == "true" litestar-2.16.0/tests/e2e/test_cors/test_cors_for_mount.py000066400000000000000000000060221500564371300237240ustar00rootroot00000000000000from __future__ import annotations from http import HTTPStatus from unittest.mock import MagicMock import pytest from litestar import Litestar, asgi from litestar.config.cors import CORSConfig from litestar.enums import ScopeType from litestar.testing import TestClient from litestar.types.asgi_types import ASGIApp, Receive, Scope, Send @pytest.fixture(name="asgi_mock") def asgi_mock_fixture() -> MagicMock: return MagicMock() @pytest.fixture(name="asgi_app") def asgi_app_fixture(asgi_mock: MagicMock) -> ASGIApp: async def asgi_app(scope: Scope, receive: Receive, send: Send) -> None: asgi_mock() assert scope["type"] == ScopeType.HTTP while True: event = await receive() if event["type"] == "http.request" and not event.get("more_body", False): break await send( { "type": "http.response.start", "status": 200, "headers": [ (b"content-type", b"text/plain"), ], } ) await send( { "type": "http.response.body", "body": b"Hello, world!", "more_body": False, } ) return asgi_app def test_cors_middleware_for_mount(asgi_app: ASGIApp, asgi_mock: MagicMock) -> None: cors_config = CORSConfig(allow_methods=["*"], allow_origins=["https://some-domain.com"]) app = Litestar( cors_config=cors_config, route_handlers=[ asgi("/app", is_mount=True)(asgi_app), ], openapi_config=None, ) with TestClient(app) as client: response = client.options( "http://127.0.0.1:8000/app", headers={"origin": "https://some-domain.com"}, ) assert response.status_code == HTTPStatus.NO_CONTENT assert response.headers["access-control-allow-origin"] == "https://some-domain.com" asgi_mock.assert_not_called() def test_asgi_app_no_origin_header(asgi_app: ASGIApp, asgi_mock: MagicMock) -> None: cors_config = CORSConfig(allow_methods=["*"], allow_origins=["https://some-domain.com"]) app = Litestar( cors_config=cors_config, route_handlers=[ asgi("/app", is_mount=True)(asgi_app), ], openapi_config=None, ) with TestClient(app) as client: response = client.options("http://127.0.0.1/app") assert response.status_code == HTTPStatus.OK assert response.headers["content-type"] == "text/plain" asgi_mock.assert_called() def test_asgi_app_without_cors_configuration(asgi_app: ASGIApp, asgi_mock: MagicMock) -> None: non_cors_app = Litestar( route_handlers=[asgi("/app", is_mount=True)(asgi_app)], openapi_config=None, ) with TestClient(non_cors_app) as client: response = client.options("http://127.0.0.1:8000/app") assert response.status_code == HTTPStatus.OK assert response.headers["content-type"] == "text/plain" asgi_mock.assert_called() litestar-2.16.0/tests/e2e/test_cors/test_cors_for_routing_exception.py000066400000000000000000000015141500564371300263300ustar00rootroot00000000000000from http import HTTPStatus from litestar import Litestar, get from litestar.config.cors import CORSConfig from litestar.testing import TestClient @get("/test") async def handler() -> str: return "Should not reach this" cors_config = CORSConfig(allow_methods=["GET"], allow_origins=["https://allowed-origin.com"], allow_credentials=True) app = Litestar(route_handlers=[handler], cors_config=cors_config) def test_cors_on_middleware_exception_with_origin_header() -> None: with TestClient(app) as client: response = client.get("/testing", headers={"Origin": "https://allowed-origin.com"}) assert response.status_code == HTTPStatus.NOT_FOUND assert response.headers["access-control-allow-origin"] == "https://allowed-origin.com" assert response.headers["access-control-allow-credentials"] == "true" litestar-2.16.0/tests/e2e/test_cors/test_cors_origins.py000066400000000000000000000030571500564371300233730ustar00rootroot00000000000000from http import HTTPStatus from litestar import Litestar, get from litestar.config.cors import CORSConfig from litestar.testing import TestClient @get("/endpoint") async def handler() -> str: return "Hello, world!" cors_config = CORSConfig( allow_methods=["GET"], allow_origins=["https://allowed-origin.com", "https://another-allowed-origin.com"] ) app = Litestar(route_handlers=[handler], cors_config=cors_config) def test_cors_with_allowed_origins() -> None: with TestClient(app) as client: response = client.options( "/custom-options", headers={"Origin": "https://allowed-origin.com", "Access-Control-Request-Method": "GET"} ) assert response.status_code == HTTPStatus.NO_CONTENT assert response.headers["access-control-allow-origin"] == "https://allowed-origin.com" response = client.options( "/custom-options", headers={"Origin": "https://another-allowed-origin.com", "Access-Control-Request-Method": "GET"}, ) assert response.status_code == HTTPStatus.NO_CONTENT assert response.headers["access-control-allow-origin"] == "https://another-allowed-origin.com" def test_cors_with_disallowed_origin() -> None: with TestClient(app) as client: response = client.options( "/custom-options", headers={"Origin": "https://disallowed-origin.com", "Access-Control-Request-Method": "GET"}, ) assert response.status_code == HTTPStatus.BAD_REQUEST assert "access-control-allow-origin" not in response.headers litestar-2.16.0/tests/e2e/test_cors/test_custom_options_handlers.py000066400000000000000000000026431500564371300256400ustar00rootroot00000000000000from http import HTTPStatus from litestar import Litestar, route from litestar.config.cors import CORSConfig from litestar.enums import HttpMethod from litestar.response import Response from litestar.testing import TestClient @route("/custom-options", http_method=HttpMethod.OPTIONS) async def custom_options_handler() -> Response[str]: return Response( status_code=200, headers={"Custom-Handler": "Active"}, content="Handled by Custom Options", ) cors_config = CORSConfig(allow_methods=["GET", "OPTIONS"], allow_origins=["https://allowed-origin.com"]) app = Litestar(route_handlers=[custom_options_handler], cors_config=cors_config) def test_custom_options_handler_cors_pre_flight_request() -> None: with TestClient(app) as client: response = client.options( "/custom-options", headers={"Origin": "https://allowed-origin.com", "Access-Control-Request-Method": "GET"} ) assert response.status_code == HTTPStatus.NO_CONTENT assert "access-control-allow-origin" in response.headers assert "Custom-Handler" not in response.headers def test_custom_options_handler_non_cors_request() -> None: with TestClient(app) as client: response = client.options("/custom-options") assert response.status_code == 200 assert response.headers.get("Custom-Handler") == "Active" assert response.text == "Handled by Custom Options" litestar-2.16.0/tests/e2e/test_cors/test_non_cors_options.py000066400000000000000000000022141500564371300242600ustar00rootroot00000000000000from litestar import Litestar, get from litestar.config.cors import CORSConfig from litestar.status_codes import HTTP_204_NO_CONTENT from litestar.testing import TestClient @get("/handler") async def handler() -> str: return "Handler" def test_non_cors_options_request_no_origin_header() -> None: cors_config = CORSConfig( allow_methods=["PUT"], allow_origins=["https://specific-domain.com"], ) app = Litestar(route_handlers=[handler], cors_config=cors_config) with TestClient(app) as client: # Request without an 'Origin' header response = client.options("/handler") assert response.status_code == HTTP_204_NO_CONTENT assert response.headers["allow"] == "GET, OPTIONS" def test_non_cors_options_no_config() -> None: app = Litestar(route_handlers=[handler]) with TestClient(app) as client: # Request with an origin that does not require CORS handling response = client.options("/handler", headers={"Origin": "https://not-configured-origin.com"}) assert response.status_code == HTTP_204_NO_CONTENT assert response.headers["allow"] == "GET, OPTIONS" litestar-2.16.0/tests/e2e/test_dependency_injection/000077500000000000000000000000001500564371300224675ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_dependency_injection/__init__.py000066400000000000000000000000001500564371300245660ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_dependency_injection/test_dependency_validation.py000066400000000000000000000011741500564371300304330ustar00rootroot00000000000000import pytest from litestar import Litestar, get from litestar.exceptions import ImproperlyConfiguredException async def first_method(query_param: int) -> int: return query_param async def second_method(path_param: str) -> str: return path_param def test_dependency_validation() -> None: @get( path="/{path_param:int}", dependencies={"first": first_method, "second": second_method}, ) def handler(first: int, second: str, third: int) -> None: pass with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[handler], dependencies={"third": first_method}) litestar-2.16.0/tests/e2e/test_dependency_injection/test_http_handler_dependency_injection.py000066400000000000000000000066621500564371300330260ustar00rootroot00000000000000from asyncio import sleep from typing import TYPE_CHECKING, Any, Dict, Type import pytest from litestar import Controller, get from litestar.di import Provide from litestar.status_codes import HTTP_200_OK, HTTP_400_BAD_REQUEST from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.connection import Request from litestar.datastructures.state import State def router_first_dependency() -> bool: return True async def router_second_dependency() -> bool: await sleep(0) return False def controller_first_dependency(headers: Dict[str, Any]) -> Dict[Any, Any]: assert headers return {} async def controller_second_dependency(request: "Request[Any, Any, State]") -> Dict[Any, Any]: assert request await sleep(0) return {} def local_method_first_dependency(query_param: int) -> int: assert isinstance(query_param, int) return query_param def local_method_second_dependency(path_param: str) -> str: assert isinstance(path_param, str) return path_param test_path = "/test" @pytest.fixture def first_controller() -> Type[Controller]: class FirstController(Controller): path = test_path dependencies = { "first": Provide(controller_first_dependency, sync_to_thread=False), "second": Provide(controller_second_dependency), } @get( path="/{path_param:str}", dependencies={ "first": Provide(local_method_first_dependency, sync_to_thread=False), }, ) def test_method(self, first: int, second: Dict[Any, Any], third: bool) -> None: assert isinstance(first, int) assert isinstance(second, dict) assert not third return FirstController def test_controller_dependency_injection(first_controller: Type[Controller]) -> None: with create_test_client( first_controller, dependencies={ "second": Provide(router_first_dependency, sync_to_thread=False), "third": Provide(router_second_dependency), }, ) as client: response = client.get(f"{test_path}/abcdef?query_param=12345") assert response.status_code == HTTP_200_OK def test_function_dependency_injection() -> None: @get( path=test_path + "/{path_param:str}", dependencies={ "first": Provide(local_method_first_dependency, sync_to_thread=False), "third": Provide(local_method_second_dependency, sync_to_thread=False), }, ) def test_function(first: int, second: bool, third: str) -> None: assert isinstance(first, int) assert second is False assert isinstance(third, str) with create_test_client( test_function, dependencies={ "first": Provide(router_first_dependency, sync_to_thread=False), "second": Provide(router_second_dependency), }, ) as client: response = client.get(f"{test_path}/abcdef?query_param=12345") assert response.status_code == HTTP_200_OK def test_dependency_isolation(first_controller: Type[Controller]) -> None: class SecondController(Controller): path = "/second" @get() def test_method(self, first: Dict[Any, Any]) -> None: pass with create_test_client([first_controller, SecondController]) as client: response = client.get("/second") assert response.status_code == HTTP_400_BAD_REQUEST litestar-2.16.0/tests/e2e/test_dependency_injection/test_injection_of_classes.py000066400000000000000000000045401500564371300302660ustar00rootroot00000000000000from dataclasses import dataclass import msgspec from litestar import Controller, get from litestar.di import Provide from litestar.testing import create_test_client def test_injection_of_classes() -> None: query_param_value = 5 path_param_value = 10 class TopLevelDependency: def __init__(self, path_param: int) -> None: self.path_param = path_param class HandlerDependency: def __init__(self, query_param: int, path_param_dependency: TopLevelDependency): self.query_param = query_param self.path_param_dependency = path_param_dependency class MyController(Controller): path = "/test" dependencies = {"path_param_dependency": Provide(TopLevelDependency, sync_to_thread=False)} @get( path="/{path_param:int}", dependencies={ "container": Provide(HandlerDependency, sync_to_thread=False), }, ) def test_function(self, container: HandlerDependency) -> str: assert container assert isinstance(container, HandlerDependency) assert container.query_param == query_param_value assert isinstance(container.path_param_dependency, TopLevelDependency) assert container.path_param_dependency.path_param == path_param_value return str(container.query_param + container.path_param_dependency.path_param) with create_test_client(MyController) as client: response = client.get(f"/test/{path_param_value}?query_param={query_param_value}") assert response.text == "15" def test_inject_dataclass() -> None: @dataclass class Foo: bar: str @get("/", dependencies={"foo": Provide(Foo, sync_to_thread=False)}) async def handler(foo: Foo) -> Foo: return foo with create_test_client([handler]) as client: res = client.get("/?bar=baz") assert res.status_code == 200 assert res.json() == {"bar": "baz"} def test_inject_msgspec_struct() -> None: class Foo(msgspec.Struct): bar: str @get("/", dependencies={"foo": Provide(Foo, sync_to_thread=False)}) async def handler(foo: Foo) -> Foo: return foo with create_test_client([handler]) as client: res = client.get("/?bar=baz") assert res.status_code == 200 assert res.json() == {"bar": "baz"} litestar-2.16.0/tests/e2e/test_dependency_injection/test_injection_of_generic_models.py000066400000000000000000000020371500564371300316070ustar00rootroot00000000000000from typing import Generic, Optional, Type, TypeVar from msgspec import Struct from litestar import get from litestar.di import Provide from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client T = TypeVar("T") class Store(Struct, Generic[T]): """Abstract store.""" model: Type[T] def get(self, value_id: str) -> Optional[T]: raise NotImplementedError class Item(Struct): name: str class DictStore(Store[Item]): """In-memory store implementation.""" def get(self, value_id: str) -> Optional[Item]: return None async def get_item_store() -> DictStore: return DictStore(model=Item) def test_generic_model_injection() -> None: @get("/") def root(store: DictStore) -> Optional[Item]: assert isinstance(store, DictStore) return store.get("0") with create_test_client(root, dependencies={"store": Provide(get_item_store, use_cache=True)}) as client: response = client.get("/") assert response.status_code == HTTP_200_OK litestar-2.16.0/tests/e2e/test_dependency_injection/test_inter_dependencies.py000066400000000000000000000034341500564371300277330ustar00rootroot00000000000000from random import randint from litestar import Controller, MediaType, get from litestar.di import Provide from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client def test_inter_dependencies() -> None: async def top_dependency(query_param: int) -> int: return query_param async def mid_level_dependency() -> int: return 5 async def local_dependency(path_param: int, mid_level: int, top_level: int) -> int: return path_param + mid_level + top_level class MyController(Controller): path = "/test" dependencies = {"mid_level": Provide(mid_level_dependency)} @get( path="/{path_param:int}", dependencies={ "summed": Provide(local_dependency), }, media_type=MediaType.TEXT, ) def test_function(self, summed: int) -> str: return str(summed) with create_test_client(MyController, dependencies={"top_level": Provide(top_dependency)}) as client: response = client.get("/test/5?query_param=5") assert response.text == "15" def test_inter_dependencies_on_same_app_level() -> None: async def first_dependency() -> int: return randint(1, 10) async def second_dependency(injected_integer: int) -> bool: return injected_integer % 2 == 0 @get("/true-or-false") def true_or_false_handler(injected_bool: bool) -> str: return "its true!" if injected_bool else "nope, its false..." with create_test_client( true_or_false_handler, dependencies={"injected_integer": Provide(first_dependency), "injected_bool": Provide(second_dependency)}, ) as client: response = client.get("/true-or-false") assert response.status_code == HTTP_200_OK litestar-2.16.0/tests/e2e/test_dependency_injection/test_request_local_caching.py000066400000000000000000000017151500564371300304220ustar00rootroot00000000000000from litestar import get from litestar.di import Provide from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client def test_caching_per_request() -> None: value = 1 async def first_dependency() -> int: nonlocal value tmp = value value += 1 return tmp async def second_dependency(first: int) -> int: return first + 5 @get() def route(first: int, second: int) -> int: return first + second with create_test_client( route, dependencies={ "first": Provide(first_dependency), "second": Provide(second_dependency), }, ) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.content == b"7" # 1 + 1 + 5 response2 = client.get("/") assert response2.status_code == HTTP_200_OK assert response2.content == b"9" # 2 + 2 + 5 litestar-2.16.0/tests/e2e/test_dependency_injection/test_websocket_handler_dependency_injection.py000066400000000000000000000071551500564371300340330ustar00rootroot00000000000000from asyncio import sleep from typing import Any, Dict import pytest from litestar import Controller, websocket from litestar.connection import WebSocket from litestar.datastructures import State from litestar.di import Provide from litestar.exceptions import WebSocketDisconnect from litestar.testing import create_test_client def router_first_dependency() -> bool: return True async def router_second_dependency() -> bool: await sleep(0) return False def controller_first_dependency(headers: Dict[str, Any]) -> Dict[Any, Any]: assert headers return {} async def controller_second_dependency(socket: WebSocket[Any, Any, Any]) -> Dict[Any, Any]: assert socket await sleep(0) return {} def local_method_first_dependency(query_param: int) -> int: assert isinstance(query_param, int) return query_param def local_method_second_dependency(path_param: str) -> str: assert isinstance(path_param, str) return path_param test_path = "/test" class FirstController(Controller): path = test_path dependencies = { "first": Provide(controller_first_dependency, sync_to_thread=True), "second": Provide(controller_second_dependency), } @websocket( path="/{path_param:str}", dependencies={ "first": Provide(local_method_first_dependency, sync_to_thread=False), }, ) async def test_method( self, socket: WebSocket[Any, Any, Any], first: int, second: Dict[Any, Any], third: bool ) -> None: await socket.accept() msg = await socket.receive_json() assert msg assert socket assert isinstance(first, int) assert isinstance(second, dict) assert not third await socket.close() def test_controller_dependency_injection() -> None: client = create_test_client( FirstController, dependencies={ "second": Provide(router_first_dependency, sync_to_thread=False), "third": Provide(router_second_dependency), }, ) with client.websocket_connect(f"{test_path}/abcdef?query_param=12345") as ws: ws.send_json({"data": "123"}) def test_function_dependency_injection() -> None: @websocket( path=test_path + "/{path_param:str}", dependencies={ "first": Provide(local_method_first_dependency, sync_to_thread=False), "third": Provide(local_method_second_dependency, sync_to_thread=False), }, ) async def test_function(socket: WebSocket[Any, Any, State], first: int, second: bool, third: str) -> None: await socket.accept() assert socket msg = await socket.receive_json() assert msg assert isinstance(first, int) assert second is False assert isinstance(third, str) await socket.close() client = create_test_client( test_function, dependencies={ "first": Provide(router_first_dependency, sync_to_thread=False), "second": Provide(router_second_dependency), }, ) with client.websocket_connect(f"{test_path}/abcdef?query_param=12345") as ws: ws.send_json({"data": "123"}) def test_dependency_isolation() -> None: class SecondController(Controller): path = "/second" @websocket() async def test_method(self, socket: WebSocket[Any, Any, Any], _: Dict[Any, Any]) -> None: await socket.accept() client = create_test_client([FirstController, SecondController]) with pytest.raises(WebSocketDisconnect), client.websocket_connect("/second/abcdef?query_param=12345") as ws: ws.send_json({"data": "123"}) litestar-2.16.0/tests/e2e/test_exception_handlers/000077500000000000000000000000001500564371300221655ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_exception_handlers/__init__.py000066400000000000000000000000001500564371300242640ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_exception_handlers/test_exception_handler_registered_on_handler.py000066400000000000000000000013751500564371300337050ustar00rootroot00000000000000# ruff: noqa: UP006 from __future__ import annotations from unittest.mock import MagicMock from litestar import Request, Response, get from litestar.testing import create_test_client mock = MagicMock() def value_error_handler(_: Request, exc: ValueError) -> Response: mock() return Response({"error": str(exc)}, status_code=500) @get("/", exception_handlers={ValueError: value_error_handler}) async def home() -> None: raise ValueError("Something went wrong") def test_exception_handler_registered_on_handler() -> None: with create_test_client([home]) as client: response = client.get("/") assert response.status_code == 500 assert response.json() == {"error": "Something went wrong"} mock.assert_called_once() litestar-2.16.0/tests/e2e/test_exception_handlers/test_exception_handlers.py000066400000000000000000000063621500564371300274630ustar00rootroot00000000000000from typing import TYPE_CHECKING, Type import pytest from docs.examples.exceptions.implicit_media_type import handler as implicit_media_type_handler from litestar import Controller, Request, Response, Router, get from litestar.exceptions import ( HTTPException, InternalServerException, NotFoundException, ServiceUnavailableException, ValidationException, ) from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.types import ExceptionHandler @pytest.mark.parametrize( ["exc_to_raise", "expected_layer"], [ (ValidationException, "router"), (InternalServerException, "controller"), (ServiceUnavailableException, "handler"), (NotFoundException, "handler"), ], ) def test_exception_handling(exc_to_raise: HTTPException, expected_layer: str) -> None: caller = {"name": ""} def create_named_handler(caller_name: str, expected_exception: Type[Exception]) -> "ExceptionHandler": def handler(req: Request, exc: Exception) -> Response: assert isinstance(exc, expected_exception) assert isinstance(req, Request) caller["name"] = caller_name return Response(content={}, status_code=exc_to_raise.status_code) return handler class ControllerWithHandler(Controller): path = "/test" exception_handlers = { InternalServerException: create_named_handler("controller", InternalServerException), ServiceUnavailableException: create_named_handler("controller", ServiceUnavailableException), } @get( "/", exception_handlers={ ServiceUnavailableException: create_named_handler("handler", ServiceUnavailableException), NotFoundException: create_named_handler("handler", NotFoundException), }, ) def my_handler(self) -> None: raise exc_to_raise my_router = Router( path="/base", route_handlers=[ControllerWithHandler], exception_handlers={ InternalServerException: create_named_handler("router", InternalServerException), ValidationException: create_named_handler("router", ValidationException), }, ) with create_test_client(route_handlers=[my_router]) as client: response = client.get("/base/test/") assert response.status_code == exc_to_raise.status_code, response.json() assert caller["name"] == expected_layer def test_exception_handler_with_custom_request_class() -> None: class CustomRequest(Request): ... def handler(req: Request, exc: Exception) -> Response: assert isinstance(req, CustomRequest) return Response(content={}) @get() async def index() -> None: _ = 1 / 0 with create_test_client([index], exception_handlers={Exception: handler}, request_class=CustomRequest) as client: client.get("/") def test_exception_handler_implicit_media_type() -> None: with create_test_client([implicit_media_type_handler]) as client: response = client.get("/", params={"q": 1}) assert response.status_code == 500 assert response.headers["content-type"] == "text/plain; charset=utf-8" assert "ValueError" in response.text litestar-2.16.0/tests/e2e/test_life_cycle_hooks/000077500000000000000000000000001500564371300216105ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_life_cycle_hooks/__init__.py000066400000000000000000000000001500564371300237070ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_life_cycle_hooks/test_after_request.py000066400000000000000000000075761500564371300261110ustar00rootroot00000000000000from typing import Any, Dict, Optional import pytest from litestar import Controller, Response, Router, get from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from litestar.types import AfterRequestHookHandler def sync_after_request_handler(response: Response[Dict[str, str]]) -> Response[Dict[str, str]]: assert isinstance(response, Response) response.content = {"hello": "moon"} return response async def async_after_request_handler(response: Response[Dict[str, str]]) -> Response[Dict[str, str]]: assert isinstance(response, Response) response.content = {"hello": "moon"} return response async def async_after_request_handler_with_hello_world(response: Response[Dict[str, str]]) -> Response[Dict[str, str]]: assert isinstance(response, Response) response.content = {"hello": "world"} return response @pytest.mark.parametrize( "after_request, expected", [ [None, {"hello": "world"}], [sync_after_request_handler, {"hello": "moon"}], [async_after_request_handler, {"hello": "moon"}], ], ) def test_after_request_handler_called( after_request: Optional[AfterRequestHookHandler], expected: Dict[str, str] ) -> None: @get(after_request=after_request) def handler() -> Dict[str, str]: return {"hello": "world"} with create_test_client(route_handlers=handler) as client: response = client.get("/") assert response.json() == expected @pytest.mark.parametrize( "app_after_request_handler, router_after_request_handler, controller_after_request_handler, method_after_request_handler, expected", [ [None, None, None, None, {"hello": "world"}], [sync_after_request_handler, None, None, None, {"hello": "moon"}], [None, sync_after_request_handler, None, None, {"hello": "moon"}], [None, None, sync_after_request_handler, None, {"hello": "moon"}], [None, None, None, sync_after_request_handler, {"hello": "moon"}], [sync_after_request_handler, async_after_request_handler_with_hello_world, None, None, {"hello": "world"}], [None, sync_after_request_handler, async_after_request_handler_with_hello_world, None, {"hello": "world"}], [None, None, sync_after_request_handler, async_after_request_handler_with_hello_world, {"hello": "world"}], [None, None, None, async_after_request_handler_with_hello_world, {"hello": "world"}], ], ) def test_after_request_handler_resolution( app_after_request_handler: Optional[AfterRequestHookHandler], router_after_request_handler: Optional[AfterRequestHookHandler], controller_after_request_handler: Optional[AfterRequestHookHandler], method_after_request_handler: Optional[AfterRequestHookHandler], expected: Dict[str, str], ) -> None: class MyController(Controller): path = "/hello" after_request = controller_after_request_handler @get(after_request=method_after_request_handler) def hello(self) -> Dict[str, str]: return {"hello": "world"} router = Router(path="/greetings", route_handlers=[MyController], after_request=router_after_request_handler) with create_test_client(route_handlers=router, after_request=app_after_request_handler) as client: response = client.get("/greetings/hello") assert response.json() == expected def test_after_request_handles_handlers_that_return_responses() -> None: def after_request(response: Response[Any]) -> Response[Any]: response.headers["Custom-Header-Name"] = "Custom Header Value" return response @get("/") def handler() -> Response[str]: return Response("test") with create_test_client(handler, after_request=after_request) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers.get("Custom-Header-Name") == "Custom Header Value" litestar-2.16.0/tests/e2e/test_life_cycle_hooks/test_after_response.py000066400000000000000000000024541500564371300262450ustar00rootroot00000000000000from unittest.mock import MagicMock, call import pytest from litestar import Controller, Request, Router, get from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client @pytest.mark.parametrize("sync", [True, False]) @pytest.mark.parametrize("layer", ["app", "router", "controller", "handler"]) def test_after_response_resolution(layer: str, sync: bool) -> None: mock = MagicMock() if sync: def handler(_: Request) -> None: # pyright: ignore mock(layer) else: async def handler(_: Request) -> None: # type: ignore[misc] mock(layer) class MyController(Controller): path = "/controller" after_response = handler if layer == "controller" else None @get("/", after_response=handler if layer == "handler" else None) def my_handler(self) -> None: return None router = Router( path="/router", route_handlers=[MyController], after_response=handler if layer == "router" else None ) with create_test_client(route_handlers=[router], after_response=handler if layer == "app" else None) as client: response = client.get("/router/controller/") assert response.status_code == HTTP_200_OK assert all(c == call(layer) for c in mock.call_args_list) litestar-2.16.0/tests/e2e/test_life_cycle_hooks/test_before_request.py000066400000000000000000000104711500564371300262360ustar00rootroot00000000000000from typing import Any, Dict, Optional import pytest from litestar import Controller, Request, Response, Router, get from litestar.datastructures import State from litestar.testing import create_test_client from litestar.types import AnyCallable, BeforeRequestHookHandler def sync_before_request_handler_with_return_value(request: Request[Any, Any, State]) -> Dict[str, str]: assert isinstance(request, Request) return {"hello": "moon"} async def async_before_request_handler_with_return_value(request: Request[Any, Any, State]) -> Dict[str, str]: assert isinstance(request, Request) return {"hello": "moon"} def sync_before_request_handler_without_return_value(request: Request[Any, Any, State]) -> None: assert isinstance(request, Request) async def async_before_request_handler_without_return_value(request: Request[Any, Any, State]) -> None: assert isinstance(request, Request) def sync_after_request_handler(response: Response[Dict[str, str]]) -> Response[Dict[str, str]]: assert isinstance(response, Response) response.content = {"hello": "moon"} return response async def async_after_request_handler(response: Response[Dict[str, str]]) -> Response[Dict[str, str]]: assert isinstance(response, Response) response.content = {"hello": "moon"} return response @pytest.mark.parametrize( "before_request, expected", ( (None, {"hello": "world"}), (sync_before_request_handler_with_return_value, {"hello": "moon"}), (async_before_request_handler_with_return_value, {"hello": "moon"}), (sync_before_request_handler_without_return_value, {"hello": "world"}), (async_before_request_handler_without_return_value, {"hello": "world"}), ), ) def test_before_request_handler_called(before_request: Optional[AnyCallable], expected: Dict[str, str]) -> None: @get(before_request=before_request) def handler() -> Dict[str, str]: return {"hello": "world"} with create_test_client(route_handlers=handler) as client: response = client.get("/") assert response.json() == expected @pytest.mark.parametrize( "app_before_request_handler, router_before_request_handler, controller_before_request_handler, method_before_request_handler, expected", [ [None, None, None, None, {"hello": "world"}], [sync_before_request_handler_with_return_value, None, None, None, {"hello": "moon"}], [None, sync_before_request_handler_with_return_value, None, None, {"hello": "moon"}], [None, None, sync_before_request_handler_with_return_value, None, {"hello": "moon"}], [None, None, None, sync_before_request_handler_with_return_value, {"hello": "moon"}], [ sync_before_request_handler_with_return_value, async_before_request_handler_without_return_value, None, None, {"hello": "world"}, ], [ None, sync_before_request_handler_with_return_value, async_before_request_handler_without_return_value, None, {"hello": "world"}, ], [ None, None, sync_before_request_handler_with_return_value, async_before_request_handler_without_return_value, {"hello": "world"}, ], [None, None, None, async_before_request_handler_without_return_value, {"hello": "world"}], ], ) def test_before_request_handler_resolution( app_before_request_handler: Optional[BeforeRequestHookHandler], router_before_request_handler: Optional[BeforeRequestHookHandler], controller_before_request_handler: Optional[BeforeRequestHookHandler], method_before_request_handler: Optional[BeforeRequestHookHandler], expected: Dict[str, str], ) -> None: class MyController(Controller): path = "/hello" before_request = controller_before_request_handler @get(before_request=method_before_request_handler) def hello(self) -> Dict[str, str]: return {"hello": "world"} router = Router(path="/greetings", route_handlers=[MyController], before_request=router_before_request_handler) with create_test_client(route_handlers=router, before_request=app_before_request_handler) as client: response = client.get("/greetings/hello") assert response.json() == expected litestar-2.16.0/tests/e2e/test_logging/000077500000000000000000000000001500564371300177355ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_logging/__init__.py000066400000000000000000000000001500564371300220340ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_logging/test_structlog_to_file.py000066400000000000000000000047501500564371300251030ustar00rootroot00000000000000from __future__ import annotations import json from pathlib import Path from typing import TYPE_CHECKING from unittest.mock import ANY import pytest import structlog from litestar import Litestar, get from litestar.logging import StructLoggingConfig from litestar.logging.config import default_json_serializer, default_structlog_processors from litestar.plugins.structlog import StructlogConfig, StructlogPlugin from litestar.testing import TestClient if TYPE_CHECKING: from collections.abc import Iterator @pytest.fixture(autouse=True) def structlog_reset() -> Iterator[None]: try: yield finally: structlog.reset_defaults() def test_structlog_to_file(tmp_path: Path) -> None: log_file = tmp_path / "log.log" logging_config = StructlogConfig( structlog_logging_config=StructLoggingConfig( logger_factory=structlog.WriteLoggerFactory(file=log_file.open("wt")), processors=default_structlog_processors( json_serializer=lambda v, **_: str(default_json_serializer(v), "utf-8") ), ), ) logger = structlog.get_logger() @get("/") def handler() -> str: logger.info("handled", hello="world") return "hello" app = Litestar(route_handlers=[handler], plugins=[StructlogPlugin(config=logging_config)], debug=True) with TestClient(app) as client: resp = client.get("/") assert resp.text == "hello" logged_data = [json.loads(line) for line in log_file.read_text().splitlines()] assert logged_data == [ { "path": "/", "method": "GET", "content_type": ["", {}], "headers": { "host": "testserver.local", "accept": "*/*", "accept-encoding": "gzip, deflate, br", "connection": "keep-alive", "user-agent": "testclient", }, "cookies": {}, "query": {}, "path_params": {}, "body": None, "event": "HTTP Request", "level": "info", "timestamp": ANY, }, {"hello": "world", "event": "handled", "level": "info", "timestamp": ANY}, { "status_code": 200, "cookies": {}, "headers": {"content-type": "text/plain; charset=utf-8", "content-length": "5"}, "body": "hello", "event": "HTTP Response", "level": "info", "timestamp": ANY, }, ] litestar-2.16.0/tests/e2e/test_middleware/000077500000000000000000000000001500564371300204245ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_middleware/__init__.py000066400000000000000000000000001500564371300225230ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_middleware/test_exception_handler_applied_to_middleware_exception.py000066400000000000000000000024131500564371300342030ustar00rootroot00000000000000"""Test exception handlers defined on layers are called for middleware exceptions.""" from __future__ import annotations from typing import TYPE_CHECKING from unittest.mock import MagicMock from litestar import get from litestar.connection import Request from litestar.response import Response from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.types.asgi_types import ASGIApp, Receive, Scope, Send exception_handler_mock = MagicMock() route_handler_mock = MagicMock() def exception_handler(_: Request, __: type[Exception]) -> Response: exception_handler_mock() return Response(content="", status_code=500) def asgi_middleware(app: ASGIApp) -> ASGIApp: async def middleware(scope: Scope, receive: Receive, send: Send) -> None: raise RuntimeError("damn") return middleware @get("/not_called", exception_handlers={RuntimeError: exception_handler}, sync_to_thread=False) def not_called() -> None: route_handler_mock() def test_middleware_send_wrapper_called_on_exception() -> None: with create_test_client([not_called], middleware=[asgi_middleware], openapi_config=None) as client: client.get("/not_called") route_handler_mock.assert_not_called() exception_handler_mock.assert_called_once() litestar-2.16.0/tests/e2e/test_middleware/test_exception_handler_called_from_mounted_app.py000066400000000000000000000016411500564371300324540ustar00rootroot00000000000000# ruff: noqa: UP006 from __future__ import annotations from typing import TYPE_CHECKING from unittest.mock import MagicMock from litestar import Request, Response, asgi from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.types import Receive, Scope, Send mock = MagicMock() def value_error_handler(_: Request, exc: ValueError) -> Response: mock() return Response({"error": str(exc)}, status_code=500) @asgi("/mount", exception_handlers={ValueError: value_error_handler}) async def mounted(scope: Scope, receive: Receive, send: Send) -> None: raise ValueError("Something went wrong") def test_exception_handler_registered_on_handler() -> None: with create_test_client([mounted]) as client: response = client.get("/mount") assert response.status_code == 500 assert response.json() == {"error": "Something went wrong"} mock.assert_called_once() litestar-2.16.0/tests/e2e/test_middleware/test_exception_handler_called_if_no_middleware.py000066400000000000000000000015611500564371300324060ustar00rootroot00000000000000"""Test exception handlers defined on layers are called for middleware exceptions.""" from __future__ import annotations from unittest.mock import MagicMock from litestar import get from litestar.connection import Request from litestar.response import Response from litestar.testing import create_test_client exception_handler_mock = MagicMock() def exception_handler(_: Request, __: type[Exception]) -> Response: exception_handler_mock() return Response(content="", status_code=500) @get("/raising", sync_to_thread=False) def raising() -> None: raise RuntimeError("damn") def test_middleware_send_wrapper_called_on_exception() -> None: with create_test_client( [raising], exception_handlers={RuntimeError: exception_handler}, openapi_config=None ) as client: client.get("/raising") exception_handler_mock.assert_called_once() litestar-2.16.0/tests/e2e/test_middleware/test_logging_middleware_with_multi_body_response.py000066400000000000000000000020631500564371300330610ustar00rootroot00000000000000from litestar import asgi from litestar.middleware.logging import LoggingMiddlewareConfig from litestar.testing import create_async_test_client from litestar.types.asgi_types import Receive, Scope, Send @asgi("/") async def asgi_app(scope: Scope, receive: Receive, send: Send) -> None: await send( { "type": "http.response.start", "status": 200, "headers": [ (b"content-type", b"text/event-stream"), (b"cache-control", b"no-cache"), (b"connection", b"keep-alive"), ], } ) # send two bodies await send({"type": "http.response.body", "body": b"data: 1\n", "more_body": True}) await send({"type": "http.response.body", "body": b"data: 2\n", "more_body": False}) async def test_app() -> None: async with create_async_test_client(asgi_app, middleware=[LoggingMiddlewareConfig().middleware]) as client: response = await client.get("/") assert response.status_code == 200 assert response.text == "data: 1\ndata: 2\n" litestar-2.16.0/tests/e2e/test_middleware/test_middleware_send_wrapper_called_on_error.py000066400000000000000000000022321500564371300321330ustar00rootroot00000000000000"""Test middleware send_wrapper called on exception.""" from __future__ import annotations from typing import TYPE_CHECKING from unittest.mock import MagicMock from litestar import get from litestar.exceptions import InternalServerException from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.types.asgi_types import ASGIApp, Message, Receive, Scope, Send mock = MagicMock() def asgi_middleware(app: ASGIApp) -> ASGIApp: async def middleware(scope: Scope, receive: Receive, send: Send) -> None: async def send_wrapper(message: Message) -> None: if message["type"] == "http.response.start": mock(message["type"], message["status"]) await send(message) await app(scope, receive, send_wrapper) return middleware @get("/raising", sync_to_thread=False) def raising() -> None: raise InternalServerException("This is an exception") def test_middleware_send_wrapper_called_on_exception() -> None: with create_test_client([raising], middleware=[asgi_middleware]) as client: client.get("/raising") mock.assert_called_once_with("http.response.start", 500) litestar-2.16.0/tests/e2e/test_openapi/000077500000000000000000000000001500564371300177425ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_openapi/__init__.py000066400000000000000000000000001500564371300220410ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_openapi/test_spec_headers.py000066400000000000000000000013461500564371300240040ustar00rootroot00000000000000from litestar import Litestar, Request, get from litestar.datastructures import ResponseHeader @get("/") async def hello_world1(request: Request) -> None: request.logger.info("inside request") return app1 = Litestar( route_handlers=[hello_world1], response_headers=[ResponseHeader(name="X-Version", value="ABCD", description="Test")], ) def test_included_header_fields() -> None: # https://github.com/litestar-org/litestar/issues/3416 assert app1.openapi_schema.to_schema()["paths"]["/"]["get"]["responses"]["200"]["headers"] == { "X-Version": { "deprecated": False, "description": "Test", "required": False, "schema": {"type": "string"}, } } litestar-2.16.0/tests/e2e/test_option_requests.py000066400000000000000000000176461500564371300221420ustar00rootroot00000000000000from itertools import permutations from typing import TYPE_CHECKING, List, Mapping, Optional import pytest from litestar import get, route from litestar.config.cors import CORSConfig from litestar.status_codes import HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST from litestar.testing import create_test_client from tests.helpers import RANDOM if TYPE_CHECKING: from litestar.types import Method @pytest.mark.parametrize( "http_methods", (list(perm) for perm in iter(permutations(["GET", "POST", "PATCH", "DELETE", "HEAD"], r=RANDOM.randrange(1, 6)))), ) def test_regular_options_request(http_methods: List["Method"]) -> None: @route("/", http_method=http_methods) def handler() -> None: return None with create_test_client(handler, openapi_config=None) as client: response = client.options("/") assert response.status_code == HTTP_204_NO_CONTENT, response.text assert response.headers.get("Allow") == ", ".join(sorted({*http_methods, "OPTIONS"})) def test_cors_options_request_without_origin_passes() -> None: @get("/") def handler() -> None: return None with create_test_client(handler, cors_config=CORSConfig(allow_origins=["http://testserver.local"])) as client: response = client.options("/") assert response.status_code == HTTP_204_NO_CONTENT assert response.headers.get("Allow") == "GET, OPTIONS" def test_cors_options_request_with_correct_origin_passes() -> None: @get("/") def handler() -> None: return None with create_test_client(handler, cors_config=CORSConfig(allow_origins=["http://testserver.local"])) as client: response = client.options("/", headers={"Origin": "http://testserver.local"}) assert response.status_code == HTTP_204_NO_CONTENT assert response.headers.get("Access-Control-Allow-Methods") == "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT" assert response.headers.get("Access-Control-Allow-Origin") == "http://testserver.local" assert response.headers.get("Vary") == "Origin" def test_cors_options_request_with_correct_origin_passes_with_allow_all_origins() -> None: @get("/") def handler() -> None: return None with create_test_client(handler, cors_config=CORSConfig(allow_origins=["*"])) as client: response = client.options("/", headers={"Origin": "http://testserver.local"}) assert response.status_code == HTTP_204_NO_CONTENT assert response.headers.get("Access-Control-Allow-Methods") == "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT" assert response.headers.get("Access-Control-Allow-Origin") == "*" assert "Vary" not in response.headers def test_cors_options_request_with_wrong_origin_fails() -> None: @get("/") def handler() -> None: return None with create_test_client(handler, cors_config=CORSConfig(allow_origins=["http://testserver.local"])) as client: response = client.options("/", headers={"Origin": "https://moishe.zuchmir"}) assert response.status_code == HTTP_400_BAD_REQUEST @pytest.mark.parametrize( "allowed_origins, allowed_origin_regex, origin", ( (["http://testserver.local", "https://moishe.zuchmir"], None, "https://moishe.zuchmir"), (["http://testserver.local", "https://moishe.zuchmir"], None, "http://testserver.local"), (["http://testserver.local", "https://moishe.*"], None, "https://moishe.zuchmir"), (["http://testserver.local", "https://moishe.*.abc.com"], None, "https://moishe.zuchmir.abc.com"), (["http://testserver.local", "https://moishe.*.*.com"], None, "https://moishe.zuchmir.zzz.com"), (["http://testserver.local"], "https://moishe.*.*.com", "https://moishe.zuchmir.zzz.com"), ([], "https://moishe.*.*.com", "https://moishe.zuchmir.zzz.com"), ), ) def test_cors_options_request_with_different_domains_matches_regex( allowed_origins: List[str], allowed_origin_regex: Optional[str], origin: str ) -> None: @get("/") def handler() -> None: return None with create_test_client( handler, cors_config=CORSConfig(allow_origins=allowed_origins, allow_origin_regex=allowed_origin_regex) ) as client: response = client.options("/", headers={"Origin": origin}) assert response.status_code == HTTP_204_NO_CONTENT @pytest.mark.parametrize( "origin, allow_credentials", (("http://testserver.local", False), ("http://testserver.local", True), (None, False), (None, True)), ) def test_cors_options_request_allow_credentials_header(origin: str, allow_credentials: bool) -> None: @get("/") def handler() -> None: return None with create_test_client( handler, cors_config=CORSConfig(allow_origins=["http://testserver.local"], allow_credentials=allow_credentials) ) as client: headers: Mapping[str, str] = {"Origin": origin} if origin else {} response = client.options("/", headers=headers) assert response.status_code == HTTP_204_NO_CONTENT if origin and allow_credentials: assert response.headers.get("Access-Control-Allow-Credentials") == str(allow_credentials).lower() else: assert "Access-Control-Allow-Credentials" not in response.headers def test_cors_options_request_with_wrong_headers_fails() -> None: @get("/") def handler() -> None: return None with create_test_client(handler, cors_config=CORSConfig(allow_headers=["X-My-Header"])) as client: response = client.options( "/", headers={ "Origin": "http://testserver.local", "Access-Control-Request-Headers": "X-My-Header, X-Another-Header", }, ) assert response.status_code == HTTP_400_BAD_REQUEST def test_cors_options_request_with_correct_headers_passes() -> None: @get("/") def handler() -> None: return None with create_test_client( handler, cors_config=CORSConfig(allow_headers=["X-My-Header", "X-Another-Header"]) ) as client: response = client.options( "/", headers={ "Origin": "http://testserver.local", "Access-Control-Request-Headers": "X-My-Header, X-Another-Header", }, ) assert response.status_code == HTTP_204_NO_CONTENT def test_requested_headers_are_reflected_back_when_allow_all_headers() -> None: @get("/") def handler() -> None: return None with create_test_client(handler, cors_config=CORSConfig(allow_headers=["*"])) as client: response = client.options( "/", headers={ "Origin": "http://testserver.local", "Access-Control-Request-Headers": "X-My-Header, X-Another-Header", }, ) assert response.status_code == HTTP_204_NO_CONTENT assert ( response.headers.get("Access-Control-Allow-Headers") == "Accept, Accept-Language, Content-Language, Content-Type, X-Another-Header, X-My-Header" ) def test_cors_options_request_fails_if_request_method_not_allowed() -> None: @get("/") def handler() -> None: return None with create_test_client(handler, cors_config=CORSConfig(allow_methods=["GET"])) as client: response = client.options( "/", headers={"Origin": "http://testserver.local", "Access-Control-Request-Method": "POST"}, ) assert response.status_code == HTTP_400_BAD_REQUEST def test_cors_options_request_succeeds_if_request_method_not_specified() -> None: @get("/") def handler() -> None: return None with create_test_client(handler, cors_config=CORSConfig(allow_methods=["GET"])) as client: response = client.options( "/", headers={ "Origin": "http://testserver.local", }, ) assert response.status_code == HTTP_204_NO_CONTENT assert response.headers.get("Access-Control-Allow-Methods") == "GET" litestar-2.16.0/tests/e2e/test_pydantic.py000066400000000000000000000102121500564371300204700ustar00rootroot00000000000000import pydantic from pydantic import v1 as pydantic_v1 from litestar import get, post from litestar.testing import create_test_client def test_app_with_v1_and_v2_models() -> None: class ModelV1(pydantic.v1.BaseModel): # pyright: ignore foo: str class ModelV2(pydantic.BaseModel): foo: str @get("/v1") def handler_v1() -> ModelV1: return ModelV1(foo="bar") @get("/v2") def handler_v2() -> ModelV2: return ModelV2(foo="bar") with create_test_client([handler_v1, handler_v2]) as client: assert client.get("/v1").json() == {"foo": "bar"} assert client.get("/v2").json() == {"foo": "bar"} def test_pydantic_v1_model_with_field_default() -> None: # https://github.com/litestar-org/litestar/issues/3471 class TestDto(pydantic_v1.BaseModel): test_str: str = pydantic_v1.Field(default="some_default", max_length=100) @post(path="/test") async def test(data: TestDto) -> str: return "success" with create_test_client(route_handlers=[test]) as client: response = client.get("/schema/openapi.json") assert response.status_code == 200 assert response.json() == { "components": { "schemas": { "test_pydantic_v1_model_with_field_default.TestDto": { "properties": {"test_str": {"default": "some_default", "maxLength": 100, "type": "string"}}, "required": [], "title": "TestDto", "type": "object", } } }, "info": {"title": "Litestar API", "version": "1.0.0"}, "openapi": "3.1.0", "paths": { "/test": { "post": { "deprecated": False, "operationId": "TestTest", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/test_pydantic_v1_model_with_field_default.TestDto" } } }, "required": True, }, "responses": { "201": { "content": {"text/plain": {"schema": {"type": "string"}}}, "description": "Document " "created, " "URL " "follows", "headers": {}, }, "400": { "content": { "application/json": { "schema": { "description": "Validation " "Exception", "examples": [{"detail": "Bad " "Request", "extra": {}, "status_code": 400}], "properties": { "detail": {"type": "string"}, "extra": { "additionalProperties": {}, "type": ["null", "object", "array"], }, "status_code": {"type": "integer"}, }, "required": ["detail", "status_code"], "type": "object", } } }, "description": "Bad " "request " "syntax or " "unsupported " "method", }, }, "summary": "Test", } } }, "servers": [{"url": "/"}], } litestar-2.16.0/tests/e2e/test_regular_handler_under_asgi_mount_path.py000066400000000000000000000024121500564371300264540ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar import Litestar, asgi, get from litestar.enums import ScopeType from litestar.testing import TestClient if TYPE_CHECKING: from litestar.types.asgi_types import Receive, Scope, Send async def asgi_app(scope: Scope, receive: Receive, send: Send) -> None: assert scope["type"] == ScopeType.HTTP await send( { "type": "http.response.start", "status": 200, "headers": [ (b"content-type", b"text/plain"), (b"content-length", b"%d" % len(scope["raw_path"])), ], } ) await send( { "type": "http.response.body", "body": scope["raw_path"], "more_body": False, } ) asgi_handler = asgi("/", is_mount=True)(asgi_app) @get("/path") async def get_handler() -> str: return "Hello, world!" app = Litestar( route_handlers=[asgi_handler, get_handler], openapi_config=None, debug=True, ) def test_regular_handler_under_mounted_asgi_app() -> None: # https://github.com/litestar-org/litestar/issues/3429 with TestClient(app) as client: resp = client.get("/some/path") assert resp.content == b"/some/path" litestar-2.16.0/tests/e2e/test_response_caching.py000066400000000000000000000265521500564371300222050ustar00rootroot00000000000000import gzip import random from datetime import timedelta from typing import TYPE_CHECKING, Any, Optional, Type, TypeVar, Union from unittest.mock import MagicMock from uuid import uuid4 import msgspec import pytest from litestar import Litestar, Request, Response, get, post from litestar.config.compression import CompressionConfig from litestar.config.response_cache import CACHE_FOREVER, ResponseCacheConfig from litestar.datastructures import State from litestar.enums import CompressionEncoding from litestar.middleware.response_cache import ResponseCacheMiddleware from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR from litestar.stores.base import Store from litestar.stores.memory import MemoryStore from litestar.testing import TestClient, create_test_client from litestar.types import HTTPScope if TYPE_CHECKING: from time_machine import Coordinates T = TypeVar("T") @pytest.fixture() def mock() -> MagicMock: return MagicMock(return_value=str(random.random())) def after_request_handler(response: "Response[T]") -> "Response[T]": response.headers["unique-identifier"] = str(uuid4()) return response @pytest.mark.parametrize("sync_to_thread", (True, False)) def test_default_cache_response(sync_to_thread: bool, mock: MagicMock) -> None: @get( "/cached", sync_to_thread=sync_to_thread, cache=True, type_encoders={int: str}, # test pickling issues. see https://github.com/litestar-org/litestar/issues/1096 ) def handler() -> str: return mock() # type: ignore[no-any-return] with create_test_client([handler], after_request=after_request_handler) as client: first_response = client.get("/cached") second_response = client.get("/cached") first_response_identifier = first_response.headers["unique-identifier"] assert first_response.status_code == 200 assert second_response.status_code == 200 assert second_response.headers["unique-identifier"] == first_response_identifier assert first_response.text == second_response.text assert mock.call_count == 1 def test_handler_expiration(mock: MagicMock, frozen_datetime: "Coordinates") -> None: @get("/cached-local", cache=10) async def handler() -> str: return mock() # type: ignore[no-any-return] with create_test_client([handler], after_request=after_request_handler) as client: first_response = client.get("/cached-local") frozen_datetime.shift(delta=timedelta(seconds=5)) second_response = client.get("/cached-local") assert first_response.headers["unique-identifier"] == second_response.headers["unique-identifier"] assert mock.call_count == 1 frozen_datetime.shift(delta=timedelta(seconds=11)) third_response = client.get("/cached-local") assert first_response.headers["unique-identifier"] != third_response.headers["unique-identifier"] assert mock.call_count == 2 def test_default_expiration(mock: MagicMock, frozen_datetime: "Coordinates") -> None: @get("/cached-default", cache=True) async def handler() -> str: return mock() # type: ignore[no-any-return] with create_test_client( [handler], after_request=after_request_handler, response_cache_config=ResponseCacheConfig(default_expiration=1) ) as client: first_response = client.get("/cached-default") second_response = client.get("/cached-default") assert first_response.headers["unique-identifier"] == second_response.headers["unique-identifier"] assert mock.call_count == 1 frozen_datetime.shift(delta=timedelta(seconds=1)) third_response = client.get("/cached-default") assert first_response.headers["unique-identifier"] != third_response.headers["unique-identifier"] assert mock.call_count == 2 @pytest.mark.parametrize("expiration,expected_expiration", [(True, None), (10, 10)]) def test_default_expiration_none( memory_store: MemoryStore, expiration: int, expected_expiration: Optional[int] ) -> None: @get("/cached", cache=expiration) def handler() -> None: return None app = Litestar( [handler], stores={"response_cache": memory_store}, response_cache_config=ResponseCacheConfig(default_expiration=None), ) with TestClient(app) as client: client.get("/cached") if expected_expiration is None: assert memory_store._store["GET/cached"].expires_at is None else: assert memory_store._store["GET/cached"].expires_at def test_cache_forever(memory_store: MemoryStore) -> None: @get("/cached", cache=CACHE_FOREVER) async def handler() -> None: return None app = Litestar([handler], stores={"response_cache": memory_store}) with TestClient(app) as client: client.get("/cached") assert memory_store._store["GET/cached"].expires_at is None @pytest.mark.parametrize("sync_to_thread", (True, False)) async def test_custom_cache_key(sync_to_thread: bool, anyio_backend: str, mock: MagicMock) -> None: def custom_cache_key_builder(request: Request[Any, Any, State]) -> str: return f"{request.url.path}:::cached" @get("/cached", sync_to_thread=sync_to_thread, cache=True, cache_key_builder=custom_cache_key_builder) def handler() -> str: return mock() # type: ignore[no-any-return] app = Litestar([handler]) with TestClient(app) as client: client.get("/cached") store = app.stores.get("response_cache") assert await store.exists("/cached:::cached") async def test_non_default_store_name(mock: MagicMock) -> None: @get(cache=True) def handler() -> str: return mock() # type: ignore[no-any-return] app = Litestar([handler], response_cache_config=ResponseCacheConfig(store="some_store")) with TestClient(app=app) as client: response_one = client.get("/") assert response_one.status_code == 200 assert response_one.text == mock.return_value response_two = client.get("/") assert response_two.status_code == 200 assert response_two.text == mock.return_value assert mock.call_count == 1 assert await app.stores.get("some_store").exists("GET/") async def test_with_stores(store: Store, mock: MagicMock) -> None: @get(cache=True) def handler() -> str: return mock() # type: ignore[no-any-return] app = Litestar([handler], stores={"response_cache": store}) with TestClient(app=app) as client: response_one = client.get("/") assert response_one.status_code == 200 assert response_one.text == mock.return_value response_two = client.get("/") assert response_two.status_code == 200 assert response_two.text == mock.return_value assert mock.call_count == 1 def test_does_not_apply_to_non_cached_routes(mock: MagicMock) -> None: @get("/") def handler() -> str: return mock() # type: ignore[no-any-return] with create_test_client([handler]) as client: first_response = client.get("/") second_response = client.get("/") assert first_response.status_code == 200 assert second_response.status_code == 200 assert mock.call_count == 2 @pytest.mark.parametrize( "cache,expect_applied", [ (True, True), (False, False), (1, True), (CACHE_FOREVER, True), ], ) def test_middleware_not_applied_to_non_cached_routes( cache: Union[bool, int, Type[CACHE_FOREVER]], expect_applied: bool ) -> None: @get(path="/", cache=cache) def handler() -> None: ... client = create_test_client(route_handlers=[handler]) unpacked_middleware = [] cur = client.app.asgi_router.root_route_map_node.children["/"].asgi_handlers["GET"][0] while hasattr(cur, "app"): unpacked_middleware.append(cur) cur = cur.app unpacked_middleware.append(cur) assert len([m for m in unpacked_middleware if isinstance(m, ResponseCacheMiddleware)]) == int(expect_applied) async def test_compression_applies_before_cache() -> None: return_value = "_litestar_" * 4000 mock = MagicMock(return_value=return_value) @get(path="/", cache=True) def handler_fn() -> str: return mock() # type: ignore[no-any-return] app = Litestar( route_handlers=[handler_fn], compression_config=CompressionConfig(backend="gzip"), ) with TestClient(app) as client: client.get("/", headers={"Accept-Encoding": str(CompressionEncoding.GZIP.value)}) stored_value = await app.response_cache_config.get_store_from_app(app).get("GET/") assert stored_value stored_messages = msgspec.msgpack.decode(stored_value) assert gzip.decompress(stored_messages[1]["body"]).decode() == return_value @pytest.mark.parametrize( ("response", "should_cache"), [ (HTTP_200_OK, True), (HTTP_400_BAD_REQUEST, False), (HTTP_500_INTERNAL_SERVER_ERROR, False), (RuntimeError, False), ], ) def test_default_do_response_cache_predicate( mock: MagicMock, response: Union[int, Type[RuntimeError]], should_cache: bool ) -> None: @get("/", cache=True) def handler() -> Response[None]: mock() if isinstance(response, int): return Response(None, status_code=response) raise RuntimeError with create_test_client([handler]) as client: client.get("/") client.get("/") assert mock.call_count == 1 if should_cache else 2 def test_custom_do_response_cache_predicate(mock: MagicMock) -> None: @get("/", cache=True) def handler() -> str: mock() return "OK" def filter_cache_response(_: HTTPScope, __: int) -> bool: return False with create_test_client( [handler], response_cache_config=ResponseCacheConfig(cache_response_filter=filter_cache_response) ) as client: client.get("/") client.get("/") assert mock.call_count == 2 def test_on_multiple_handlers(mock: MagicMock) -> None: @get("/cached-local", cache=10) async def handler() -> str: mock() return "get_response" @post("/cached-local", cache=10) async def handler_post() -> str: mock() return "post_response" with create_test_client([handler, handler_post], after_request=after_request_handler) as client: # POST request to have this cached first_post_response = client.post("/cached-local") assert first_post_response.status_code == HTTP_201_CREATED assert first_post_response.text == "post_response" assert mock.call_count == 1 # GET request to verify it doesn't use the cache created by the previous POST request get_response = client.get("/cached-local") assert get_response.status_code == HTTP_200_OK assert get_response.text == "get_response" assert first_post_response.headers["unique-identifier"] != get_response.headers["unique-identifier"] assert mock.call_count == 2 # POST request to verify it uses the cache generated during the initial POST request second_post_response = client.post("/cached-local") assert second_post_response.status_code == HTTP_201_CREATED assert second_post_response.text == "post_response" assert first_post_response.headers["unique-identifier"] == second_post_response.headers["unique-identifier"] assert mock.call_count == 2 litestar-2.16.0/tests/e2e/test_router_registration.py000066400000000000000000000146431500564371300230030ustar00rootroot00000000000000from typing import Type import pytest from litestar import ( Controller, HttpMethod, Litestar, Router, WebSocket, get, patch, post, put, websocket, ) from litestar import route as route_decorator from litestar.exceptions import ImproperlyConfiguredException @pytest.fixture def controller() -> Type[Controller]: class MyController(Controller): path = "/test" @post(include_in_schema=False) def post_method(self) -> None: pass @get() def get_method(self) -> None: pass @get(path="/{id:int}") def get_by_id_method(self) -> None: pass @websocket(path="/socket") async def ws(self, socket: WebSocket) -> None: pass return MyController def test_register_with_controller_class(controller: Type[Controller]) -> None: router = Router(path="/base", route_handlers=[controller]) assert len(router.routes) == 3 for route in router.routes: if len(route.methods) == 2: assert sorted(route.methods) == sorted(["GET", "OPTIONS"]) # pyright: ignore assert route.path == "/base/test/{id:int}" elif len(route.methods) == 3: assert sorted(route.methods) == sorted(["GET", "POST", "OPTIONS"]) # pyright: ignore assert route.path == "/base/test" def test_register_controller_on_different_routers(controller: Type[Controller]) -> None: first_router = Router(path="/first", route_handlers=[controller]) second_router = Router(path="/second", route_handlers=[controller]) third_router = Router(path="/third", route_handlers=[controller]) for router in (first_router, second_router, third_router): for route in router.routes: if hasattr(route, "route_handlers"): for route_handler in [ handler for handler in route.route_handlers # pyright: ignore if handler.handler_name != "options_handler" ]: assert route_handler.owner is not None assert route_handler.owner.owner is not None assert route_handler.owner.owner is router else: assert route.route_handler.owner is not None # pyright: ignore assert route.route_handler.owner.owner is not None # pyright: ignore assert route.route_handler.owner.owner is router # pyright: ignore def test_register_with_router_instance(controller: Type[Controller]) -> None: top_level_router = Router(path="/top-level", route_handlers=[controller]) base_router = Router(path="/base", route_handlers=[top_level_router]) assert len(base_router.routes) == 3 for route in base_router.routes: if len(route.methods) == 2: assert sorted(route.methods) == sorted(["GET", "OPTIONS"]) # pyright: ignore assert route.path == "/base/top-level/test/{id:int}" elif len(route.methods) == 3: assert sorted(route.methods) == sorted(["GET", "POST", "OPTIONS"]) # pyright: ignore assert route.path == "/base/top-level/test" def test_register_with_route_handler_functions() -> None: @route_decorator(path="/first", http_method=[HttpMethod.GET, HttpMethod.POST], status_code=200) def first_route_handler() -> None: pass @get(path="/second") def second_route_handler() -> None: pass @patch(path="/first") def third_route_handler() -> None: pass router = Router(path="/base", route_handlers=[first_route_handler, second_route_handler, third_route_handler]) assert len(router.routes) == 2 for route in router.routes: if len(route.methods) == 2: assert sorted(route.methods) == sorted(["GET", "OPTIONS"]) # pyright: ignore assert route.path == "/base/second" else: assert sorted(route.methods) == sorted(["GET", "POST", "PATCH", "OPTIONS"]) # pyright: ignore assert route.path == "/base/first" assert route.path == "/base/first" def test_register_validation_wrong_class() -> None: class MyCustomClass: @get(path="/first") def first_route_handler(self) -> None: pass @get(path="/first") def second_route_handler(self) -> None: pass with pytest.raises(ImproperlyConfiguredException): Router(path="/base", route_handlers=[MyCustomClass]) def test_register_already_registered_router() -> None: first_router = Router(path="/first", route_handlers=[]) Router(path="/second", route_handlers=[first_router]) Router(path="/third", route_handlers=[first_router]) def test_register_router_on_itself() -> None: router = Router(path="/first", route_handlers=[]) with pytest.raises(ImproperlyConfiguredException): router.register(router) def test_route_handler_method_view(controller: Type[Controller]) -> None: @get(path="/root") def handler() -> None: ... def _handler() -> None: ... put_handler = put("/modify")(_handler) post_handler = post("/send")(_handler) first_router = Router(path="/first", route_handlers=[controller, post_handler, put_handler]) second_router = Router(path="/second", route_handlers=[controller, post_handler, put_handler]) app = Litestar(route_handlers=[first_router, second_router, handler]) assert app.route_handler_method_view[str(handler)] == ["/root"] assert app.route_handler_method_view[str(controller.get_method)] == [ # type: ignore[attr-defined] "/first/test", "/second/test", ] assert app.route_handler_method_view[str(controller.ws)] == [ # type: ignore[attr-defined] "/first/test/socket", "/second/test/socket", ] assert app.route_handler_method_view[str(put_handler)] == [ "/first/send", "/first/modify", "/second/send", "/second/modify", ] assert app.route_handler_method_view[str(post_handler)] == [ "/first/send", "/first/modify", "/second/send", "/second/modify", ] def test_missing_path_param_type(controller: Type[Controller]) -> None: missing_path_type = "/missing_path_type/{path_type}" @get(path=missing_path_type) def handler() -> None: ... with pytest.raises(ImproperlyConfiguredException) as exc: Router(path="/", route_handlers=[handler]) assert missing_path_type in exc.value.args[0] litestar-2.16.0/tests/e2e/test_routing/000077500000000000000000000000001500564371300177765ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_routing/__init__.py000066400000000000000000000000001500564371300220750ustar00rootroot00000000000000litestar-2.16.0/tests/e2e/test_routing/conftest.py000066400000000000000000000020071500564371300221740ustar00rootroot00000000000000import time from pathlib import Path from typing import Callable, List import httpx import psutil import pytest from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch @pytest.fixture() def run_server(tmp_path: Path, request: FixtureRequest, monkeypatch: MonkeyPatch) -> Callable[[str, List[str]], None]: def runner(app: str, server_command: List[str]) -> None: tmp_path.joinpath("app.py").write_text(app) monkeypatch.chdir(tmp_path) proc = psutil.Popen(server_command) def kill() -> None: for child in proc.children(recursive=True): child.kill() proc.kill() proc.wait() request.addfinalizer(kill) for _ in range(50): try: httpx.get("http://127.0.0.1:9999/", timeout=0.1) break except httpx.TransportError: time.sleep(0.1) else: raise RuntimeError("App failed to come online") return runner litestar-2.16.0/tests/e2e/test_routing/test_asset_url_path.py000066400000000000000000000025611500564371300244300ustar00rootroot00000000000000from typing import TYPE_CHECKING import pytest from litestar import Litestar, get from litestar.exceptions import NoRouteMatchFoundException from litestar.static_files.config import StaticFilesConfig if TYPE_CHECKING: from pathlib import Path def test_url_for_static_asset(tmp_path: "Path") -> None: app = Litestar( route_handlers=[], static_files_config=[StaticFilesConfig(path="/static/path", directories=[tmp_path], name="asset")], ) url_path = app.url_for_static_asset("asset", "abc/def.css") assert url_path == "/static/path/abc/def.css" def test_url_for_static_asset_doesnt_work_with_http_handler_name(tmp_path: "Path") -> None: @get("/handler", name="handler") def handler() -> None: pass app = Litestar( route_handlers=[handler], static_files_config=[StaticFilesConfig(path="/static/path", directories=[tmp_path], name="asset")], ) with pytest.raises(NoRouteMatchFoundException): app.url_for_static_asset("handler", "abc/def.css") def test_url_for_static_asset_validates_name(tmp_path: "Path") -> None: app = Litestar( route_handlers=[], static_files_config=[StaticFilesConfig(path="/static/path", directories=[tmp_path], name="asset")], ) with pytest.raises(NoRouteMatchFoundException): app.url_for_static_asset("non-existing-name", "abc/def.css") litestar-2.16.0/tests/e2e/test_routing/test_path_mounting.py000066400000000000000000000115101500564371300242610ustar00rootroot00000000000000from pathlib import Path from typing import TYPE_CHECKING, Callable, List import httpx import pytest from _pytest.monkeypatch import MonkeyPatch from litestar import Litestar, MediaType, asgi, get, websocket from litestar.exceptions import ImproperlyConfiguredException from litestar.response.base import ASGIResponse from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.connection import WebSocket from litestar.types import Receive, Scope, Send def test_supports_mounting() -> None: @asgi("/base/sub/path", is_mount=True) async def asgi_handler(scope: "Scope", receive: "Receive", send: "Send") -> None: response = ASGIResponse(body=scope["path"].encode(), media_type=MediaType.TEXT) await response(scope, receive, send) @asgi("/sub/path", is_mount=True) async def asgi_handler_mount_path(scope: "Scope", receive: "Receive", send: "Send") -> None: response = ASGIResponse(body=scope["path"].encode(), media_type=MediaType.TEXT) await response(scope, receive, send) @asgi("/not/mount") async def asgi_handler_not_mounted_path(scope: "Scope", receive: "Receive", send: "Send") -> None: response = ASGIResponse(body=scope["path"].encode(), media_type=MediaType.TEXT) await response(scope, receive, send) with create_test_client( route_handlers=[asgi_handler, asgi_handler_mount_path, asgi_handler_not_mounted_path] ) as client: response = client.get("/base/sub/path") assert response.status_code == HTTP_200_OK assert response.text == "/" response = client.get("/base/sub/path/abcd") assert response.status_code == HTTP_200_OK assert response.text == "/abcd/" response = client.get("/base/sub/path/abcd/complex/123/terminus") assert response.status_code == HTTP_200_OK assert response.text == "/abcd/complex/123/terminus/" response = client.get("/sub/path/deep/path") assert response.status_code == HTTP_200_OK assert response.text == "/deep/path/" response = client.get("/not/mount") assert response.status_code == HTTP_200_OK assert response.text == "/not/mount" def test_supports_sub_routes_below_asgi_handlers() -> None: @asgi("/base/sub/path") async def asgi_handler(scope: "Scope", receive: "Receive", send: "Send") -> None: response = ASGIResponse(body=scope["path"].encode(), media_type=MediaType.TEXT) await response(scope, receive, send) @get("/base/sub/path/abc") def regular_handler() -> None: return assert Litestar(route_handlers=[asgi_handler, regular_handler]) def test_does_not_support_asgi_handlers_on_same_level_as_regular_handlers() -> None: @asgi("/base/sub/path") async def asgi_handler(scope: "Scope", receive: "Receive", send: "Send") -> None: response = ASGIResponse(body=scope["path"].encode(), media_type=MediaType.TEXT) await response(scope, receive, send) @get("/base/sub/path") def regular_handler() -> None: return with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[asgi_handler, regular_handler]) def test_does_not_support_asgi_handlers_on_same_level_as_websockets() -> None: @asgi("/base/sub/path") async def asgi_handler(scope: "Scope", receive: "Receive", send: "Send") -> None: response = ASGIResponse(body=scope["path"].encode(), media_type=MediaType.TEXT) await response(scope, receive, send) @websocket("/base/sub/path") async def regular_handler(socket: "WebSocket") -> None: return with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[asgi_handler, regular_handler]) @pytest.mark.parametrize( "server_command", [ pytest.param(["uvicorn", "app:app", "--port", "9999"], id="uvicorn"), pytest.param(["hypercorn", "app:app", "--bind", "127.0.0.1:9999"], id="hypercorn"), pytest.param(["daphne", "app:app", "--port", "9999"], id="daphne"), ], ) @pytest.mark.xdist_group("live_server_test") @pytest.mark.server_integration def test_path_mounting_live_server( tmp_path: Path, monkeypatch: MonkeyPatch, server_command: List[str], run_server: Callable[[str, List[str]], None] ) -> None: app = """ from litestar import asgi, Litestar from litestar.types import Receive, Scope, Send from litestar.response.base import ASGIResponse @asgi("/sub/path", is_mount=True) async def handler(scope: Scope, receive: Receive, send: Send) -> None: response = ASGIResponse(body=scope["path"].encode()) await response(scope, receive, send) app = Litestar(route_handlers=[handler]) """ run_server(app, server_command) res = httpx.get("http://127.0.0.1:9999/sub/path/fragment") assert res.status_code == 200 assert res.text == "/fragment/" litestar-2.16.0/tests/e2e/test_routing/test_path_resolution.py000066400000000000000000000337071500564371300246400ustar00rootroot00000000000000from pathlib import Path from typing import Any, Callable, List, Optional, Type import httpx import pytest from _pytest.monkeypatch import MonkeyPatch from litestar import Controller, MediaType, Router, delete, get, post from litestar.status_codes import ( HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND, HTTP_405_METHOD_NOT_ALLOWED, ) from litestar.testing import create_test_client @delete(sync_to_thread=False) def root_delete_handler() -> None: return None @pytest.mark.parametrize( "request_path, router_path, status_code", [ ( "/path/1/2/sub/c892496f-b1fd-4b91-bdb8-b46f92df1716", "/path/{first:int}/{second:str}/sub/{third:uuid}", int(HTTP_200_OK), ), ( "/path/1/2/sub/2535a9cb-6554-4d85-bb3b-ad38362f63c7/", "/path/{first:int}/{second:str}/sub/{third:uuid}/", int(HTTP_200_OK), ), ("/", "/", int(HTTP_200_OK)), ("", "", int(HTTP_200_OK)), ( "/a/b/c/d/path/1/2/sub/d4aca431-2e02-4818-824b-a2ddc6a64e9c/", "/path/{first:int}/{second:str}/sub/{third:uuid}/", int(HTTP_404_NOT_FOUND), ), ], ) def test_path_parsing_and_matching(request_path: str, router_path: str, status_code: int) -> None: @get(path=router_path) def test_method() -> None: return None with create_test_client(test_method) as client: response = client.get(request_path) assert response.status_code == status_code def test_path_parsing_with_ambiguous_paths() -> None: @get(path="/{path_param:int}", media_type=MediaType.TEXT) def path_param(path_param: int) -> str: return str(path_param) @get(path="/query_param", media_type=MediaType.TEXT) def query_param(value: int) -> str: return str(value) @get(path="/mixed/{path_param:int}", media_type=MediaType.TEXT) def mixed_params(path_param: int, value: int) -> str: return str(path_param + value) with create_test_client([path_param, query_param, mixed_params]) as client: response = client.get("/1") assert response.status_code == HTTP_200_OK response = client.get("/query_param?value=1") assert response.status_code == HTTP_200_OK response = client.get("/mixed/1/?value=1") assert response.status_code == HTTP_200_OK @pytest.mark.parametrize( "decorator, test_path, decorator_path, delete_handler", [ (get, "", "/something", None), (get, "/", "/something", None), (get, "", "/", None), (get, "/", "/", None), (get, "", "", None), (get, "/", "", None), (get, "", "/something", root_delete_handler), (get, "/", "/something", root_delete_handler), (get, "", "/", root_delete_handler), (get, "/", "/", root_delete_handler), (get, "", "", root_delete_handler), (get, "/", "", root_delete_handler), ], ) def test_root_route_handler( decorator: Type[get], test_path: str, decorator_path: str, delete_handler: Optional[Callable] ) -> None: class MyController(Controller): path = test_path @decorator(path=decorator_path) def test_method(self) -> str: return "hello" with create_test_client([MyController, delete_handler] if delete_handler else MyController) as client: response = client.get(decorator_path or test_path) assert response.status_code == HTTP_200_OK if delete_handler: delete_response = client.delete("/") assert delete_response.status_code == HTTP_204_NO_CONTENT def test_handler_multi_paths() -> None: @get(path=["/", "/something", "/{some_id:int}", "/something/{some_id:int}"], media_type=MediaType.TEXT) def handler_fn(some_id: int = 1) -> str: assert some_id return str(some_id) with create_test_client(handler_fn) as client: first_response = client.get("/") assert first_response.status_code == HTTP_200_OK assert first_response.text == "1" second_response = client.get("/2") assert second_response.status_code == HTTP_200_OK assert second_response.text == "2" third_response = client.get("/something") assert third_response.status_code == HTTP_200_OK assert third_response.text == "1" fourth_response = client.get("/something/2") assert fourth_response.status_code == HTTP_200_OK assert fourth_response.text == "2" @pytest.mark.parametrize( "handler_path, request_path, expected_status_code", [ ("/sub-path", "/", HTTP_404_NOT_FOUND), ("/sub/path", "/sub-path", HTTP_404_NOT_FOUND), ("/sub/path", "/sub", HTTP_404_NOT_FOUND), ("/sub/path/{path_param:int}", "/sub/path", HTTP_404_NOT_FOUND), ("/sub/path/{path_param:int}", "/sub/path/abcd", HTTP_404_NOT_FOUND), ("/sub/path/{path_param:uuid}", "/sub/path/100", HTTP_404_NOT_FOUND), ("/sub/path/{path_param:float}", "/sub/path/abcd", HTTP_404_NOT_FOUND), ], ) def test_path_validation(handler_path: str, request_path: str, expected_status_code: int) -> None: @get(handler_path) def handler_fn(**kwargs: Any) -> None: ... with create_test_client(handler_fn) as client: response = client.get(request_path) assert response.status_code == expected_status_code async def test_http_route_raises_for_unsupported_method(anyio_backend: str) -> None: @get() def my_get_handler() -> None: pass @post() def my_post_handler() -> None: pass with create_test_client(route_handlers=[my_get_handler, my_post_handler]) as client: response = client.delete("/") assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED def test_path_order() -> None: @get(path=["/something/{some_id:int}", "/"], media_type=MediaType.TEXT) def handler_fn(some_id: int = 1) -> str: return str(some_id) with create_test_client(handler_fn) as client: first_response = client.get("/something/5") assert first_response.status_code == HTTP_200_OK assert first_response.text == "5" second_response = client.get("/") assert second_response.status_code == HTTP_200_OK assert second_response.text == "1" @pytest.mark.parametrize( "handler_path, request_path, expected_status_code, expected_param", [ ("/name:str/{name:str}", "/name:str/test", HTTP_200_OK, "test"), ("/user/*/{name:str}", "/user/foo/bar", HTTP_404_NOT_FOUND, None), ("/user/*/{name:str}", "/user/*/bar", HTTP_200_OK, "bar"), ], ) def test_special_chars( handler_path: str, request_path: str, expected_status_code: int, expected_param: Optional[str] ) -> None: @get(path=handler_path, media_type=MediaType.TEXT) def handler_fn(name: str) -> str: return name with create_test_client(handler_fn) as client: response = client.get(request_path) assert response.status_code == expected_status_code if response.status_code == HTTP_200_OK: assert response.text == expected_param def test_no_404_where_list_route_has_handlers_and_child_route_has_path_param() -> None: # https://github.com/litestar-org/litestar/issues/816 # the error condition requires the path to not be a plain route, hence the prefixed path parameters @get("/{a:str}/b") def get_list() -> List[str]: return ["ok"] @get("/{a:str}/b/{c:int}") def get_member() -> str: return "ok" with create_test_client(route_handlers=[get_list, get_member]) as client: resp = client.get("/scope/b") assert resp.status_code == 200 assert resp.json() == ["ok"] def test_support_of_different_branches() -> None: @get("/{foo:int}/foo") def foo_handler(foo: int) -> int: return foo @get("/{bar:str}/bar") def bar_handler(bar: str) -> str: return bar with create_test_client([foo_handler, bar_handler]) as client: response = client.get("1/foo") assert response.status_code == HTTP_200_OK response = client.get("a/bar") assert response.status_code == HTTP_200_OK def test_support_for_path_type_parameters() -> None: @get(path="/{string_param:str}") def lower_handler(string_param: str) -> str: return string_param @get(path="/{string_param:str}/{path_param:path}") def upper_handler(string_param: str, path_param: Path) -> str: return string_param + str(path_param) with create_test_client([lower_handler, upper_handler]) as client: response = client.get("/abc") assert response.status_code == HTTP_200_OK response = client.get("/abc/a/b/c") assert response.status_code == HTTP_200_OK def test_base_path_param_resolution() -> None: # https://github.com/litestar-org/litestar/issues/1830 @get("/{name:str}") async def hello_world(name: str) -> str: return f"Hello, {name}!" with create_test_client(hello_world) as client: response = client.get("/jon") assert response.status_code == HTTP_200_OK assert response.text == "Hello, jon!" response = client.get("/jon/bon") assert response.status_code == HTTP_404_NOT_FOUND response = client.get("/jon/bon/jovi") assert response.status_code == HTTP_404_NOT_FOUND def test_base_path_param_resolution_2() -> None: # https://github.com/litestar-org/litestar/issues/1830#issuecomment-1642291149 @get("/{name:str}") async def name_greeting(name: str) -> str: return f"Hello, {name}!" @get("/{age:int}") async def age_greeting(name: str, age: int) -> str: return f"Hello, {name}! {age} is a great age to be!" age_router = Router("/{name:str}/age", route_handlers=[age_greeting]) name_router = Router("/name", route_handlers=[name_greeting, age_router]) with create_test_client(name_router) as client: response = client.get("/name/jon") assert response.status_code == HTTP_200_OK assert response.text == "Hello, jon!" response = client.get("/name/jon/age/42") assert response.status_code == HTTP_200_OK assert response.text == "Hello, jon! 42 is a great age to be!" response = client.get("/name/jon/bon") assert response.status_code == HTTP_404_NOT_FOUND @pytest.mark.parametrize( "server_command", [ pytest.param(["uvicorn", "app:app", "--port", "9999", "--root-path", "/test"], id="uvicorn"), pytest.param(["hypercorn", "app:app", "--bind", "127.0.0.1:9999", "--root-path", "/test"], id="hypercorn"), pytest.param(["daphne", "app:app", "--port", "9999", "--root-path", "/test"], id="daphne"), ], ) @pytest.mark.xdist_group("live_server_test") @pytest.mark.server_integration def test_server_root_path_handling( tmp_path: Path, monkeypatch: MonkeyPatch, server_command: List[str], run_server: Callable[[str, List[str]], None] ) -> None: # https://github.com/litestar-org/litestar/issues/2998 app = """ from litestar import Litestar, get, Request from typing import List @get("/handler") async def handler(request: Request) -> List[str]: return [request.scope["path"], request.scope["root_path"]] app = Litestar(route_handlers=[handler]) """ run_server(app, server_command) assert httpx.get("http://127.0.0.1:9999/handler").json() == ["/handler", "/test"] @pytest.mark.parametrize( "server_command", [ pytest.param(["uvicorn", "app:app", "--port", "9999", "--root-path", "/test"], id="uvicorn"), pytest.param(["hypercorn", "app:app", "--bind", "127.0.0.1:9999", "--root-path", "/test"], id="hypercorn"), pytest.param(["daphne", "app:app", "--port", "9999", "--root-path", "/test"], id="daphne"), ], ) @pytest.mark.xdist_group("live_server_test") @pytest.mark.server_integration def test_server_root_path_handling_empty_path( tmp_path: Path, monkeypatch: MonkeyPatch, server_command: List[str], run_server: Callable[[str, List[str]], None] ) -> None: # https://github.com/litestar-org/litestar/issues/3041 app = """ from pathlib import Path from litestar import Litestar from litestar.handlers import get from typing import Optional @get(path=["/", "/{path:path}"]) async def pathfinder(path: Optional[Path]) -> str: return str(path) app = Litestar(route_handlers=[pathfinder], debug=True) """ run_server(app, server_command) assert httpx.get("http://127.0.0.1:9999/").text == "None" assert httpx.get("http://127.0.0.1:9999/something").text == "/something" @pytest.mark.parametrize( "server_command", [ pytest.param(["uvicorn", "app:app", "--port", "9999", "--root-path", "/test"], id="uvicorn"), pytest.param(["hypercorn", "app:app", "--bind", "127.0.0.1:9999", "--root-path", "/test"], id="hypercorn"), pytest.param(["daphne", "app:app", "--port", "9999", "--root-path", "/test"], id="daphne"), ], ) @pytest.mark.xdist_group("live_server_test") @pytest.mark.server_integration def test_no_path_traversal_from_static_directory( tmp_path: Path, monkeypatch: MonkeyPatch, server_command: List[str], run_server: Callable[[str, List[str]], None] ) -> None: import http.client static = tmp_path / "static" static.mkdir() (static / "index.html").write_text("Hello, World!") app = """ from pathlib import Path from litestar import Litestar from litestar.static_files import create_static_files_router import uvicorn app = Litestar( route_handlers=[ create_static_files_router(path="/static", directories=["static"]), ], ) """ def send_request(host: str, port: int, path: str) -> http.client.HTTPResponse: connection = http.client.HTTPConnection(host, port) connection.request("GET", path) resp = connection.getresponse() connection.close() return resp run_server(app, server_command) response = send_request("127.0.0.1", 9999, "/static/index.html") assert response.status == 200 response = send_request("127.0.0.1", 9999, "/static/../app.py") assert response.status == 404 litestar-2.16.0/tests/e2e/test_routing/test_route_indexing.py000066400000000000000000000112761500564371300244410ustar00rootroot00000000000000from typing import TYPE_CHECKING, Any, Type import pytest from litestar import ( Controller, Litestar, Router, asgi, delete, get, patch, post, put, websocket, ) from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.static_files.config import StaticFilesConfig if TYPE_CHECKING: from pathlib import Path @pytest.mark.parametrize("decorator", [get, post, patch, put, delete]) def test_indexes_handlers(decorator: Type[HTTPRouteHandler]) -> None: @decorator("/path-one/{param:str}", name="handler-name") # type: ignore[call-arg] def handler() -> None: return None @asgi("/asgi-path", name="asgi-name") async def asgi_handler(scope: Any, receive: Any, send: Any) -> None: pass @websocket("/websocket-path", name="websocket-name") async def websocket_handler(socket: Any) -> None: pass router = Router("router-path/", route_handlers=[handler]) app = Litestar(route_handlers=[router, websocket_handler, asgi_handler]) handler_index = app.get_handler_index_by_name("handler-name") assert handler_index assert handler_index["paths"] == ["/router-path/path-one/{param:str}"] assert str(handler_index["handler"]) == str(handler) handler_index = app.get_handler_index_by_name("asgi-name") assert handler_index assert handler_index["paths"] == ["/asgi-path"] assert str(handler_index["handler"]) == str(asgi_handler) handler_index = app.get_handler_index_by_name("websocket-name") assert handler_index assert handler_index["paths"] == ["/websocket-path"] assert str(handler_index["handler"]) == str(websocket_handler) assert app.get_handler_index_by_name("nope") is None @pytest.mark.parametrize("decorator", [get, post, patch, put, delete]) def test_default_indexes_handlers(decorator: Type[HTTPRouteHandler]) -> None: @decorator("/handler") # type: ignore[call-arg] def handler() -> None: pass @decorator("/named_handler", name="named_handler") # type: ignore[call-arg] def named_handler() -> None: pass class MyController(Controller): path = "/test" @decorator() # type: ignore[call-arg] def handler(self) -> None: pass router = Router("router/", route_handlers=[handler, named_handler, MyController]) app = Litestar(route_handlers=[router]) handler_index = app.get_handler_index_by_name(str(handler)) assert handler_index assert handler_index["paths"] == ["/router/handler"] assert str(handler_index["handler"]) == str(handler) assert handler_index["identifier"] == str(handler) handler_index = app.get_handler_index_by_name(str(MyController.handler)) assert handler_index assert handler_index["paths"] == ["/router/test"] assert handler_index["identifier"] == str(MyController.handler) # check that default str(named_handler) does not override explicit name handler_index = app.get_handler_index_by_name(str(named_handler)) assert handler_index is None @pytest.mark.parametrize("decorator", [get, post, patch, put, delete]) def test_indexes_handlers_with_multiple_paths(decorator: Type[HTTPRouteHandler]) -> None: @decorator(["/path-one", "/path-one/{param:str}"], name="handler") # type: ignore[call-arg] def handler() -> None: return None @decorator(["/path-two"], name="handler-two") # type: ignore[call-arg] def handler_two() -> None: return None router = Router("router-one/", route_handlers=[handler_two]) router_two = Router("router-two/", route_handlers=[handler_two]) app = Litestar(route_handlers=[router, router_two, handler]) handler_index = app.get_handler_index_by_name("handler") assert handler_index assert handler_index["paths"] == ["/path-one", "/path-one/{param:str}"] assert str(handler_index["handler"]) == str(handler) handler_index = app.get_handler_index_by_name("handler-two") assert handler_index assert handler_index["paths"] == ["/router-one/path-two", "/router-two/path-two"] assert str(handler_index["handler"]) == str(handler_two) def test_indexing_validation(tmp_path: "Path") -> None: @get("/abc", name="same-name") def handler_one() -> None: pass @get("/xyz", name="same-name") def handler_two() -> None: pass with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[handler_one, handler_two]) with pytest.raises(ImproperlyConfiguredException): Litestar( route_handlers=[handler_one], static_files_config=[StaticFilesConfig(path="/static", directories=[tmp_path], name="same-name")], ) litestar-2.16.0/tests/e2e/test_routing/test_route_reverse.py000066400000000000000000000126241500564371300243050ustar00rootroot00000000000000from datetime import date, datetime, time, timedelta from pathlib import Path from typing import Type from uuid import UUID import pytest from litestar import Litestar, Router, delete, get, patch, post, put from litestar.exceptions import NoRouteMatchFoundException from litestar.handlers.http_handlers import HTTPRouteHandler @pytest.mark.parametrize("decorator", [get, post, patch, put, delete]) def test_route_reverse(decorator: Type[HTTPRouteHandler]) -> None: @decorator("/path-one/{param:str}", name="handler-name") # type: ignore[call-arg] def handler() -> None: return None @decorator("/path-two", name="handler-no-params") # type: ignore[call-arg] def handler_no_params() -> None: return None @decorator("/multiple/{str_param:str}/params/{int_param:int}/", name="multiple-params-handler-name") # type: ignore[call-arg] def handler2() -> None: return None @decorator( ["/handler3", "/handler3/{str_param:str}/", "/handler3/{str_param:str}/{int_param:int}/"], name="multiple-default-params", ) # type: ignore[call-arg] def handler3(str_param: str = "default", int_param: int = 0) -> None: return None @decorator(["/handler4/int/{int_param:int}", "/handler4/str/{str_param:str}"], name="handler4") # type: ignore[call-arg] def handler4(int_param: int = 1, str_param: str = "str") -> None: return None router = Router("router-path/", route_handlers=[handler, handler_no_params, handler3, handler4]) router_with_param = Router("router-with-param/{router_param:str}", route_handlers=[handler2]) app = Litestar(route_handlers=[router, router_with_param]) reversed_url_path = app.route_reverse("handler-name", param="param-value") assert reversed_url_path == "/router-path/path-one/param-value" reversed_url_path = app.route_reverse("handler-no-params") assert reversed_url_path == "/router-path/path-two" reversed_url_path = app.route_reverse( "multiple-params-handler-name", router_param="router", str_param="abc", int_param=123 ) assert reversed_url_path == "/router-with-param/router/multiple/abc/params/123" reversed_url_path = app.route_reverse("handler4", int_param=100) assert reversed_url_path == "/router-path/handler4/int/100" reversed_url_path = app.route_reverse("handler4", str_param="string") assert reversed_url_path == "/router-path/handler4/str/string" with pytest.raises(NoRouteMatchFoundException): reversed_url_path = app.route_reverse("nonexistent-handler") @pytest.mark.parametrize( "complex_path_param", [("time", time(hour=14), "14:00"), ("float", float(1 / 3), "0.33")], ) def test_route_reverse_validation_complex_params(complex_path_param) -> None: # type: ignore[no-untyped-def] param_type, param_value, param_manual_str = complex_path_param @get(f"/abc/{{param:{param_type}}}", name="handler") def handler() -> None: pass app = Litestar(route_handlers=[handler]) # test that complex types of path params accept either itself # or string but nothing else with pytest.raises(NoRouteMatchFoundException): app.route_reverse("handler", param=123) reversed_url_path = app.route_reverse("handler", param=param_manual_str) assert reversed_url_path == f"/abc/{param_manual_str}" reversed_url_path = app.route_reverse("handler", param=param_value) assert reversed_url_path == f"/abc/{param_value}" def test_route_reverse_validation() -> None: @get("/abc/{param:int}", name="handler-name") def handler_one() -> None: pass @get("/def/{param:str}", name="another-handler-name") def handler_two() -> None: pass app = Litestar(route_handlers=[handler_one, handler_two]) with pytest.raises(NoRouteMatchFoundException): app.route_reverse("handler-name") with pytest.raises(NoRouteMatchFoundException): app.route_reverse("handler-name", param="str") with pytest.raises(NoRouteMatchFoundException): app.route_reverse("another-handler-name", param=1) def test_route_reverse_allow_string_params() -> None: @get( "/strings-everywhere/{datetime_param:datetime}/{date_param:date}/" "{time_param:time}/{timedelta_param:timedelta}/" "{float_param:float}/{uuid_param:uuid}/{path_param:path}", name="strings-everywhere-handler", ) def strings_everywhere_handler( datetime_param: datetime, date_param: date, time_param: time, timedelta_param: timedelta, float_param: float, uuid_param: UUID, path_param: Path, ) -> None: return None app = Litestar(route_handlers=[strings_everywhere_handler]) reversed_url_path = app.route_reverse( "strings-everywhere-handler", datetime_param="0001-01-01T01:01:01.000001Z", # datetime(1, 1, 1, 1, 1, 1, 1, tzinfo=UTC) date_param="0001-01-01", # date(1,1,1) time_param="01:01:01.000001Z", # time(1, 1, 1, 1, tzinfo=UTC) timedelta_param="P8DT3661.001001S", # timedelta(1, 1, 1, 1, 1, 1, 1) float_param="0.1", uuid_param="00000000-0000-0000-0000-000000000000", # UUID(int=0) path_param="/home/user", # Path("/home/user/"), ) assert ( reversed_url_path == "/strings-everywhere/0001-01-01T01:01:01.000001Z/" "0001-01-01/01:01:01.000001Z/P8DT3661.001001S/0.1/" "00000000-0000-0000-0000-000000000000/home/user" ) litestar-2.16.0/tests/e2e/test_routing/test_validations.py000066400000000000000000000051271500564371300237310ustar00rootroot00000000000000from typing import Any import pytest from litestar import Controller, Litestar, WebSocket, get, post, websocket from litestar.exceptions import ImproperlyConfiguredException from litestar.static_files import StaticFilesConfig from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client def test_register_validation_duplicate_handlers_for_same_route_and_method() -> None: @get(path="/first") def first_route_handler() -> None: pass @get(path="/first") def second_route_handler() -> None: pass with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[first_route_handler, second_route_handler]) def test_supports_websocket_and_http_handlers() -> None: @get(path="/") def http_handler() -> dict: return {"hello": "world"} @websocket(path="/") async def websocket_handler(socket: "WebSocket[Any, Any, Any]") -> None: await socket.accept() await socket.send_json({"hello": "world"}) await socket.close() with create_test_client([http_handler, websocket_handler]) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.json() == {"hello": "world"} with client.websocket_connect("/") as ws: ws_response = ws.receive_json() assert ws_response == {"hello": "world"} def test_controller_supports_websocket_and_http_handlers() -> None: class MyController(Controller): path = "/" @get() def http_handler( self, ) -> dict: return {"hello": "world"} @websocket() async def websocket_handler(self, socket: "WebSocket[Any, Any, Any]") -> None: await socket.accept() await socket.send_json({"hello": "world"}) await socket.close() with create_test_client(MyController) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.json() == {"hello": "world"} with client.websocket_connect("/") as ws: ws_response = ws.receive_json() assert ws_response == {"hello": "world"} def test_validate_static_files_with_same_path_in_handler() -> None: # make sure this works and does not lead to a recursion error # https://github.com/litestar-org/litestar/issues/2629 @post("/uploads") async def handler() -> None: pass Litestar( [handler], static_files_config=[ StaticFilesConfig(directories=["uploads"], path="/uploads"), ], ) litestar-2.16.0/tests/e2e/test_starlette_responses.py000066400000000000000000000010721500564371300227710ustar00rootroot00000000000000from starlette.responses import JSONResponse from litestar import get from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client def test_starlette_json_response() -> None: @get("/starlette-json-response") def get_json_response() -> JSONResponse: return JSONResponse(content={"hello": "world"}) with create_test_client(get_json_response) as client: response = client.get("/starlette-json-response") assert response.status_code == HTTP_200_OK assert response.json() == {"hello": "world"} litestar-2.16.0/tests/examples/000077500000000000000000000000001500564371300164135ustar00rootroot00000000000000litestar-2.16.0/tests/examples/__init__.py000066400000000000000000000000001500564371300205120ustar00rootroot00000000000000litestar-2.16.0/tests/examples/conftest.py000066400000000000000000000006221500564371300206120ustar00rootroot00000000000000import logging from typing import Generator import pytest @pytest.fixture(autouse=True) def disable_httpx_logging() -> Generator[None, None, None]: # ensure that httpx logging is not interfering with our test client httpx_logger = logging.getLogger("httpx") initial_level = httpx_logger.level httpx_logger.setLevel(logging.WARNING) yield httpx_logger.setLevel(initial_level) litestar-2.16.0/tests/examples/test_application_hooks/000077500000000000000000000000001500564371300231605ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_application_hooks/__init__.py000066400000000000000000000000001500564371300252570ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_application_hooks/test_application_after_exception_hook.py000066400000000000000000000013451500564371300333560ustar00rootroot00000000000000import logging from typing import TYPE_CHECKING import pytest from docs.examples.application_hooks import after_exception_hook from litestar.testing import TestClient if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture @pytest.mark.usefixtures("reset_httpx_logging") def test_application_shutdown_hooks(caplog: "LogCaptureFixture") -> None: with caplog.at_level(logging.INFO), TestClient(app=after_exception_hook.app) as client: assert len(caplog.messages) == 0 client.get("/some-path") assert client.app.state.error_count == 1 assert len(caplog.messages) == 1 client.get("/some-path") assert client.app.state.error_count == 2 assert len(caplog.messages) == 2 litestar-2.16.0/tests/examples/test_application_hooks/test_application_before_send.py000066400000000000000000000006541500564371300314340ustar00rootroot00000000000000from docs.examples.application_hooks import before_send_hook from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient def test_application_before_send_hooks() -> None: with TestClient(app=before_send_hook.app) as client: response = client.get("/test") assert response.status_code == HTTP_200_OK assert response.headers.get("My Header") == "value injected during send" litestar-2.16.0/tests/examples/test_application_hooks/test_lifespan_manager.py000066400000000000000000000014411500564371300300640ustar00rootroot00000000000000from unittest.mock import AsyncMock from docs.examples.application_hooks.lifespan_manager import app from pytest_mock import MockerFixture from litestar import get from litestar.datastructures import State from litestar.testing import TestClient class FakeAsyncEngine: dispose = AsyncMock() async def test_startup_and_shutdown_example(mocker: MockerFixture) -> None: mock_create_engine = mocker.patch("docs.examples.application_hooks.lifespan_manager.create_async_engine") mock_create_engine.return_value = FakeAsyncEngine @get("/") def handler(state: State) -> None: assert state.engine is mock_create_engine.return_value app.register(handler) with TestClient(app=app) as client: client.get("/") FakeAsyncEngine.dispose.assert_awaited_once() litestar-2.16.0/tests/examples/test_application_hooks/test_on_app_init.py000066400000000000000000000002451500564371300270710ustar00rootroot00000000000000from docs.examples.application_hooks.on_app_init import app, close_db_connection def test_on_app_init() -> None: assert close_db_connection in app.on_shutdown litestar-2.16.0/tests/examples/test_application_state/000077500000000000000000000000001500564371300231555ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_application_state/__init__.py000066400000000000000000000000001500564371300252540ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_application_state/test_passing_initial_state.py000066400000000000000000000005741500564371300311510ustar00rootroot00000000000000from docs.examples.application_state.passing_initial_state import app from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient def test_passing_initial_state_example() -> None: with TestClient(app) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.json() == {"count": 100} litestar-2.16.0/tests/examples/test_application_state/test_using_application_state.py000066400000000000000000000014711500564371300315010ustar00rootroot00000000000000from logging import INFO from typing import Any import pytest from docs.examples.application_state.using_application_state import app from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient @pytest.mark.usefixtures("reset_httpx_logging") def test_using_application_state(caplog: Any) -> None: with caplog.at_level(INFO, "docs.examples.application_state.using_application_state"), TestClient( app=app ) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert {record.getMessage() for record in caplog.records} == { "state value in middleware: abc123", "state value in dependency: abc123", "state value in handler from `State`: abc123", "state value in handler from `Request`: abc123", } litestar-2.16.0/tests/examples/test_application_state/test_using_custom_state.py000066400000000000000000000005641500564371300305120ustar00rootroot00000000000000from docs.examples.application_state.using_custom_state import app from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient def test_using_custom_state_example() -> None: with TestClient(app) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.json() == {"count": 1} litestar-2.16.0/tests/examples/test_application_state/test_using_immutable_state.py000066400000000000000000000005561500564371300311600ustar00rootroot00000000000000from docs.examples.application_state.using_immutable_state import app from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR from litestar.testing import TestClient def test_using_custom_state_example() -> None: with TestClient(app) as client: response = client.get("/") assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR litestar-2.16.0/tests/examples/test_cache_control_headers.py000066400000000000000000000011461500564371300243240ustar00rootroot00000000000000from docs.examples.datastructures.headers import cache_control from litestar.testing import TestClient def test_cache_control_header() -> None: with TestClient(app=cache_control.app) as client: response = client.get("/population") assert response.headers["cache-control"] in ("max-age=2628288, public", "public, max-age=2628288") response = client.get("/chance_of_rain") assert response.headers["cache-control"] in ("max-age=86400, public", "public, max-age=86400") response = client.get("/timestamp") assert response.headers["cache-control"] == "no-store" litestar-2.16.0/tests/examples/test_contrib/000077500000000000000000000000001500564371300211125ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_contrib/__init__.py000066400000000000000000000000001500564371300232110ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_contrib/prometheus/000077500000000000000000000000001500564371300233055ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_contrib/prometheus/__init__.py000066400000000000000000000000001500564371300254040ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_contrib/prometheus/test_prometheus_exporter_example.py000066400000000000000000000052631500564371300325620ustar00rootroot00000000000000from typing import Any, Dict import pytest from prometheus_client import REGISTRY from litestar import Controller, Litestar, Request, get from litestar.plugins.prometheus import PrometheusMiddleware from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient def clear_collectors() -> None: collectors = list(REGISTRY._collector_to_names.keys()) for collector in collectors: REGISTRY.unregister(collector) PrometheusMiddleware._metrics = {} @pytest.mark.parametrize( "group_path, route_path, route_template, expected_path", [ (True, "/test/litestar", "test/litestar", "/test/litestar"), (False, "/test/litestar", "test/litestar", "/test/litestar"), (True, "/test/litestar", "test/{name:str}", "/test/{name}"), (False, "/test/litestar", "test/{name:str}", "/test/litestar"), ( True, "/project/123a/team/abc/test/hi", "project/{project:str}/team/{team:str}/test/{name:str}", "/project/{project}/team/{team}/test/{name}", ), ( False, "/project/123a/team/abc/test/hi", "project/{project:str}/team/{team:str}/test/{name:str}", "/project/123a/team/abc/test/hi", ), ], ) def test_prometheus_exporter_example( group_path: bool, route_path: str, route_template: str, expected_path: str ) -> None: from docs.examples.plugins.prometheus.using_prometheus_exporter import create_app app = create_app(group_path=group_path) clear_collectors() @get(route_template) def home(name: str) -> Dict[str, Any]: return {"hello": name} app.register(home) with TestClient(app) as client: client.get(route_path) metrics_exporter_response = client.get("/metrics") assert metrics_exporter_response.status_code == HTTP_200_OK metrics = metrics_exporter_response.content.decode() assert expected_path in metrics def test_correct_population_path_template() -> None: class TestController(Controller): path = "/prefix" @get("/{id_:int}") async def b(self, request: Request, id_: int) -> str: return request.scope["path_template"] @get("/{id_:int}/postfix") async def a(self, request: Request, id_: int) -> str: return request.scope["path_template"] app = Litestar([TestController]) with TestClient(app) as client: without_postfix_resp = client.get("/prefix/1") with_postfix_resp = client.get("/prefix/1/postfix") assert without_postfix_resp.content.decode() == "/prefix/{id_}" assert with_postfix_resp.content.decode() == "/prefix/{id_}/postfix" test_prometheus_exporter_example_with_extra_config.py000066400000000000000000000023101500564371300362540ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_contrib/prometheusfrom typing import Any, Dict from prometheus_client import REGISTRY from litestar import get from litestar.plugins.prometheus import PrometheusMiddleware from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient def clear_collectors() -> None: collectors = list(REGISTRY._collector_to_names.keys()) for collector in collectors: REGISTRY.unregister(collector) PrometheusMiddleware._metrics = {} def test_prometheus_exporter_with_extra_config_example() -> None: from docs.examples.plugins.prometheus.using_prometheus_exporter_with_extra_configs import app clear_collectors() @get("/test") def home() -> Dict[str, Any]: return {"hello": "world"} app.register(home) with TestClient(app) as client: client.get("/home") metrics_exporter_response = client.get("/custom-path") assert metrics_exporter_response.status_code == HTTP_200_OK metrics = metrics_exporter_response.content.decode() assert ( """litestar_requests_in_progress{app_name="litestar-example",location="earth",method="GET",path="/custom-path",status_code="200",version_no="v2.0"} 1.0""" in metrics ) litestar-2.16.0/tests/examples/test_contrib/test_piccolo_orm.py000066400000000000000000000046341500564371300250370ustar00rootroot00000000000000import sys from importlib import reload from pathlib import Path from types import ModuleType import pytest from litestar.testing import TestClient try: import piccolo # noqa: F401 except ImportError: pytest.skip("Piccolo not installed", allow_module_level=True) from docs.examples.contrib.piccolo import app as _app_module from piccolo.testing.model_builder import ModelBuilder pytestmark = [ pytest.mark.xdist_group("piccolo"), pytest.mark.skipif( sys.platform != "linux", reason="piccolo ORM itself is not tested against windows and macOS", ), ] @pytest.fixture() def app_module() -> ModuleType: return reload(_app_module) @pytest.fixture(autouse=True) def create_test_data(app_module: ModuleType) -> None: db_path = Path(app_module.DB.path) db_path.unlink(missing_ok=True) app_module.Task.create_table(if_not_exists=True).run_sync() ModelBuilder.build_sync(app_module.Task) yield app_module.Task.alter().drop_table().run_sync() db_path.unlink() def test_get_tasks(app_module): with TestClient(app=app_module.app) as client: response = client.get("/tasks") assert response.status_code == 200 assert len(response.json()) == 1 def test_task_crud(app_module): with TestClient(app=app_module.app) as client: payload = { "name": "Task 1", "completed": False, } response = client.post( "/tasks", json=payload, ) assert response.status_code == 201 assert response.json()["name"] == "Task 1" response = client.get("/tasks") assert response.status_code == 200 assert len(response.json()) == 2 task = app_module.Task.select().first().run_sync() payload = { "id": task["id"], "name": "Task 2", "completed": True, } response = client.patch( f"/tasks/{task['id']}", json=payload, ) assert response.status_code == 200 assert response.json()["name"] == "Task 2" assert response.json()["completed"] is True response = client.delete( f"/tasks/{task['id']}", ) assert response.status_code == 204 response = client.get("/tasks") assert response.status_code == 200 assert response.json()[0]["name"] == "Task 1" assert len(response.json()) == 1 litestar-2.16.0/tests/examples/test_contrib/test_sqlalchemy/000077500000000000000000000000001500564371300243135ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_contrib/test_sqlalchemy/__init__.py000066400000000000000000000000001500564371300264120ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_contrib/test_sqlalchemy/plugins/000077500000000000000000000000001500564371300257745ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_contrib/test_sqlalchemy/plugins/__init__.py000066400000000000000000000000001500564371300300730ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_contrib/test_sqlalchemy/plugins/test_example_apps.py000066400000000000000000000132171500564371300320670ustar00rootroot00000000000000from __future__ import annotations from typing import Any, AsyncIterator, Generator import pytest from _pytest.monkeypatch import MonkeyPatch from sqlalchemy import Engine, StaticPool, create_engine from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine from litestar.plugins.sqlalchemy import EngineConfig from litestar.testing import TestClient pytestmark = pytest.mark.xdist_group("sqlalchemy_examples") @pytest.fixture def data() -> list[dict[str, Any]]: return [{"title": "test", "done": False}] @pytest.fixture() def sqlite_engine() -> Generator[None, Engine]: yield create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool) @pytest.fixture() async def aiosqlite_engine() -> AsyncIterator[AsyncEngine]: yield create_async_engine("sqlite+aiosqlite://", connect_args={"check_same_thread": False}) def test_sqlalchemy_async_plugin_example( data: dict[str, Any], monkeypatch: MonkeyPatch, aiosqlite_engine: AsyncEngine ) -> None: from docs.examples.contrib.sqlalchemy.plugins import sqlalchemy_async_plugin_example monkeypatch.setattr(sqlalchemy_async_plugin_example.config, "engine_instance", aiosqlite_engine) with TestClient(sqlalchemy_async_plugin_example.app) as client: assert client.post("/", json=data[0]).json() == data def test_sqlalchemy_sync_plugin_example(data: dict[str, Any], monkeypatch: MonkeyPatch, sqlite_engine: Engine) -> None: from docs.examples.contrib.sqlalchemy.plugins import sqlalchemy_sync_plugin_example monkeypatch.setattr(sqlalchemy_sync_plugin_example.config, "engine_instance", sqlite_engine) with TestClient(sqlalchemy_sync_plugin_example.app) as client: assert client.post("/", json=data[0]).json() == data def test_sqlalchemy_async_init_plugin_example( data: dict[str, Any], monkeypatch: MonkeyPatch, aiosqlite_engine: AsyncEngine ) -> None: from docs.examples.contrib.sqlalchemy.plugins import sqlalchemy_async_init_plugin_example monkeypatch.setattr(sqlalchemy_async_init_plugin_example.config, "engine_instance", aiosqlite_engine) with TestClient(sqlalchemy_async_init_plugin_example.app) as client: assert client.post("/", json=data[0]).json() == data async def test_sqlalchemy_sync_init_plugin_example( data: dict[str, Any], monkeypatch: MonkeyPatch, sqlite_engine: Engine ) -> None: from docs.examples.contrib.sqlalchemy.plugins import sqlalchemy_sync_init_plugin_example monkeypatch.setattr(sqlalchemy_sync_init_plugin_example.config, "engine_instance", sqlite_engine) with TestClient(sqlalchemy_sync_init_plugin_example.app) as client: assert client.post("/", json=data[0]).json() == data async def test_sqlalchemy_async_init_plugin_dependencies( monkeypatch: MonkeyPatch, aiosqlite_engine: AsyncEngine ) -> None: from docs.examples.contrib.sqlalchemy.plugins import sqlalchemy_async_dependencies monkeypatch.setattr(sqlalchemy_async_dependencies.config, "engine_instance", aiosqlite_engine) with TestClient(sqlalchemy_async_dependencies.app) as client: assert client.post("/").json() == [1, 2] def test_sqlalchemy_sync_init_plugin_dependencies(monkeypatch: MonkeyPatch) -> None: from docs.examples.contrib.sqlalchemy.plugins import sqlalchemy_sync_dependencies engine_config = EngineConfig(connect_args={"check_same_thread": False}, poolclass=StaticPool) monkeypatch.setattr(sqlalchemy_sync_dependencies.config, "connection_string", "sqlite://") monkeypatch.setattr(sqlalchemy_sync_dependencies.config, "engine_config", engine_config) with TestClient(sqlalchemy_sync_dependencies.app) as client: assert client.post("/").json() == [1, 2] def test_sqlalchemy_async_before_send_handler() -> None: from docs.examples.contrib.sqlalchemy.plugins.sqlalchemy_async_before_send_handler import app from litestar.plugins.sqlalchemy import async_autocommit_before_send_handler assert async_autocommit_before_send_handler is app.before_send[0] def test_sqlalchemy_sync_before_send_handler() -> None: from docs.examples.contrib.sqlalchemy.plugins.sqlalchemy_sync_before_send_handler import app from litestar.plugins.sqlalchemy import sync_autocommit_before_send_handler assert sync_autocommit_before_send_handler is app.before_send[0].func def test_sqlalchemy_async_serialization_plugin(data: dict[str, Any]) -> None: from docs.examples.contrib.sqlalchemy.plugins.sqlalchemy_async_serialization_plugin import app with TestClient(app) as client: assert client.post("/", json=data[0]).json() == data def test_sqlalchemy_sync_serialization_plugin(data: dict[str, Any]) -> None: from docs.examples.contrib.sqlalchemy.plugins.sqlalchemy_sync_serialization_plugin import app with TestClient(app) as client: assert client.post("/", json=data[0]).json() == data def test_sqlalchemy_async_serialization_dto(data: dict[str, Any]) -> None: from docs.examples.contrib.sqlalchemy.plugins.sqlalchemy_async_serialization_dto import app with TestClient(app) as client: assert client.post("/", json=data[0]).json() == data def test_sqlalchemy_async_serialization_plugin_marking_fields(data: dict[str, Any]) -> None: from docs.examples.contrib.sqlalchemy.plugins.sqlalchemy_async_serialization_plugin_marking_fields import app with TestClient(app) as client: assert client.post("/", json=data[0]).json() == data def test_sqlalchemy_sync_serialization_plugin_marking_fields(data: dict[str, Any]) -> None: from docs.examples.contrib.sqlalchemy.plugins.sqlalchemy_sync_serialization_plugin_marking_fields import app with TestClient(app) as client: assert client.post("/", json=data[0]).json() == data litestar-2.16.0/tests/examples/test_contrib/test_sqlalchemy/plugins/test_tutorial_example_apps.py000066400000000000000000000046601500564371300340140ustar00rootroot00000000000000from __future__ import annotations import importlib import sys import pytest from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch from docs.examples.contrib.sqlalchemy.plugins.tutorial import ( full_app_no_plugins, full_app_with_init_plugin, full_app_with_plugin, full_app_with_serialization_plugin, full_app_with_session_di, ) from sqlalchemy.ext.asyncio import create_async_engine from litestar import Litestar from litestar.testing import TestClient pytestmark = pytest.mark.xdist_group("sqlalchemy_examples") @pytest.fixture( params=[ full_app_no_plugins, full_app_with_init_plugin, full_app_with_plugin, full_app_with_serialization_plugin, full_app_with_session_di, ] ) async def app(monkeypatch: MonkeyPatch, request: FixtureRequest) -> Litestar: app_module = importlib.reload(request.param) engine = create_async_engine("sqlite+aiosqlite://", connect_args={"check_same_thread": False}) async with engine.begin() as conn: await conn.run_sync(app_module.Base.metadata.create_all) try: monkeypatch.setattr(app_module, "create_async_engine", lambda *a, **kw: engine) except AttributeError: app_module.db_config.connection_string = None app_module.db_config.engine_instance = engine yield app_module.app @pytest.mark.skipif(sys.platform != "linux", reason="Unknown - fails on Windows and macOS, in CI only") def test_no_plugins_full_app(app: Litestar) -> None: todo = {"title": "Start writing todo list", "done": True} todo_list = [todo] with TestClient(app) as client: response = client.post("/", json=todo) assert response.status_code == 201 assert response.json() == todo response = client.post("/", json=todo) assert response.status_code == 409 response = client.get("/") assert response.status_code == 200 assert response.json() == todo_list response = client.get("/?done=false") assert response.status_code == 200 assert response.json() == [] response = client.put("/Start writing another list", json=todo) assert response.status_code == 404 updated_todo = dict(todo) updated_todo["done"] = False response = client.put("/Start writing todo list", json=updated_todo) assert response.status_code == 200 assert response.json() == updated_todo litestar-2.16.0/tests/examples/test_contrib/test_sqlalchemy/test_sqlalchemy_examples.py000066400000000000000000000024521500564371300317670ustar00rootroot00000000000000from pathlib import Path import pytest from pytest import MonkeyPatch from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.pool import NullPool from litestar.plugins.sqlalchemy import AsyncSessionConfig, SQLAlchemyAsyncConfig from litestar.testing import TestClient pytestmark = pytest.mark.xdist_group("sqlalchemy_examples") async def test_sqlalchemy_declarative_models(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: engine = create_async_engine("sqlite+aiosqlite:///test.sqlite", poolclass=NullPool) session_config = AsyncSessionConfig(expire_on_commit=False) sqlalchemy_config = SQLAlchemyAsyncConfig( session_config=session_config, create_all=True, engine_instance=engine, ) # Create 'async_session' dependency. from docs.examples.contrib.sqlalchemy import sqlalchemy_declarative_models monkeypatch.setattr(sqlalchemy_declarative_models, "sqlalchemy_config", sqlalchemy_config) async with engine.begin() as connection: await connection.run_sync(sqlalchemy_declarative_models.Author.metadata.create_all) await connection.commit() with TestClient(sqlalchemy_declarative_models.app) as client: response = client.get("/authors") assert response.status_code == 200 assert len(response.json()) > 0 litestar-2.16.0/tests/examples/test_data_transfer_objects/000077500000000000000000000000001500564371300240005ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_data_transfer_objects/__init__.py000066400000000000000000000000001500564371300260770ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_data_transfer_objects/conftest.py000066400000000000000000000002571500564371300262030ustar00rootroot00000000000000from __future__ import annotations import pytest @pytest.fixture def user_data() -> dict: return {"name": "Mr Sunglass", "email": "mr.sunglass@example.com", "age": 30} litestar-2.16.0/tests/examples/test_data_transfer_objects/test_defining_dtos_on_layers.py000066400000000000000000000041131500564371300322770ustar00rootroot00000000000000from __future__ import annotations from unittest.mock import ANY from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT from litestar.testing import TestClient def test_create_user(user_data: dict) -> None: from docs.examples.data_transfer_objects.defining_dtos_on_layers import app with TestClient(app=app) as client: response = client.post("/", json=user_data) assert response.status_code == HTTP_201_CREATED assert response.json() == {"id": ANY, "name": "Mr Sunglass", "email": "mr.sunglass@example.com", "age": 30} def test_get_users() -> None: from docs.examples.data_transfer_objects.defining_dtos_on_layers import app with TestClient(app=app) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.json() == [{"id": ANY, "name": "Mr Sunglass", "email": "mr.sunglass@example.com", "age": 30}] def test_get_user() -> None: from docs.examples.data_transfer_objects.defining_dtos_on_layers import app with TestClient(app=app) as client: response = client.get("/a3cad591-5b01-4341-ae8f-94f78f790674") assert response.status_code == HTTP_200_OK assert response.json() == { "id": "a3cad591-5b01-4341-ae8f-94f78f790674", "name": "Mr Sunglass", "email": "mr.sunglass@example.com", "age": 30, } def test_update_user(user_data: dict) -> None: from docs.examples.data_transfer_objects.defining_dtos_on_layers import app with TestClient(app=app) as client: response = client.put("/a3cad591-5b01-4341-ae8f-94f78f790674", json=user_data) assert response.status_code == HTTP_200_OK assert response.json() == {"id": ANY, "name": "Mr Sunglass", "email": "mr.sunglass@example.com", "age": 30} def test_delete_user() -> None: from docs.examples.data_transfer_objects.defining_dtos_on_layers import app with TestClient(app=app) as client: response = client.delete("/a3cad591-5b01-4341-ae8f-94f78f790674") assert response.status_code == HTTP_204_NO_CONTENT assert response.content == b"" litestar-2.16.0/tests/examples/test_data_transfer_objects/test_factory/000077500000000000000000000000001500564371300265065ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_data_transfer_objects/test_factory/__init__.py000066400000000000000000000000001500564371300306050ustar00rootroot00000000000000test_dto_data_problem_statement.py000066400000000000000000000010241500564371300354200ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_data_transfer_objects/test_factoryfrom unittest.mock import ANY from litestar.status_codes import HTTP_201_CREATED from litestar.testing.client import TestClient def test_create_user(user_data: dict) -> None: from docs.examples.data_transfer_objects.factory.dto_data_problem_statement import app with TestClient(app=app) as client: response = client.post("/users", json=user_data) assert response.status_code == HTTP_201_CREATED assert response.json() == {"id": ANY, "name": "Mr Sunglass", "email": "mr.sunglass@example.com", "age": 30} litestar-2.16.0/tests/examples/test_data_transfer_objects/test_factory/test_dto_data_usage.py000066400000000000000000000006671500564371300330730ustar00rootroot00000000000000from unittest.mock import ANY from litestar.testing import TestClient def test_create_user(user_data) -> None: from docs.examples.data_transfer_objects.factory.dto_data_usage import app with TestClient(app) as client: response = client.post("/users", json=user_data) assert response.status_code == 201 assert response.json() == {"id": ANY, "name": "Mr Sunglass", "email": "mr.sunglass@example.com", "age": 30} test_leading_underscore_private.py000066400000000000000000000007301500564371300354260ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_data_transfer_objects/test_factoryfrom litestar.status_codes import HTTP_201_CREATED from litestar.testing.client import TestClient def test_create_underscored_value() -> None: from docs.examples.data_transfer_objects.factory.leading_underscore_private import app with TestClient(app=app) as client: response = client.post("/", json={"this_will": "stay", "_this_will": "go_away!"}) assert response.status_code == HTTP_201_CREATED assert response.json() == {"this_will": "stay"} test_leading_underscore_private_override.py000066400000000000000000000010031500564371300373170ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_data_transfer_objects/test_factoryfrom litestar.status_codes import HTTP_201_CREATED from litestar.testing.client import TestClient def test_create_underscored_field() -> None: from docs.examples.data_transfer_objects.factory.leading_underscore_private_override import app with TestClient(app=app) as client: response = client.post("/", json={"this_will": "stay", "_this_will": "not_go_away!"}) assert response.status_code == HTTP_201_CREATED assert response.json() == {"this_will": "stay", "_this_will": "not_go_away!"} litestar-2.16.0/tests/examples/test_data_transfer_objects/test_factory/test_type_checking.py000066400000000000000000000004541500564371300327360ustar00rootroot00000000000000import pytest from litestar.exceptions.dto_exceptions import InvalidAnnotationException def test_should_raise_error_on_route_registration() -> None: with pytest.raises(InvalidAnnotationException): from docs.examples.data_transfer_objects.factory.type_checking import app # noqa: F401 litestar-2.16.0/tests/examples/test_data_transfer_objects/test_overriding_implicit_return_dto.py000066400000000000000000000006521500564371300337230ustar00rootroot00000000000000from litestar.status_codes import HTTP_201_CREATED from litestar.testing.client import TestClient def test_create_user(user_data: dict) -> None: from docs.examples.data_transfer_objects.overriding_implicit_return_dto import app with TestClient(app=app) as client: response = client.post("/", json=user_data) assert response.status_code == HTTP_201_CREATED assert response.content == b"Mr Sunglass" litestar-2.16.0/tests/examples/test_datastructures/000077500000000000000000000000001500564371300225275ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_datastructures/__init__.py000066400000000000000000000000001500564371300246260ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_datastructures/test_secrets.py000066400000000000000000000013511500564371300256100ustar00rootroot00000000000000from docs.examples.datastructures.secrets.secret_body import post_handler from docs.examples.datastructures.secrets.secret_header import get_handler from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED from litestar.testing import create_test_client def test_secret_header() -> None: with create_test_client(get_handler) as client: r = client.get("/", headers={"x-secret": "super-secret"}) assert r.status_code == HTTP_200_OK assert r.json() == {"value": "sensitive data"} def test_secret_body() -> None: with create_test_client(post_handler) as client: r = client.post("/", json={"value": "super-secret"}) assert r.status_code == HTTP_201_CREATED assert r.json() == {"value": "******"} litestar-2.16.0/tests/examples/test_dependency_injection/000077500000000000000000000000001500564371300236325ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_dependency_injection/__init__.py000066400000000000000000000000001500564371300257310ustar00rootroot00000000000000test_dependency_default_value_no_dependency_fn.py000066400000000000000000000007141500564371300355610ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_dependency_injectionfrom docs.examples.dependency_injection import dependency_with_default from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient def test_optional_dependency_in_openapi_schema() -> None: with TestClient(app=dependency_with_default.app) as client: r = client.get("/schema/openapi.json") assert r.status_code == HTTP_200_OK assert r.json()["paths"]["/"]["get"]["parameters"][0]["name"] == "optional_dependency" test_dependency_default_value_with_dependency_fn.py000066400000000000000000000007341500564371300361220ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_dependency_injectionfrom docs.examples.dependency_injection import dependency_with_dependency_fn_and_default from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient def test_optional_dependency_not_in_openapi_schema() -> None: with TestClient(app=dependency_with_dependency_fn_and_default.app) as client: r = client.get("/schema/openapi.json") assert r.status_code == HTTP_200_OK assert r.json()["paths"]["/"]["get"].get("parameters") is None litestar-2.16.0/tests/examples/test_dependency_injection/test_dependency_skip_validation.py000066400000000000000000000005251500564371300326230ustar00rootroot00000000000000from docs.examples.dependency_injection import dependency_skip_validation from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient def test_route_skips_validation() -> None: with TestClient(app=dependency_skip_validation.app) as client: r = client.get("/") assert r.status_code == HTTP_200_OK litestar-2.16.0/tests/examples/test_dependency_injection/test_dependency_validation_error.py000066400000000000000000000006121500564371300330030ustar00rootroot00000000000000from docs.examples.dependency_injection import dependency_validation_error from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR from litestar.testing import TestClient def test_route_returns_internal_server_error() -> None: with TestClient(app=dependency_validation_error.app) as client: r = client.get("/") assert r.status_code == HTTP_500_INTERNAL_SERVER_ERROR tests_dependency_non_optional_not_provided.py000066400000000000000000000006041500564371300350200ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_dependency_injectionimport pytest from docs.examples.dependency_injection import dependency_non_optional_not_provided from litestar import Litestar from litestar.exceptions import ImproperlyConfiguredException def test_route_returns_internal_server_error() -> None: with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[dependency_non_optional_not_provided.hello_world]) litestar-2.16.0/tests/examples/test_dto/000077500000000000000000000000001500564371300202405ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_dto/__init__.py000066400000000000000000000000001500564371300223370ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_dto/test_example_apps.py000066400000000000000000000074011500564371300243310ustar00rootroot00000000000000from __future__ import annotations from litestar.testing import TestClient def test_dto_data_nested_data_create_instance_app() -> None: from docs.examples.data_transfer_objects.factory.providing_values_for_nested_data import app with TestClient(app) as client: response = client.post( "/person", json={ "name": "John", "age": 30, "address": {"street": "Fake Street"}, }, ) assert response.status_code == 201 assert response.json() == { "id": 1, "name": "John", "age": 30, "address": {"id": 2, "street": "Fake Street"}, } def test_patch_requests_app() -> None: from docs.examples.data_transfer_objects.factory.patch_requests import app with TestClient(app) as client: response = client.patch( "/person/f32ff2ce-e32f-4537-9dc0-26e7599f1380", json={"name": "Peter Pan"}, ) assert response.status_code == 200 assert response.json() == { "id": "f32ff2ce-e32f-4537-9dc0-26e7599f1380", "name": "Peter Pan", "age": 40, } def test_exclude_fields_app() -> None: from docs.examples.data_transfer_objects.factory.excluding_fields import app with TestClient(app) as client: response = client.post( "/users", json={"name": "Litestar User", "password": "xyz", "created_at": "2023-04-24T00:00:00Z"}, ) assert response.status_code == 201 assert response.json() == { "created_at": "0001-01-01T00:00:00", "address": {"city": "Anytown", "state": "NY", "zip": "12345"}, "pets": [{"name": "Fido"}, {"name": "Spot"}], "name": "Litestar User", } def test_include_fields_app() -> None: from docs.examples.data_transfer_objects.factory.included_fields import app with TestClient(app) as client: response = client.post( "/users", json={"name": "Litestar User", "password": "xyz", "created_at": "2023-04-24T00:00:00Z"}, ) assert response.status_code == 201 assert response.json() == { "address": {"street": "123 Main St"}, "pets": [{"name": "Fido"}, {"name": "Spot"}], } def test_enveloped_return_data_app() -> None: from docs.examples.data_transfer_objects.factory.enveloping_return_data import app with TestClient(app) as client: response = client.get("/users") assert response.status_code == 200 assert response.json() == { "count": 1, "data": [{"id": 1, "name": "Litestar User"}], } def test_paginated_return_data_app() -> None: from docs.examples.data_transfer_objects.factory.paginated_return_data import app with TestClient(app) as client: response = client.get("/users") assert response.status_code == 200 assert response.json() == { "page_size": 10, "total_pages": 1, "current_page": 1, "items": [{"id": 1, "name": "Litestar User"}], } def test_response_return_data_app() -> None: from docs.examples.data_transfer_objects.factory.response_return_data import app with TestClient(app) as client: response = client.get("/users") assert response.status_code == 200 assert response.json() == {"id": 1, "name": "Litestar User"} assert response.headers["X-Total-Count"] == "1" def test_unknown_fields() -> None: from docs.examples.data_transfer_objects.factory.unknown_fields import app with TestClient(app) as client: response = client.post("/users", json={"id": "1", "name": "Peter"}) assert response.status_code == 400 litestar-2.16.0/tests/examples/test_dto/test_tutorial.py000066400000000000000000000140041500564371300235130ustar00rootroot00000000000000from __future__ import annotations from litestar.testing.client import TestClient def test_initial_pattern_app(): from docs.examples.data_transfer_objects.factory.tutorial.initial_pattern import app with TestClient(app=app) as client: response = client.get("/person/peter") assert response.status_code == 200 assert response.json() == {"name": "peter", "age": 30, "email": "email_of_peter@example.com"} def test_simple_dto_exclude(): from docs.examples.data_transfer_objects.factory.tutorial.simple_dto_exclude import app with TestClient(app=app) as client: response = client.get("/person/peter") assert response.status_code == 200 assert response.json() == {"name": "peter", "age": 30} def test_nested_exclude(): from docs.examples.data_transfer_objects.factory.tutorial.nested_exclude import app with TestClient(app=app) as client: response = client.get("/person/peter") assert response.status_code == 200 assert response.json() == {"name": "peter", "age": 30, "address": {"city": "Cityville", "country": "Countryland"}} def test_nested_collection_exclude(): from docs.examples.data_transfer_objects.factory.tutorial.nested_collection_exclude import app with TestClient(app=app) as client: response = client.get("/person/peter") assert response.status_code == 200 assert response.json() == { "name": "peter", "age": 30, "address": {"city": "Cityville", "country": "Countryland"}, "children": [{"name": "Child1", "age": 10}, {"name": "Child2", "age": 8}], } def test_max_nested_depth(): from docs.examples.data_transfer_objects.factory.tutorial.max_nested_depth import app with TestClient(app=app) as client: response = client.get("/person/peter") assert response.status_code == 200 assert response.json() == { "name": "peter", "age": 30, "address": {"city": "Cityville", "country": "Countryland"}, "children": [{"name": "Child1", "age": 10, "children": []}, {"name": "Child2", "age": 8, "children": []}], } def test_explicit_field_renaming(): from docs.examples.data_transfer_objects.factory.tutorial.explicit_field_renaming import app with TestClient(app=app) as client: response = client.get("/person/peter") assert response.status_code == 200 assert response.json() == { "name": "peter", "age": 30, "location": {"city": "Cityville", "country": "Countryland"}, "children": [{"name": "Child1", "age": 10}, {"name": "Child2", "age": 8}], } def test_field_renaming_strategy(): from docs.examples.data_transfer_objects.factory.tutorial.field_renaming_strategy import app with TestClient(app=app) as client: response = client.get("/person/peter") assert response.status_code == 200 assert response.json() == { "NAME": "peter", "AGE": 30, "ADDRESS": {"CITY": "Cityville", "COUNTRY": "Countryland"}, "CHILDREN": [{"NAME": "Child1", "AGE": 10}, {"NAME": "Child2", "AGE": 8}], } def test_simple_receiving_data(): from docs.examples.data_transfer_objects.factory.tutorial.simple_receiving_data import app with TestClient(app=app) as client: response = client.post("/person", json={"name": "peter", "age": 40, "email": "email_of_peter@example.com"}) assert response.status_code == 201 assert response.json() == {"name": "peter", "age": 40} def test_read_only_fields(): from docs.examples.data_transfer_objects.factory.tutorial.read_only_fields_error import app with TestClient(app=app) as client: response = client.post("/person", json={"name": "peter", "age": 40, "email": "email_of_peter@example.com"}) assert response.status_code == 500 def test_dto_data(): from docs.examples.data_transfer_objects.factory.tutorial.dto_data import app with TestClient(app=app) as client: response = client.post("/person", json={"name": "peter", "age": 40, "email": "email_of_peter@example.com"}) assert response.status_code == 201 assert response.json() == {"id": 1, "name": "peter", "age": 40} def test_put_handler(): from docs.examples.data_transfer_objects.factory.tutorial.put_handlers import app with TestClient(app=app) as client: response = client.put("/person/1", json={"name": "peter", "age": 50, "email": "email_of_peter@example.com"}) assert response.status_code == 200 assert response.json() == {"id": 1, "name": "peter", "age": 50} def test_patch_handler(): from docs.examples.data_transfer_objects.factory.tutorial.patch_handlers import app with TestClient(app=app) as client: response = client.patch("/person/1", json={"name": "peter"}) assert response.status_code == 200 assert response.json() == {"id": 1, "name": "peter", "age": 50} def test_multiple_handlers(): from docs.examples.data_transfer_objects.factory.tutorial.multiple_handlers import app with TestClient(app=app) as client: response = client.put("/person/1", json={"name": "peter", "age": 50, "email": "email_of_peter@example.com"}) assert response.status_code == 200 with TestClient(app=app) as client: response = client.patch("/person/1", json={"name": "peter"}) assert response.status_code == 200 with TestClient(app=app) as client: response = client.post("/person", json={"name": "peter", "age": 40, "email": "email_of_peter@example.com"}) assert response.status_code == 201 def test_controller(): from docs.examples.data_transfer_objects.factory.tutorial.controller import app with TestClient(app=app) as client: response = client.put("/person/1", json={"name": "peter", "age": 50, "email": "email_of_peter@example.com"}) assert response.status_code == 200 response = client.patch("/person/1", json={"name": "peter"}) assert response.status_code == 200 response = client.post("/person", json={"name": "peter", "age": 40, "email": "email_of_peter@example.com"}) assert response.status_code == 201 litestar-2.16.0/tests/examples/test_encoding_decoding/000077500000000000000000000000001500564371300230745ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_encoding_decoding/__init__.py000066400000000000000000000000001500564371300251730ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_encoding_decoding/test_custom_type_encoding_decoding.py000066400000000000000000000007651500564371300325720ustar00rootroot00000000000000from docs.examples.encoding_decoding.custom_type_encoding_decoding import app from litestar.status_codes import HTTP_201_CREATED from litestar.testing import TestClient def test_custom_type_encoding_decoding_works() -> None: with TestClient(app) as client: response = client.post( "/asset", json={ "user": "TenantA_Somebody", "name": "Some Asset", }, ) assert response.status_code == HTTP_201_CREATED litestar-2.16.0/tests/examples/test_encoding_decoding/test_custom_type_pydantic.py000066400000000000000000000007541500564371300307610ustar00rootroot00000000000000from docs.examples.encoding_decoding.custom_type_pydantic import app from litestar.status_codes import HTTP_201_CREATED from litestar.testing import TestClient def test_custom_type_encoding_decoding_works() -> None: with TestClient(app) as client: response = client.post( "/asset", json={ "user": "TenantA_Somebody", "name": "Some Asset", }, ) assert response.status_code == HTTP_201_CREATED litestar-2.16.0/tests/examples/test_exceptions.py000066400000000000000000000026611500564371300222120ustar00rootroot00000000000000from docs.examples.exceptions import ( layered_handlers, override_default_handler, per_exception_handlers, ) from litestar.testing import TestClient def test_override_default_handler() -> None: with TestClient(app=override_default_handler.app) as client: res = client.get("/") assert res.status_code == 400 assert res.text == "an error occurred" def test_per_exception_handlers() -> None: with TestClient(app=per_exception_handlers.app) as client: res = client.get("/validation-error") assert res.status_code == 400 assert res.text.startswith("validation error:") res = client.get("/server-error") assert res.status_code == 500 assert res.text == "server error: 500: Internal Server Error" res = client.get("/value-error") assert res.status_code == 400 assert res.text == "value error: this is wrong" def test_layered_handlers() -> None: with TestClient(app=layered_handlers.app) as client: res = client.get("/") assert res.status_code == 500 assert res.json() == { "error": "server error", "path": "/", "detail": "something's gone wrong", "status_code": 500, } res = client.get("/greet") assert res.status_code == 400 assert res.json() == { "error": "validation error", "path": "/greet", } litestar-2.16.0/tests/examples/test_hello_world.py000066400000000000000000000005111500564371300223330ustar00rootroot00000000000000from docs.examples import hello_world from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient def test_hello_world_example() -> None: with TestClient(app=hello_world.app) as client: r = client.get("/") assert r.status_code == HTTP_200_OK assert r.json() == {"hello": "world"} litestar-2.16.0/tests/examples/test_lifecycle_hooks.py000066400000000000000000000034201500564371300231650ustar00rootroot00000000000000from docs.examples.lifecycle_hooks.after_request import app as after_request_app from docs.examples.lifecycle_hooks.after_response import app as after_response_app from docs.examples.lifecycle_hooks.before_request import app as before_request_app from docs.examples.lifecycle_hooks.layered_hooks import app as layered_hooks_app from litestar.testing import TestClient def test_layered_hooks() -> None: with TestClient(app=layered_hooks_app) as client: res = client.get("/") assert res.status_code == 200 assert res.text == "app after request" res = client.get("/override") assert res.status_code == 200 assert res.text == "handler after request" def test_before_request_app() -> None: with TestClient(app=before_request_app) as client: res = client.get("/", params={"name": "Luke"}) assert res.status_code == 200 assert res.json() == {"message": "Use the handler, Luke"} res = client.get("/", params={"name": "Ben"}) assert res.status_code == 200 assert res.json() == {"message": "These are not the bytes you are looking for"} def test_after_request_app() -> None: with TestClient(app=after_request_app) as client: res = client.get("/hello") assert res.status_code == 200 assert res.json() == {"message": "Hello, world"} res = client.get("/goodbye") assert res.status_code == 200 assert res.json() == {"message": "Goodbye"} def test_after_response_app() -> None: with TestClient(app=after_response_app) as client: res = client.get("/hello") assert res.status_code == 200 assert res.json() == {} res = client.get("/hello") assert res.status_code == 200 assert res.json() == {"/hello": 1} litestar-2.16.0/tests/examples/test_middleware/000077500000000000000000000000001500564371300215675ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_middleware/__init__.py000066400000000000000000000000001500564371300236660ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_middleware/test_abstract_middleware.py000066400000000000000000000021661500564371300272050ustar00rootroot00000000000000from docs.examples.middleware.base import app from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient def test_base_middleware_example_websocket() -> None: with TestClient(app).websocket_connect("/my-websocket") as ws: assert b"x-process-time" not in dict(ws.scope["headers"]) def test_exclude_by_regex() -> None: with TestClient(app) as client: response = client.get("first_path") assert response.status_code == HTTP_200_OK assert "x-process-time" not in response.headers response = client.get("second_path") assert response.status_code == HTTP_200_OK assert "x-process-time" not in response.headers def test_exclude_by_opt_key() -> None: with TestClient(app) as client: response = client.get("third_path") assert response.status_code == HTTP_200_OK assert "x-process-time" not in response.headers def test_not_excluded() -> None: with TestClient(app) as client: response = client.get("/greet") assert response.status_code == HTTP_200_OK assert "x-process-time" in response.headers litestar-2.16.0/tests/examples/test_middleware/test_call_order.py000066400000000000000000000005101500564371300253020ustar00rootroot00000000000000from docs.examples.middleware.call_order import app from litestar.testing import TestClient def test_call_order() -> None: with TestClient(app=app) as client: response = client.get("/router/controller/handler") assert response.status_code == 200 assert response.json() == [0, 1, 2, 3, 4, 5, 6, 7] litestar-2.16.0/tests/examples/test_middleware/test_logging_middleware.py000066400000000000000000000013001500564371300270150ustar00rootroot00000000000000import logging from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from pytest import LogCaptureFixture from litestar.types.callable_types import GetLogger from docs.examples.middleware.logging_middleware import app from litestar.testing import TestClient @pytest.mark.usefixtures("reset_httpx_logging") def test_logging_middleware_regular_logger(get_logger: "GetLogger", caplog: "LogCaptureFixture") -> None: with TestClient(app=app) as client, caplog.at_level(logging.INFO): client.app.get_logger = get_logger response = client.get("/", headers={"request-header": "1"}) assert response.status_code == 200 assert len(caplog.messages) == 2 litestar-2.16.0/tests/examples/test_middleware/test_rate_limit_middleware.py000066400000000000000000000007441500564371300275330ustar00rootroot00000000000000from docs.examples.middleware.rate_limit import app from litestar.status_codes import HTTP_200_OK, HTTP_429_TOO_MANY_REQUESTS from litestar.testing import TestClient def test_rate_limit_middleware_example() -> None: with TestClient(app=app) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "ok" response = client.get("/") assert response.status_code == HTTP_429_TOO_MANY_REQUESTS litestar-2.16.0/tests/examples/test_middleware/test_session_middleware.py000066400000000000000000000016551500564371300270670ustar00rootroot00000000000000from docs.examples.middleware.session.cookies_full_example import app from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT from litestar.testing import TestClient def test_session_middleware_example() -> None: with TestClient(app=app) as client: response = client.get("/session") assert response.status_code == HTTP_200_OK assert response.json() == {"has_session": False} response = client.post("/session") assert response.status_code == HTTP_201_CREATED response = client.get("/session") assert response.status_code == HTTP_200_OK assert response.json() == {"has_session": True} response = client.delete("/session") assert response.status_code == HTTP_204_NO_CONTENT response = client.get("/session") assert response.status_code == HTTP_200_OK assert response.json() == {"has_session": False} litestar-2.16.0/tests/examples/test_openapi/000077500000000000000000000000001500564371300211055ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_openapi/__init__.py000066400000000000000000000000001500564371300232040ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_openapi/test_openapi.py000066400000000000000000000032261500564371300241540ustar00rootroot00000000000000from docs.examples.openapi import customize_pydantic_model_name from litestar.testing import TestClient def test_schema_generation() -> None: with TestClient(app=customize_pydantic_model_name.app) as client: assert client.app.openapi_schema.to_schema() == { "info": {"title": "Litestar API", "version": "1.0.0"}, "openapi": "3.1.0", "servers": [{"url": "/"}], "paths": { "/id": { "get": { "summary": "RetrieveIdHandler", "operationId": "IdRetrieveIdHandler", "responses": { "200": { "description": "Request fulfilled, document follows", "headers": {}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/IdModel"}}}, } }, "deprecated": False, } } }, "components": { "schemas": { "IdModel": { "properties": {"id": {"type": "string", "format": "uuid"}}, "type": "object", "required": ["id"], "title": "IdContainer", } } }, } def test_customize_path() -> None: from docs.examples.openapi.customize_path import app with TestClient(app=app) as client: resp = client.get("/docs/openapi.json") assert resp.status_code == 200 litestar-2.16.0/tests/examples/test_openapi/test_plugins.py000066400000000000000000000102531500564371300242000ustar00rootroot00000000000000import pytest from litestar.openapi.config import OpenAPIConfig from litestar.testing import TestClient, create_test_client def test_scalar_simple() -> None: from docs.examples.openapi.plugins.scalar_simple import app with TestClient(app=app) as client: resp = client.get("/schema/scalar") assert resp.status_code == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" assert "Litestar Example" in resp.text def test_rapidoc_simple() -> None: from docs.examples.openapi.plugins.rapidoc_simple import app with TestClient(app=app) as client: resp = client.get("/schema/rapidoc") assert resp.status_code == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" assert "Litestar Example" in resp.text def test_redoc_simple() -> None: from docs.examples.openapi.plugins.redoc_simple import app with TestClient(app=app) as client: resp = client.get("/schema/redoc") assert resp.status_code == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" assert "Litestar Example" in resp.text def test_stoplights_simple() -> None: from docs.examples.openapi.plugins.stoplight_simple import app with TestClient(app=app) as client: resp = client.get("/schema/elements") assert resp.status_code == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" assert "Litestar Example" in resp.text def test_swagger_ui_simple() -> None: from docs.examples.openapi.plugins.swagger_ui_simple import app with TestClient(app=app) as client: resp = client.get("/schema/swagger") assert resp.status_code == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" assert "Litestar Example" in resp.text @pytest.mark.parametrize("path", ["/schema/openapi.yml", "/schema/openapi.yaml"]) def test_yaml_simple(path: str) -> None: from docs.examples.openapi.plugins.yaml_simple import app with TestClient(app=app) as client: resp = client.get(path) assert resp.status_code == 200 assert resp.headers["content-type"] == "application/vnd.oai.openapi" assert "Litestar Example" in resp.text def test_serving_multiple_uis() -> None: from docs.examples.openapi.plugins.serving_multiple_uis import app with TestClient(app=app) as client: resp = client.get("/schema/rapidoc") assert resp.status_code == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" assert "Litestar Example" in resp.text resp = client.get("/schema/swagger") assert resp.status_code == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" assert "Litestar Example" in resp.text def test_custom_plugin() -> None: from docs.examples.openapi.plugins.custom_plugin import ScalarRenderPlugin openapi_config = OpenAPIConfig( title="My API", description="This is the description of my API", version="0.1.0", render_plugins=[ScalarRenderPlugin()], ) with create_test_client(route_handlers=[], openapi_config=openapi_config) as client: resp = client.get("/schema/scalar") assert resp.status_code == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" assert "My API" in resp.text def test_receive_router() -> None: from docs.examples.openapi.plugins.receive_router import MyOpenAPIPlugin openapi_config = OpenAPIConfig( title="My API", description="This is the description of my API", version="0.1.0", render_plugins=[MyOpenAPIPlugin(path="/custom")], ) with create_test_client(route_handlers=[], openapi_config=openapi_config) as client: resp = client.get("/schema/custom") assert resp.status_code == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" assert "My UI of Choice" in resp.text resp = client.get("/schema/something") assert resp.status_code == 200 assert resp.headers["content-type"] == "text/plain; charset=utf-8" assert "Something" in resp.text litestar-2.16.0/tests/examples/test_pagination/000077500000000000000000000000001500564371300216035ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_pagination/__init__.py000066400000000000000000000000001500564371300237020ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_pagination/test_using_classic_pagination.py000066400000000000000000000011371500564371300302550ustar00rootroot00000000000000from docs.examples.pagination.using_classic_pagination import app from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient def test_using_classic_pagination() -> None: with TestClient(app) as client: response = client.get("/people", params={"page_size": 5, "current_page": 1}) assert response.status_code == HTTP_200_OK response_data = response.json() assert len(response_data["items"]) == 5 assert response_data["total_pages"] == 10 assert response_data["page_size"] == 5 assert response_data["current_page"] == 1 litestar-2.16.0/tests/examples/test_pagination/test_using_cursor_pagination.py000066400000000000000000000010541500564371300301470ustar00rootroot00000000000000from docs.examples.pagination.using_cursor_pagination import app from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient def test_using_cursor_pagination() -> None: with TestClient(app) as client: response = client.get("/people", params={"results_per_page": 5}) assert response.status_code == HTTP_200_OK response_data = response.json() assert len(response_data["items"]) == 5 assert response_data["results_per_page"] == 5 assert isinstance(response_data["cursor"], str) litestar-2.16.0/tests/examples/test_pagination/test_using_offset_pagination.py000066400000000000000000000011031500564371300301130ustar00rootroot00000000000000from docs.examples.pagination.using_offset_pagination import app from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient def test_using_offset_pagination() -> None: with TestClient(app) as client: response = client.get("/people", params={"limit": 5, "offset": 0}) assert response.status_code == HTTP_200_OK response_data = response.json() assert len(response_data["items"]) == 5 assert response_data["total"] == 50 assert response_data["limit"] == 5 assert response_data["offset"] == 0 litestar-2.16.0/tests/examples/test_parameters/000077500000000000000000000000001500564371300216155ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_parameters/__init__.py000066400000000000000000000000001500564371300237140ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_parameters/test_header_and_cookies_parameters.py000066400000000000000000000015341500564371300312420ustar00rootroot00000000000000from docs.examples.parameters.header_and_cookie_parameters import app from litestar.status_codes import ( HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, ) from litestar.testing import TestClient def test_header_and_cookie_parameters() -> None: with TestClient(app=app) as client: response = client.get("/users/1") assert response.status_code == HTTP_400_BAD_REQUEST client.cookies["my-cookie-param"] = "bar" response = client.get("/users/1", headers={"X-API-KEY": "foo"}) assert response.status_code == HTTP_401_UNAUTHORIZED client.cookies["my-cookie-param"] = "cookie-secret" response = client.get("/users/1", headers={"X-API-KEY": "super-secret-secret"}) assert response.status_code == HTTP_200_OK assert response.json() == {"id": 1, "name": "John Doe"} litestar-2.16.0/tests/examples/test_parameters/test_layered_parameters.py000066400000000000000000000031671500564371300271050ustar00rootroot00000000000000from typing import Any, Dict import pytest from docs.examples.parameters.layered_parameters import app from litestar.testing import TestClient @pytest.mark.parametrize( "params,status_code,expected", [ ( { "headers": {"MyHeader": "foo"}, "cookies": {"special-cookie": "bar"}, "params": {"controller_param": "11", "local_param": "foo"}, }, 200, {"controller_param": 11, "local_param": "foo", "path_param": 11, "router_param": "foo"}, ), ( { "cookies": {"special-cookie": "bar"}, "params": {"controller_param": "11", "local_param": "foo"}, }, 400, None, ), ( { "headers": {"MyHeader": "foo"}, "cookies": {"special-cookie": "bar"}, "params": {"controller_param": "11"}, }, 400, None, ), ( { "headers": {"MyHeader": "foo"}, "cookies": {"special-cookie": "bar"}, "params": {"local_param": "foo"}, }, 400, None, ), ], ) def test_layered_parameters(params: Dict[str, Any], status_code: int, expected: Dict[str, Any]) -> None: with TestClient(app=app) as client: client.cookies = params.pop("cookies") response = client.get("/router/controller/11", **params) assert response.status_code == status_code, response.json() if expected: assert response.json() == expected litestar-2.16.0/tests/examples/test_parameters/test_path_parameters.py000066400000000000000000000017631500564371300264140ustar00rootroot00000000000000from docs.examples.parameters.path_parameters_1 import app from docs.examples.parameters.path_parameters_2 import app as app_2 from docs.examples.parameters.path_parameters_3 import app as app_3 from litestar.testing import TestClient def test_path_parameters_1() -> None: with TestClient(app=app) as client: response = client.get("/user/1") assert response.status_code == 200 assert response.json() == {"id": 1, "name": "John Doe"} def test_path_parameters_2() -> None: with TestClient(app=app_2) as client: response = client.get("/orders/1667924386") assert response.status_code == 200 assert response.json() == [ {"id": 1, "customer_id": 2}, {"id": 2, "customer_id": 2}, ] def test_path_parameters_3() -> None: with TestClient(app=app_3) as client: response = client.get("/versions/1") assert response.status_code == 200 assert response.json() == {"id": 1, "specs": {"some": "value"}} litestar-2.16.0/tests/examples/test_parameters/test_query_parameters.py000066400000000000000000000045461500564371300266270ustar00rootroot00000000000000from docs.examples.parameters.query_params import app as query_params_app from docs.examples.parameters.query_params_constraints import ( app as query_params_constraints_app, ) from docs.examples.parameters.query_params_default import app as query_params_default_app from docs.examples.parameters.query_params_optional import app as query_params_optional_app from docs.examples.parameters.query_params_remap import app as query_params_remap_app from docs.examples.parameters.query_params_types import app as query_params_types_app from litestar.testing import TestClient def test_query_params() -> None: with TestClient(app=query_params_app) as client: res = client.get("/?param=hello") assert res.status_code == 200 assert res.json() == {"param": "hello"} def test_query_params_default() -> None: with TestClient(app=query_params_default_app) as client: res = client.get("/") assert res.status_code == 200 assert res.json() == {"param": "hello"} res = client.get("/?param=world") assert res.status_code == 200 assert res.json() == {"param": "world"} def test_query_params_optional() -> None: with TestClient(app=query_params_optional_app) as client: res = client.get("/") assert res.status_code == 200 assert res.json() == {"param": None} res = client.get("/?param=world") assert res.status_code == 200 assert res.json() == {"param": "world"} def test_query_params_types() -> None: with TestClient(app=query_params_types_app) as client: res = client.get("/?date=2022-11-28T13:22:06.916540&floating_number=0.1&number=42&strings=1&strings=2") assert res.status_code == 200, res.json() assert res.json() == {"datetime": "2022-11-29T13:22:06.916540", "int": 42, "float": 0.1, "list": ["1", "2"]} def test_query_params_remap() -> None: with TestClient(app=query_params_remap_app) as client: res = client.get("/?camelCase=hello") assert res.status_code == 200 assert res.json() == {"param": "hello"} def test_query_params_constraint() -> None: with TestClient(app=query_params_constraints_app) as client: res = client.get("/?param=1") assert res.status_code == 400 res = client.get("/?param=6") assert res.status_code == 200 assert res.json() == {"param": 6} litestar-2.16.0/tests/examples/test_plugins/000077500000000000000000000000001500564371300211335ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_plugins/__init__.py000066400000000000000000000000001500564371300232320ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_plugins/test_di_plugin.py000066400000000000000000000004301500564371300245130ustar00rootroot00000000000000from docs.examples.plugins.di_plugin import app from litestar.testing import TestClient def test_di_plugin_example() -> None: with TestClient(app) as client: res = client.get("/?param=hello") assert res.status_code == 200 assert res.text == "hello" litestar-2.16.0/tests/examples/test_plugins/test_example_apps.py000066400000000000000000000004411500564371300252210ustar00rootroot00000000000000from __future__ import annotations from litestar.testing import TestClient def test_dto_data_problem_statement_app() -> None: from docs.examples.plugins.init_plugin_protocol import app with TestClient(app) as client: assert client.get("/").json() == {"hello": "world"} litestar-2.16.0/tests/examples/test_plugins/test_sqlalchemy_init_plugin.py000066400000000000000000000017551500564371300273170ustar00rootroot00000000000000from __future__ import annotations import pytest from _pytest.monkeypatch import MonkeyPatch from litestar.testing import TestClient pytestmark = pytest.mark.xdist_group("sqlalchemy_examples") def test_sync_app(monkeypatch: MonkeyPatch) -> None: from docs.examples.plugins.sqlalchemy_init_plugin import sqlalchemy_sync monkeypatch.setattr(sqlalchemy_sync.sqlalchemy_config, "connection_string", "sqlite://") with TestClient(app=sqlalchemy_sync.app) as client: res = client.get("/sqlalchemy-app") assert res.status_code == 200 assert res.text == "1 2" def test_async_app(monkeypatch: MonkeyPatch) -> None: from docs.examples.plugins.sqlalchemy_init_plugin import sqlalchemy_async monkeypatch.setattr(sqlalchemy_async.sqlalchemy_config, "connection_string", "sqlite+aiosqlite://") with TestClient(app=sqlalchemy_async.app) as client: res = client.get("/sqlalchemy-app") assert res.status_code == 200 assert res.text == "1 2" litestar-2.16.0/tests/examples/test_request_data.py000066400000000000000000000142221500564371300225060ustar00rootroot00000000000000import io from io import BytesIO from docs.examples.request_data.custom_request import app as custom_request_class_app from docs.examples.request_data.msgpack_request import app as msgpack_app from docs.examples.request_data.request_data_1 import app from docs.examples.request_data.request_data_2 import app as app_2 from docs.examples.request_data.request_data_3 import app as app_3 from docs.examples.request_data.request_data_4 import app as app_4 from docs.examples.request_data.request_data_5 import app as app_5 from docs.examples.request_data.request_data_6 import app as app_6 from docs.examples.request_data.request_data_7 import app as app_7 from docs.examples.request_data.request_data_8 import app as app_8 from docs.examples.request_data.request_data_9 import app as app_9 from docs.examples.request_data.request_data_10 import app as app_10 from litestar.serialization import encode_msgpack from litestar.testing import TestClient def test_request_data_1() -> None: with TestClient(app=app) as client: response = client.post("/", json={"hello": "world"}) assert response.status_code == 201 assert response.json() == {"hello": "world"} def test_request_data_2() -> None: with TestClient(app=app_2) as client: response = client.post("/", json={"id": 1, "name": "John"}) assert response.status_code == 201 assert response.json() == {"id": 1, "name": "John"} def test_request_data_3() -> None: with TestClient(app=app_3) as client: response = client.post("/", json={"id": 1, "name": "John"}) assert response.status_code == 201 assert response.json() == {"id": 1, "name": "John"} schema = client.get("/schema/openapi.json") assert "Create a new user." in schema.json()["components"]["schemas"]["User"]["description"] def test_request_data_4() -> None: with TestClient(app=app_4) as client: response = client.post("/", data={"id": 1, "name": "John"}) assert response.status_code == 201 assert response.json() == {"id": 1, "name": "John"} def test_request_data_5() -> None: with TestClient(app=app_5) as client: response = client.post( "/", files={"form_input_name": ("filename", BytesIO(b"file content"))}, data={"id": 1, "name": "John"}, ) assert response.status_code == 201 assert response.json() == { "id": 1, "name": "John", "filename": "filename", "size": len(b"file content"), } def test_request_data_6() -> None: with TestClient(app=app_6) as client: response = client.post("/", files={"upload": ("hello", b"world")}) assert response.status_code == 201 assert response.text == f"hello,length: {len(b'world')}" def test_request_data_7() -> None: with TestClient(app=app_7) as client: response = client.post("/", files={"upload": ("hello", b"world")}) assert response.status_code == 201 assert response.text == f"hello,length: {len(b'world')}" def test_request_data_8() -> None: with TestClient(app=app_8) as client: response = client.post( "/", files={"cv": ("cv.odf", b"very impressive"), "diploma": ("diploma.pdf", b"the best")} ) assert response.status_code == 201 assert response.json() == {"cv": "very impressive", "diploma": "the best"} def test_request_data_9() -> None: with TestClient(app=app_9) as client: response = client.post("/", files={"hello": ("filename", b"there"), "i'm": ("another_filename", "steve")}) assert response.status_code == 201 assert response.json() == { "filename": len(b"there"), "another_filename": len(b"steve"), } def test_request_data_10() -> None: with TestClient(app=app_10) as client: # if you pass a dict to the `files` parameter without specifying a filename, it will default to `upload # so in this app it will be return the last one only... # # file (or bytes) response = client.post( "/", files={ "will default to upload": io.BytesIO(b"hello world"), "will default to upload also": io.BytesIO(b"another"), }, ) assert response.status_code == 201 assert response.json().get("upload")[0] != len(b"hello world") assert response.json().get("upload")[0] == len(b"another") # if you pass the filename explicitly, it will be used as the filename # # (filename, file (or bytes)) response = client.post("/", files={"file": ("hello.txt", io.BytesIO(b"hello"))}) assert response.status_code == 201 assert response.json().get("hello.txt")[0] == len(b"hello") # if you add the content type, it will be used as the content type # # (filename, file (or bytes), content_type) response = client.post("/", files={"file": ("hello.txt", io.BytesIO(b"hello"), "application/x-bittorrent")}) assert response.status_code == 201 assert response.json().get("hello.txt")[0] == len(b"hello") assert response.json().get("hello.txt")[1] == "application/x-bittorrent" # finally you can specify headers like so # # (filename, file (or bytes), content_type, headers) response = client.post( "/", files={"file": ("hello.txt", io.BytesIO(b"hello"), "application/x-bittorrent", {"X-Foo": "bar"})} ) assert response.status_code == 201 assert response.json().get("hello.txt")[0] == len(b"hello") assert response.json().get("hello.txt")[1] == "application/x-bittorrent" assert ("X-Foo", "bar") in response.json().get("hello.txt")[2].items() def test_msgpack_app() -> None: test_data = {"name": "Moishe Zuchmir", "age": 30, "programmer": True} with TestClient(app=msgpack_app) as client: response = client.post("/", content=encode_msgpack(test_data)) assert response.json() == test_data def test_custom_request_app() -> None: with TestClient(app=custom_request_class_app) as client: response = client.get("/kitten-name") assert response.content == b"Whiskers" litestar-2.16.0/tests/examples/test_responses/000077500000000000000000000000001500564371300214735ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_responses/__init__.py000066400000000000000000000000001500564371300235720ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_responses/test_background_tasks.py000066400000000000000000000031521500564371300264310ustar00rootroot00000000000000import logging from typing import TYPE_CHECKING import pytest from docs.examples.responses.background_tasks_1 import app as app_1 from docs.examples.responses.background_tasks_2 import app as app_2 from docs.examples.responses.background_tasks_3 import app as app_3 from docs.examples.responses.background_tasks_3 import greeted as greeted_3 from litestar.testing import TestClient if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture pytestmark = pytest.mark.usefixtures("reset_httpx_logging") def test_background_tasks_1(caplog: "LogCaptureFixture") -> None: with caplog.at_level(logging.INFO), TestClient(app=app_1) as client: name = "Jane" res = client.get("/", params={"name": name}) assert res.status_code == 200 assert res.json()["hello"] == name assert len(caplog.messages) == 1 assert name in caplog.messages[0] def test_background_tasks_2(caplog: "LogCaptureFixture") -> None: with caplog.at_level(logging.INFO), TestClient(app=app_2) as client: res = client.get("/") assert res.status_code == 200 assert "hello" in res.json() assert len(caplog.messages) == 1 assert "greeter" in caplog.messages[0] def test_background_tasks_3(caplog: "LogCaptureFixture") -> None: with caplog.at_level(logging.INFO), TestClient(app=app_3) as client: name = "Jane" res = client.get("/", params={"name": name}) assert res.status_code == 200 assert res.json()["hello"] == name assert len(caplog.messages) == 1 assert name in caplog.messages[0] assert name in greeted_3 litestar-2.16.0/tests/examples/test_responses/test_custom_responses.py000066400000000000000000000004651500564371300265240ustar00rootroot00000000000000from docs.examples.responses.custom_responses import app as app_1 from litestar.testing import TestClient def test_custom_responses() -> None: with TestClient(app=app_1) as client: res = client.get("/") assert res.status_code == 200 assert res.json() == {"foo": ["bar", "baz"]} litestar-2.16.0/tests/examples/test_responses/test_json_suffix_responses.py000066400000000000000000000007761500564371300275540ustar00rootroot00000000000000from docs.examples.responses.json_suffix_responses import app from litestar.testing import TestClient def test_json_suffix_responses() -> None: with TestClient(app=app) as client: res = client.get("/resources") assert res.status_code == 418 assert res.json() == { "title": "Server thinks it is a teapot", "type": "Server delusion", "status": 418, } assert res.headers["content-type"] == "application/vnd.example.resource+json" litestar-2.16.0/tests/examples/test_responses/test_response_cookies.py000066400000000000000000000040261500564371300264600ustar00rootroot00000000000000from unittest.mock import patch from docs.examples.responses.response_cookies_1 import app from docs.examples.responses.response_cookies_2 import app as app_2 from docs.examples.responses.response_cookies_3 import app as app_3 from docs.examples.responses.response_cookies_4 import app as app_4 from docs.examples.responses.response_cookies_5 import app as app_5 from litestar.testing import TestClient def test_response_cookies() -> None: with TestClient(app=app) as client: res = client.get("/router-path/controller-path") assert res.status_code == 200 assert res.cookies["local-cookie"] == '"local value"' assert res.cookies["controller-cookie"] == '"controller value"' assert res.cookies["router-cookie"] == '"router value"' assert res.cookies["app-cookie"] == '"app value"' def test_response_cookies_2() -> None: with TestClient(app=app_2) as client: res = client.get("/controller-path") assert res.status_code == 200 assert res.cookies["my-cookie"] == "456" def test_response_cookies_3() -> None: with TestClient(app=app_3) as client, patch("docs.examples.responses.response_cookies_3.randint") as mock_randint: mock_randint.return_value = "42" res = client.get("/resources") assert res.status_code == 200 assert res.cookies["Random-Cookie"] == "42" def test_response_cookies_4() -> None: with TestClient(app=app_4) as client, patch("docs.examples.responses.response_cookies_4.randint") as mock_randint: mock_randint.return_value = "42" res = client.get("/router-path/resources") assert res.status_code == 200 assert res.cookies["Random-Cookie"] == "42" def test_response_cookies_5() -> None: with TestClient(app=app_5) as client, patch("docs.examples.responses.response_cookies_5.randint") as mock_randint: mock_randint.return_value = "42" res = client.get("/router-path/resources") assert res.status_code == 200 assert res.cookies["Random-Cookie"] == "42" litestar-2.16.0/tests/examples/test_responses/test_response_headers.py000066400000000000000000000036171500564371300264440ustar00rootroot00000000000000from unittest.mock import patch from docs.examples.responses.response_headers_1 import app from docs.examples.responses.response_headers_2 import app as app_2 from docs.examples.responses.response_headers_3 import app as app_3 from docs.examples.responses.response_headers_4 import app as app_4 from litestar.testing import TestClient def test_response_headers() -> None: with TestClient(app=app) as client: res = client.get("/router-path/controller-path/handler-path") assert res.status_code == 200 assert res.headers["my-local-header"] == "local header" assert res.headers["controller-level-header"] == "controller header" assert res.headers["router-level-header"] == "router header" assert res.headers["app-level-header"] == "app header" def test_response_headers_2() -> None: with TestClient(app=app_2) as client, patch("docs.examples.responses.response_headers_2.randint") as mock_randint: mock_randint.return_value = "42" res = client.get("/resources") assert res.status_code == 200 assert res.headers["Random-Header"] == "42" def test_response_headers_3() -> None: with TestClient(app=app_3) as client, patch("docs.examples.responses.response_headers_3.randint") as mock_randint: mock_randint.return_value = "42" res = client.get("/router-path/resources") assert res.status_code == 200 assert res.headers["Random-Header"] == "42" assert res.json() == {"id": 1, "name": "my resource"} def test_response_headers_4() -> None: with TestClient(app=app_4) as client, patch("docs.examples.responses.response_headers_4.randint") as mock_randint: mock_randint.return_value = "42" res = client.get("/router-path/resources") assert res.status_code == 200 assert res.headers["Random-Header"] == "42" assert res.json() == {"id": 1, "name": "my resource"} litestar-2.16.0/tests/examples/test_responses/test_returning_responses.py000066400000000000000000000005761500564371300272320ustar00rootroot00000000000000from docs.examples.responses.returning_responses import app from litestar.testing import TestClient def test_returning_responses() -> None: with TestClient(app=app) as client: res = client.get("/resources") assert res.headers["MY-HEADER"] == "xyz" assert res.cookies["my-cookie"] == "abc" assert res.json() == {"id": 1, "name": "my resource"} litestar-2.16.0/tests/examples/test_responses/test_sse_responses.py000066400000000000000000000006621500564371300260030ustar00rootroot00000000000000from docs.examples.responses.sse_responses import app from httpx_sse import aconnect_sse from litestar.testing import AsyncTestClient async def test_sse_responses_example() -> None: async with AsyncTestClient(app=app) as client: async with aconnect_sse(client, "GET", f"{client.base_url}/count") as event_source: events = [sse async for sse in event_source.aiter_sse()] assert len(events) == 50 litestar-2.16.0/tests/examples/test_routing.py000066400000000000000000000017531500564371300215210ustar00rootroot00000000000000from typing import TYPE_CHECKING import pytest from docs.examples.routing import mount_custom_app, mounting_starlette_app from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient if TYPE_CHECKING: from litestar import Litestar @pytest.mark.parametrize( "app", ( mount_custom_app.app, mounting_starlette_app.app, ), ) def test_mounting_asgi_app_example(app: "Litestar") -> None: with TestClient(app) as client: response = client.get("/some/sub-path") assert response.status_code == HTTP_200_OK assert response.json() == {"forwarded_path": "/"} response = client.get("/some/sub-path/abc") assert response.status_code == HTTP_200_OK assert response.json() == {"forwarded_path": "/abc/"} response = client.get("/some/sub-path/123/another/sub-path") assert response.status_code == HTTP_200_OK assert response.json() == {"forwarded_path": "/123/another/sub-path/"} litestar-2.16.0/tests/examples/test_security/000077500000000000000000000000001500564371300213215ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_security/__init__.py000066400000000000000000000000001500564371300234200ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_security/test_jwt/000077500000000000000000000000001500564371300231645ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_security/test_jwt/__init__.py000066400000000000000000000000001500564371300252630ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_security/test_jwt/test_using_jwt_auth.py000066400000000000000000000014011500564371300276230ustar00rootroot00000000000000from uuid import uuid4 from docs.examples.security.jwt.using_jwt_auth import app from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_401_UNAUTHORIZED from litestar.testing import TestClient def test_using_jwt_auth() -> None: with TestClient(app) as client: response = client.get("/some-path") assert response.status_code == HTTP_401_UNAUTHORIZED response = client.post( "/login", json={"name": "Moishe Zuchmir", "email": "moishe@zuchmir.com", "id": str(uuid4())} ) assert response.status_code == HTTP_201_CREATED, response.json() response = client.get("/some-path", headers={"Authorization": response.headers.get("authorization")}) assert response.status_code == HTTP_200_OK litestar-2.16.0/tests/examples/test_security/test_jwt/test_using_jwt_cookie_auth.py000066400000000000000000000013151500564371300311600ustar00rootroot00000000000000from uuid import uuid4 from docs.examples.security.jwt.using_jwt_cookie_auth import app from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_401_UNAUTHORIZED from litestar.testing import TestClient def test_using_jwt_cookie_auth() -> None: with TestClient(app) as client: response = client.get("/some-path") assert response.status_code == HTTP_401_UNAUTHORIZED response = client.post( "/login", json={"name": "Moishe Zuchmir", "email": "moishe@zuchmir.com", "id": str(uuid4())} ) assert response.status_code == HTTP_201_CREATED, response.json() response = client.get("/some-path") assert response.status_code == HTTP_200_OK litestar-2.16.0/tests/examples/test_security/test_jwt/test_using_oauth2_password_bearer.py000066400000000000000000000011641500564371300324500ustar00rootroot00000000000000from uuid import uuid4 from docs.examples.security.jwt.using_oauth2_password_bearer import app from litestar.status_codes import HTTP_201_CREATED, HTTP_401_UNAUTHORIZED from litestar.testing import TestClient def test_using_oauth2_password_bearer_auth() -> None: with TestClient(app) as client: response = client.get("/some-path") assert response.status_code == HTTP_401_UNAUTHORIZED response = client.post( "/login", json={"name": "Moishe Zuchmir", "email": "moishe@zuchmir.com", "id": str(uuid4())} ) assert response.status_code == HTTP_201_CREATED, response.json() litestar-2.16.0/tests/examples/test_security/test_jwt/test_verify_issuer_audience.py000066400000000000000000000012711500564371300313310ustar00rootroot00000000000000from litestar.testing import TestClient def test_app() -> None: from docs.examples.security.jwt.verify_issuer_audience import app, jwt_auth valid_token = jwt_auth.create_token( "foo", token_audience=jwt_auth.accepted_audiences[0], token_issuer=jwt_auth.accepted_issuers[0], ) invalid_token = jwt_auth.create_token("foo") with TestClient(app) as client: response = client.get("/", headers={"Authorization": jwt_auth.format_auth_header(valid_token)}) assert response.status_code == 200 response = client.get("/", headers={"Authorization": jwt_auth.format_auth_header(invalid_token)}) assert response.status_code == 401 litestar-2.16.0/tests/examples/test_signature_namespace.py000066400000000000000000000004571500564371300240470ustar00rootroot00000000000000from docs.examples.signature_namespace.app import app from litestar.testing import TestClient def test_msgpack_app() -> None: test_data = {"a": 1, "b": "two"} with TestClient(app=app) as client: response = client.post("/", json=test_data) assert response.json() == test_data litestar-2.16.0/tests/examples/test_startup_and_shutdown.py000066400000000000000000000014631500564371300243070ustar00rootroot00000000000000from typing import TYPE_CHECKING from unittest.mock import AsyncMock, MagicMock from docs.examples import startup_and_shutdown from litestar import get from litestar.datastructures import State from litestar.testing import TestClient if TYPE_CHECKING: from pytest import MonkeyPatch class FakeAsyncEngine: dispose = AsyncMock() async def test_startup_and_shutdown_example(monkeypatch: "MonkeyPatch") -> None: monkeypatch.setattr(startup_and_shutdown, "create_async_engine", MagicMock(return_value=FakeAsyncEngine)) @get("/") def handler(state: State) -> None: assert state.engine is FakeAsyncEngine startup_and_shutdown.app.register(handler) with TestClient(app=startup_and_shutdown.app) as client: client.get("/") FakeAsyncEngine.dispose.assert_awaited_once() litestar-2.16.0/tests/examples/test_static_files.py000066400000000000000000000042071500564371300225000ustar00rootroot00000000000000import secrets from pathlib import Path import pytest from _pytest.monkeypatch import MonkeyPatch from litestar.testing import TestClient @pytest.fixture(autouse=True) def _chdir(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: monkeypatch.chdir(tmp_path) @pytest.fixture() def assets_file(tmp_path: Path) -> str: content = secrets.token_hex() assets_path = tmp_path / "assets" assets_path.mkdir() assets_path.joinpath("test.txt").write_text(content) return content def test_custom_router() -> None: from docs.examples.static_files import custom_router # noqa: F401 def test_full_example() -> None: from docs.examples.static_files import full_example with TestClient(full_example.app) as client: assert client.get("/static/hello.txt").text == "Hello, world!" def test_html_mode() -> None: from docs.examples.static_files import html_mode with TestClient(html_mode.app) as client: assert client.get("/").text == "Hello, world!" assert client.get("/index.html").text == "Hello, world!" assert client.get("/something").text == "

Not found

" def test_passing_options() -> None: from docs.examples.static_files import passing_options # noqa: F401 def test_route_reverse(capsys) -> None: from docs.examples.static_files import route_reverse # noqa: F401 assert capsys.readouterr().out.strip() == "/static/some_file.txt" def test_send_as_attachment(tmp_path: Path, assets_file: str) -> None: from docs.examples.static_files import send_as_attachment with TestClient(send_as_attachment.app) as client: res = client.get("/static/test.txt") assert res.text == assets_file assert res.headers["content-disposition"].startswith("attachment") def test_upgrade_from_static(tmp_path: Path, assets_file: str) -> None: from docs.examples.static_files import upgrade_from_static_1, upgrade_from_static_2 for app in [upgrade_from_static_1.app, upgrade_from_static_2.app]: with TestClient(app) as client: res = client.get("/static/test.txt") assert res.text == assets_file litestar-2.16.0/tests/examples/test_stores.py000066400000000000000000000066601500564371300213530ustar00rootroot00000000000000from pathlib import Path from unittest.mock import MagicMock, patch import anyio from litestar import get from litestar.stores.file import FileStore from litestar.stores.memory import MemoryStore from litestar.stores.redis import RedisStore from litestar.testing import TestClient @patch("litestar.stores.redis.Redis") async def test_configure_integrations_set_names(mock_redis: MagicMock) -> None: from docs.examples.stores.configure_integrations_set_names import app assert isinstance(app.stores.get("redis"), RedisStore) assert isinstance(app.stores.get("file"), FileStore) assert app.stores.get("file").path == Path("data") async def test_delete_expired_after_response(frozen_datetime) -> None: from docs.examples.stores.delete_expired_after_response import app, memory_store @get() async def handler() -> bytes: return (await memory_store.get("foo")) or b"" app.register(handler) await memory_store.set("foo", "bar", expires_in=1) with TestClient(app) as client: assert client.get("/").content == b"bar" frozen_datetime.shift(1) client.get("/") assert client.get("/").content == b"" async def test_delete_expired_on_startup(tmp_path) -> None: from docs.examples.stores.delete_expired_on_startup import app, file_store file_store.path = anyio.Path(tmp_path) await file_store.set("foo", "bar", expires_in=0.01) await anyio.sleep(0.01) with TestClient(app): assert not await file_store.exists("foo") async def test_get_set(capsys) -> None: from docs.examples.stores.get_set import main await main() assert capsys.readouterr().out == "None\nb'value'\n" async def test_registry() -> None: from docs.examples.stores.registry import app, memory_store, some_other_store assert app.stores.get("memory") is memory_store assert isinstance(memory_store, MemoryStore) assert isinstance(some_other_store, MemoryStore) assert some_other_store is not memory_store async def test_registry_access_integration() -> None: from docs.examples.stores.registry_access_integration import app, rate_limit_store assert app.stores.get("rate_limit") is rate_limit_store # this is a weird assertion but the easiest way to check if our example is correct assert app.middleware[0].kwargs["config"].get_store_from_app(app) is rate_limit_store @patch("litestar.stores.redis.Redis") async def test_configure_integrations(mock_redis: MagicMock) -> None: from docs.examples.stores.registry_configure_integrations import app session_store = app.middleware[0].kwargs["backend"].config.get_store_from_app(app) cache_store = app.response_cache_config.get_store_from_app(app) assert isinstance(session_store, RedisStore) assert isinstance(cache_store, FileStore) assert cache_store.path == Path("response-cache") async def test_registry_default_factory() -> None: from docs.examples.stores.registry_default_factory import app, memory_store assert app.stores.get("foo") is memory_store assert app.stores.get("bar") is memory_store @patch("litestar.stores.redis.Redis") async def test_default_factory_namespacing(mock_redis: MagicMock) -> None: from docs.examples.stores.registry_default_factory_namespacing import app, root_store foo_store = app.stores.get("foo") assert isinstance(foo_store, RedisStore) assert foo_store._redis is root_store._redis assert foo_store.namespace == "LITESTAR_foo" litestar-2.16.0/tests/examples/test_templating/000077500000000000000000000000001500564371300216165ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_templating/__init__.py000066400000000000000000000000001500564371300237150ustar00rootroot00000000000000litestar-2.16.0/tests/examples/test_templating/test_engine_instance.py000066400000000000000000000015241500564371300263620ustar00rootroot00000000000000from docs.examples.templating.engine_instance_jinja import template_config as template_config_jinja from docs.examples.templating.engine_instance_mako import template_config as template_config_mako from docs.examples.templating.engine_instance_minijinja import template_config as template_config_minijinja from jinja2 import Environment as JinjaEnvironment from mako.lookup import TemplateLookup from minijinja import Environment as MiniJinjaEnvoronment def test_engine_instance_jinja() -> None: assert isinstance(template_config_jinja.engine_instance.engine, JinjaEnvironment) def test_engine_instance_mako() -> None: assert isinstance(template_config_mako.engine_instance.engine, TemplateLookup) def test_engine_instance_minijinja() -> None: assert isinstance(template_config_minijinja.engine_instance.engine, MiniJinjaEnvoronment) litestar-2.16.0/tests/examples/test_templating/test_returning_templates.py000066400000000000000000000023151500564371300273230ustar00rootroot00000000000000import pytest from docs.examples.templating.returning_templates_jinja import app as jinja_app from docs.examples.templating.returning_templates_mako import app as mako_app from docs.examples.templating.returning_templates_minijinja import app as minijinja_app from litestar.testing import TestClient apps_with_expected_responses = [ (jinja_app, "Jinja", "Hello Jinja", "Hello Jinja using strings"), (mako_app, "Mako", "Hello Mako", "Hello Mako using strings"), (minijinja_app, "Minijinja", "Hello Minijinja", "Hello Minijinja using strings"), ] @pytest.mark.parametrize("app, app_name, file_response, string_response", apps_with_expected_responses) @pytest.mark.parametrize("template_type", ["file", "string"]) def test_returning_templates(app, app_name, file_response, string_response, template_type): with TestClient(app) as client: response = client.get(f"/{template_type}", params={"name": app_name}) if template_type == "file": assert response.text.strip() == file_response elif template_type == "string": assert response.text.strip() == string_response litestar-2.16.0/tests/examples/test_templating/test_template_functions.py000066400000000000000000000015471500564371300271410ustar00rootroot00000000000000from docs.examples.templating.template_functions_jinja import app as jinja_app from docs.examples.templating.template_functions_mako import app as mako_app from docs.examples.templating.template_functions_minijinja import app as minijinja_app from litestar.testing import TestClient def test_template_functions_jinja(): with TestClient(jinja_app) as client: response = client.get("/") assert response.text == "check_context_key: nope" def test_template_functions_mako(): with TestClient(mako_app) as client: response = client.get("/") assert response.text.strip() == "check_context_key: nope" def test_template_functions_minijinja(): with TestClient(minijinja_app) as client: response = client.get("/") assert response.text == "check_context_key: nope" litestar-2.16.0/tests/examples/test_todo_app.py000066400000000000000000000052351500564371300216360ustar00rootroot00000000000000from typing import Any import pytest from docs.examples.todo_app import full_app from docs.examples.todo_app import update as update_app from docs.examples.todo_app.create import dataclass as dataclass_create_app from docs.examples.todo_app.create import dict as dict_create_app from docs.examples.todo_app.get_list import dataclass as get_dataclass_app from docs.examples.todo_app.get_list import dict as get_dict_app from docs.examples.todo_app.get_list import ( query_param, query_param_default, query_param_validate, query_param_validate_manually, ) from msgspec import to_builtins from litestar.testing import TestClient @pytest.mark.parametrize("module", [update_app, full_app]) def test_update(module: Any) -> None: with TestClient(module.app) as client: res = client.put("/Profit", json={"title": "Profit", "done": True}) assert res.status_code == 200 assert module.TODO_LIST[2].done assert res.json() == to_builtins(module.TODO_LIST) @pytest.mark.parametrize("module", [get_dataclass_app, get_dict_app, query_param_default]) def test_get_list_dataclass(module) -> None: with TestClient(module.app) as client: res = client.get("/") assert res.status_code == 200 assert res.json() == to_builtins(module.TODO_LIST) @pytest.mark.parametrize("module", [query_param, query_param_validate_manually, query_param_validate]) def test_get_list_query_param(module) -> None: with TestClient(module.app) as client: res = client.get("/?done=1") assert res.status_code == 200 assert res.json() == to_builtins([i for i in module.TODO_LIST if i.done]) @pytest.mark.parametrize("module", [query_param_validate_manually, query_param_validate, full_app]) def test_get_list_query_param_invalid(module) -> None: with TestClient(module.app) as client: res = client.get("/?done=john") assert res.status_code == 400 def test_dict_create() -> None: with TestClient(dict_create_app.app) as client: res = client.post("/", json={"title": "foo", "done": True}) assert res.status_code == 201 assert res.json() == [{"title": "foo", "done": True}] assert [{"title": "foo", "done": True}] == dict_create_app.TODO_LIST @pytest.mark.parametrize("module", [dataclass_create_app, full_app]) def test_dataclass_create(module: Any) -> None: with TestClient(module.app) as client: res = client.post("/", json={"title": "foo", "done": True}) assert res.status_code == 201 assert res.json() == to_builtins(module.TODO_LIST) assert len(module.TODO_LIST) assert module.TODO_LIST[-1] == module.TodoItem(title="foo", done=True) litestar-2.16.0/tests/examples/test_using_session_auth.py000066400000000000000000000025521500564371300237410ustar00rootroot00000000000000from docs.examples.security.using_session_auth import app from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_401_UNAUTHORIZED from litestar.testing import TestClient def test_using_session_auth_signup_flow() -> None: with TestClient(app) as client: response = client.get("/user") assert response.status_code == HTTP_401_UNAUTHORIZED response = client.post( "/signup", json={"name": "Moishe Zuchmir", "email": "moishe@zuchmir.com", "password": "abcd12345"} ) assert response.status_code == HTTP_201_CREATED response = client.get("/user") assert response.status_code == HTTP_200_OK def test_using_session_auth_login_flow() -> None: with TestClient(app) as client: response = client.post("/login", json={"email": "ludwig@zuchmir.com", "password": "abcd12345"}) assert response.status_code == HTTP_401_UNAUTHORIZED response = client.post( "/signup", json={"name": "ludwig Zuchmir", "email": "ludwig@zuchmir.com", "password": "abcd12345"} ) assert response.status_code == HTTP_201_CREATED response = client.post("/login", json={"email": "ludwig@zuchmir.com", "password": "abcd12345"}) assert response.status_code == HTTP_201_CREATED response = client.get("/user") assert response.status_code == HTTP_200_OK litestar-2.16.0/tests/examples/test_websockets.py000066400000000000000000000027521500564371300222030ustar00rootroot00000000000000from docs.examples.websockets.custom_websocket import app as custom_websocket_class_app from docs.examples.websockets.stream_and_receive_listener import app as app_stream_and_receive_listener from docs.examples.websockets.stream_and_receive_raw import app as app_stream_and_receive_raw from litestar.testing import AsyncTestClient from litestar.testing.client.sync_client import TestClient def test_custom_websocket_class(): client = TestClient(app=custom_websocket_class_app) with client.websocket_connect("/") as ws: ws.send({"data": "I should not be in response"}) data = ws.receive() assert data["text"] == "Fixed response" async def test_websocket_listener() -> None: """Test the websocket listener.""" async with AsyncTestClient(app_stream_and_receive_listener) as client: with await client.websocket_connect("/") as ws: ws.send_text("Hello") data_1 = ws.receive_text() data_2 = ws.receive_text() assert sorted([data_1, data_2]) == sorted(["Hello", "ping"]) async def test_websocket_handler(): async with AsyncTestClient(app_stream_and_receive_raw) as client: with await client.websocket_connect("/") as ws: echo_data = {"data": "I should be in response"} ws.send_json(echo_data) assert ws.receive_json(timeout=0.5) == {"handle_receive": "start"} assert ws.receive_json(timeout=0.5) == echo_data assert ws.receive_text(timeout=0.5) litestar-2.16.0/tests/helpers.py000066400000000000000000000100771500564371300166160ustar00rootroot00000000000000from __future__ import annotations import atexit import importlib.util import inspect import logging import random import sys from contextlib import AbstractContextManager, contextmanager from pathlib import Path from typing import Any, AsyncContextManager, Awaitable, ContextManager, Generator, TypeVar, cast, overload import pytest from _pytest.logging import LogCaptureHandler, _LiveLoggingNullHandler from litestar._openapi.schema_generation import SchemaCreator from litestar._openapi.schema_generation.plugins import openapi_schema_plugins from litestar.openapi.spec import Schema from litestar.plugins import OpenAPISchemaPluginProtocol from litestar.typing import FieldDefinition T = TypeVar("T") RANDOM = random.Random(b"bA\xcd\x00\xa9$\xa7\x17\x1c\x10") # TODO: Remove when dropping 3.9 if sys.version_info < (3, 9): def randbytes(n: int) -> bytes: return RANDOM.getrandbits(8 * n).to_bytes(n, "little") else: randbytes = RANDOM.randbytes if sys.version_info >= (3, 12): getHandlerByName = logging.getHandlerByName else: from logging import _handlers # type: ignore[attr-defined] def getHandlerByName(name: str) -> Any: return _handlers.get(name) @overload async def maybe_async(obj: Awaitable[T]) -> T: ... @overload async def maybe_async(obj: T) -> T: ... async def maybe_async(obj: Awaitable[T] | T) -> T: return await obj if inspect.isawaitable(obj) else obj # pyright: ignore class _AsyncContextManagerWrapper(AsyncContextManager): def __init__(self, cm: AbstractContextManager): self.cm = cm async def __aenter__(self) -> Any: return self.cm.__enter__() async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Any: return self.cm.__exit__(exc_type, exc_val, exc_tb) def maybe_async_cm(obj: ContextManager[T] | AsyncContextManager[T]) -> AsyncContextManager[T]: if isinstance(obj, AbstractContextManager): return cast(AsyncContextManager[T], _AsyncContextManagerWrapper(obj)) return obj def get_schema_for_field_definition( field_definition: FieldDefinition, *, plugins: list[OpenAPISchemaPluginProtocol] | None = None ) -> Schema: plugins = [*openapi_schema_plugins, *(plugins or [])] creator = SchemaCreator(plugins=plugins) result = creator.for_field_definition(field_definition) if isinstance(result, Schema): return result return creator.schema_registry.from_reference(result).schema @contextmanager def cleanup_logging_impl() -> Generator: # Reset root logger (`logging` module) std_root_logger: logging.Logger = logging.getLogger() for std_handler in std_root_logger.handlers: # Don't interfere with PyTest handler config if not isinstance(std_handler, (_LiveLoggingNullHandler, LogCaptureHandler)): std_root_logger.removeHandler(std_handler) picologging = pytest.importorskip("picologging") # Reset root logger (`picologging` module) pico_root_logger: picologging.Logger = picologging.getLogger() # type: ignore[name-defined,unused-ignore] # pyright: ignore[reportPrivateUsage,reportGeneralTypeIssues,reportAssignmentType,reportInvalidTypeForm] for pico_handler in pico_root_logger.handlers: pico_root_logger.removeHandler(pico_handler) yield # Stop queue_listener listener (mandatory for the 'logging' module with Python 3.12, # else the test suite would hang on at the end of the tests and some tests would fail) queue_listener_handler = getHandlerByName("queue_listener") if queue_listener_handler and hasattr(queue_listener_handler, "listener"): atexit.unregister(queue_listener_handler.listener.stop) queue_listener_handler.listener.stop() queue_listener_handler.close() del queue_listener_handler def not_none(val: T | None) -> T: assert val is not None return val def purge_module(module_names: list[str], path: str | Path) -> None: for name in module_names: if name in sys.modules: del sys.modules[name] Path(importlib.util.cache_from_source(path)).unlink(missing_ok=True) # type: ignore[arg-type] litestar-2.16.0/tests/models.py000066400000000000000000000026551500564371300164420ustar00rootroot00000000000000import dataclasses from enum import Enum from typing import Dict, List, Optional from uuid import UUID import msgspec from polyfactory.factories import DataclassFactory from typing_extensions import NotRequired, Required, TypedDict class Species(str, Enum): DOG = "Dog" CAT = "Cat" MONKEY = "Monkey" PIG = "Pig" @dataclasses.dataclass class DataclassPet: name: str age: float species: Species = Species.MONKEY @dataclasses.dataclass class DataclassPerson: first_name: str last_name: str id: str optional: Optional[str] complex: Dict[str, List[Dict[str, str]]] pets: Optional[List[DataclassPet]] = None class TypedDictPerson(TypedDict): first_name: Required[str] last_name: Required[str] id: Required[str] optional: NotRequired[Optional[str]] complex: Required[Dict[str, List[Dict[str, str]]]] pets: NotRequired[Optional[List[DataclassPet]]] class MsgSpecStructPerson(msgspec.Struct): first_name: str last_name: str id: str optional: Optional[str] complex: Dict[str, List[Dict[str, str]]] pets: Optional[List[DataclassPet]] @dataclasses.dataclass class User: name: str id: UUID class UserFactory(DataclassFactory[User]): __model__ = User class DataclassPersonFactory(DataclassFactory[DataclassPerson]): __model__ = DataclassPerson class DataclassPetFactory(DataclassFactory[DataclassPet]): __model__ = DataclassPet litestar-2.16.0/tests/unit/000077500000000000000000000000001500564371300155545ustar00rootroot00000000000000litestar-2.16.0/tests/unit/__init__.py000066400000000000000000000000001500564371300176530ustar00rootroot00000000000000litestar-2.16.0/tests/unit/conftest.py000066400000000000000000000012201500564371300177460ustar00rootroot00000000000000from typing import Generator, cast import pytest from _pytest.fixtures import FixtureRequest from litestar.dto import AbstractDTO from litestar.dto._backend import DTOBackend @pytest.fixture(autouse=True) def reset_cached_dto_backends() -> Generator[None, None, None]: DTOBackend._seen_model_names = set() AbstractDTO._dto_backends = {} yield DTOBackend._seen_model_names = set() AbstractDTO._dto_backends = {} @pytest.fixture(params=[pytest.param(True, id="experimental_backend"), pytest.param(False, id="default_backend")]) def use_experimental_dto_backend(request: FixtureRequest) -> bool: return cast(bool, request.param) litestar-2.16.0/tests/unit/piccolo_conf.py000066400000000000000000000004001500564371300205550ustar00rootroot00000000000000from piccolo.conf.apps import AppRegistry from piccolo.engine import SQLiteEngine DB = SQLiteEngine(path=":memory:", check_same_thread=False) APP_REGISTRY = AppRegistry( apps=[ "tests.unit.test_contrib.test_piccolo_orm.piccolo_app", ], ) litestar-2.16.0/tests/unit/test_app.py000066400000000000000000000370651500564371300177600ustar00rootroot00000000000000# ruff: noqa: UP006 from __future__ import annotations import inspect import logging from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from dataclasses import fields from typing import TYPE_CHECKING, Any, Callable, List, Tuple from unittest.mock import MagicMock, Mock, PropertyMock import pytest from click import Group from pytest import MonkeyPatch from litestar import Litestar, MediaType, Request, Response, get from litestar._asgi.asgi_router import ASGIRouter from litestar.config.app import AppConfig, ExperimentalFeatures from litestar.config.response_cache import ResponseCacheConfig from litestar.contrib.sqlalchemy.plugins import SQLAlchemySerializationPlugin from litestar.datastructures import MutableScopeHeaders, State from litestar.events.emitter import SimpleEventEmitter from litestar.exceptions import ( ImproperlyConfiguredException, InternalServerException, LitestarWarning, NotFoundException, ) from litestar.logging.config import LoggingConfig from litestar.plugins import CLIPluginProtocol from litestar.router import Router from litestar.status_codes import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR from litestar.testing import TestClient, create_test_client if TYPE_CHECKING: from typing import Dict from litestar.types import Message, Scope @pytest.fixture() def app_config_object() -> AppConfig: return AppConfig( after_exception=[], after_request=None, after_response=None, allowed_hosts=[], before_request=None, before_send=[], response_cache_config=ResponseCacheConfig(), cache_control=None, compression_config=None, cors_config=None, csrf_config=None, debug=False, dependencies={}, etag=None, event_emitter_backend=SimpleEventEmitter, exception_handlers={}, guards=[], listeners=[], logging_config=None, middleware=[], multipart_form_part_limit=1000, on_shutdown=[], on_startup=[], openapi_config=None, opt={}, parameters={}, plugins=[], request_class=None, response_class=None, response_cookies=[], response_headers=[], route_handlers=[], security=[], static_files_config=[], tags=[], template_config=None, websocket_class=None, ) def test_access_openapi_schema_raises_if_not_configured() -> None: """Test that accessing the openapi schema raises if not configured.""" app = Litestar(openapi_config=None) with pytest.raises(ImproperlyConfiguredException): app.openapi_schema def test_set_debug_updates_logging_level() -> None: app = Litestar() assert app.logger is not None assert app.logger.level == logging.INFO # type: ignore[attr-defined] app.debug = True assert app.logger.level == logging.DEBUG # type: ignore[attr-defined] app.debug = False assert app.logger.level == logging.INFO # type: ignore[attr-defined] @pytest.mark.parametrize("env_name,app_attr", [("LITESTAR_DEBUG", "debug"), ("LITESTAR_PDB", "pdb_on_exception")]) @pytest.mark.parametrize( "env_value,app_value,expected_value", [ (None, None, False), (None, False, False), (None, True, True), ("0", None, False), ("0", False, False), ("0", True, True), ("1", None, True), ("1", False, False), ("1", True, True), ], ) @pytest.mark.filterwarnings("ignore::litestar.utils.warnings.LitestarWarning:") def test_set_env_flags( monkeypatch: MonkeyPatch, env_value: str | None, app_value: bool | None, expected_value: bool, env_name: str, app_attr: str, ) -> None: if env_value is not None: monkeypatch.setenv(env_name, env_value) else: monkeypatch.delenv(env_name, raising=False) app = Litestar(**{app_attr: app_value}) # type: ignore[arg-type] assert getattr(app, app_attr) is expected_value def test_warn_pdb_on_exception() -> None: with pytest.warns(LitestarWarning, match="Debugger"): Litestar(pdb_on_exception=True) def test_app_params_defined_on_app_config_object() -> None: """Ensures that all parameters to the `Litestar` constructor are present on the `AppConfig` object.""" litestar_signature = inspect.signature(Litestar) app_config_fields = {f.name for f in fields(AppConfig)} for name in litestar_signature.parameters: if name in {"on_app_init", "initial_state", "_preferred_validation_backend"}: continue assert name in app_config_fields # ensure there are not fields defined on AppConfig that aren't in the Litestar signature assert not (app_config_fields - set(litestar_signature.parameters.keys())) def test_app_config_object_used(app_config_object: AppConfig, monkeypatch: pytest.MonkeyPatch) -> None: """Ensure that the properties on the `AppConfig` object are accessed within the `Litestar` constructor. In the test we replace every field on the `AppConfig` type with a property mock so that we can check that it has at least been accessed. It doesn't actually check that we do the right thing with it, but is a guard against the case of adding a parameter to the `Litestar` signature and to the `AppConfig` object, and using the value from the parameter downstream from construction of the `AppConfig` object. """ # replace each field on the `AppConfig` object with a `PropertyMock`, this allows us to assert that the properties # have been accessed during app instantiation. property_mocks: List[Tuple[str, Mock]] = [] for field in fields(AppConfig): property_mock = PropertyMock() property_mocks.append((field.name, property_mock)) monkeypatch.setattr(type(app_config_object), field.name, property_mock, raising=False) # Things that we don't actually need to call for this test monkeypatch.setattr(Litestar, "register", MagicMock()) monkeypatch.setattr(Litestar, "_create_asgi_handler", MagicMock()) monkeypatch.setattr(Router, "__init__", MagicMock()) monkeypatch.setattr(ASGIRouter, "__init__", MagicMock(return_value=None)) # instantiates the app with an `on_app_config` that returns our patched `AppConfig` object. Litestar(on_app_init=[MagicMock(return_value=app_config_object)]) # this ensures that each of the properties of the `AppConfig` object have been accessed within `Litestar.__init__()` for name, mock in property_mocks: assert mock.called, f"expected {name} to be called" def test_app_debug_create_logger() -> None: app = Litestar([], debug=True) assert app.logging_config assert app.logging_config.loggers["litestar"]["level"] == "DEBUG" # type: ignore[attr-defined] def test_app_debug_explicitly_disable_logging() -> None: app = Litestar([], logging_config=None) assert not app.logging_config def test_app_debug_update_logging_config() -> None: logging_config = LoggingConfig() app = Litestar([], logging_config=logging_config, debug=True) assert app.logging_config is logging_config assert app.logging_config.loggers["litestar"]["level"] == "DEBUG" # type: ignore[attr-defined] def test_set_state() -> None: def modify_state_in_hook(app_config: AppConfig) -> AppConfig: assert isinstance(app_config.state, State) app_config.state["c"] = "D" app_config.state["e"] = "f" return app_config app = Litestar(state=State({"a": "b", "c": "d"}), on_app_init=[modify_state_in_hook]) assert app.state._state == {"a": "b", "c": "D", "e": "f"} async def test_dont_override_initial_state(create_scope: Callable[..., Scope]) -> None: app = Litestar() scope = create_scope(headers=[], state={"foo": "bar"}) async def send(message: Message) -> None: pass async def receive() -> None: pass await app(scope, receive, send) # type: ignore[arg-type] assert scope["state"].get("foo") == "bar" def test_app_from_config(app_config_object: AppConfig) -> None: Litestar.from_config(app_config_object) def test_before_send() -> None: @get("/test") def handler() -> Dict[str, str]: return {"key": "value"} async def before_send_hook_handler(message: Message, scope: Scope) -> None: if message["type"] == "http.response.start": headers = MutableScopeHeaders(message) headers.add("My Header", Litestar.from_scope(scope).state.message) def on_startup(app: Litestar) -> None: app.state.message = "value injected during send" with create_test_client(handler, on_startup=[on_startup], before_send=[before_send_hook_handler]) as client: response = client.get("/test") assert response.status_code == HTTP_200_OK assert response.headers.get("My Header") == "value injected during send" def test_using_custom_http_exception_handler() -> None: @get("/{param:int}") def my_route_handler(param: int) -> None: ... def my_custom_handler(_: Request[Any, Any, State], __: Exception) -> Response[str]: return Response(content="custom message", media_type=MediaType.TEXT, status_code=HTTP_400_BAD_REQUEST) with create_test_client(my_route_handler, exception_handlers={NotFoundException: my_custom_handler}) as client: response = client.get("/abc") assert response.text == "custom message" assert response.status_code == HTTP_400_BAD_REQUEST def test_debug_response_created() -> None: # this will test exception causes are recorded in output # since frames include code in context we should not raise # exception directly def exception_thrower() -> float: return 1 / 0 @get("/") def my_route_handler() -> None: try: exception_thrower() except Exception as e: raise InternalServerException() from e app = Litestar(route_handlers=[my_route_handler], debug=True) client = TestClient(app=app) response = client.get("/") assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert "text/plain" in response.headers["content-type"] response = client.get("/", headers={"accept": "text/html"}) assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert "text/html" in response.headers["content-type"] assert "ZeroDivisionError" in response.text def test_handler_error_return_status_500() -> None: @get("/") def my_route_handler() -> None: raise KeyError("custom message") with create_test_client(my_route_handler) as client: response = client.get("/") assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR def test_lifespan() -> None: events: list[str] = [] counter = {"value": 0} def sync_function_without_app() -> None: events.append("sync_function_without_app") counter["value"] += 1 async def async_function_without_app() -> None: events.append("async_function_without_app") counter["value"] += 1 def sync_function_with_app(app: Litestar) -> None: events.append("sync_function_with_app") assert app is not None assert isinstance(app.state, State) counter["value"] += 1 app.state.x = True async def async_function_with_app(app: Litestar) -> None: events.append("async_function_with_app") assert app is not None assert isinstance(app.state, State) counter["value"] += 1 app.state.y = True with create_test_client( [], on_startup=[ sync_function_without_app, async_function_without_app, sync_function_with_app, async_function_with_app, ], on_shutdown=[ sync_function_without_app, async_function_without_app, sync_function_with_app, async_function_with_app, ], ) as client: assert counter["value"] == 4 assert client.app.state.x assert client.app.state.y counter["value"] = 0 assert counter["value"] == 0 assert counter["value"] == 4 assert events == [ "sync_function_without_app", "async_function_without_app", "sync_function_with_app", "async_function_with_app", "sync_function_without_app", "async_function_without_app", "sync_function_with_app", "async_function_with_app", ] def test_registering_route_handler_generates_openapi_docs() -> None: def fn() -> None: return app = Litestar(route_handlers=[]) assert app.openapi_schema app.register(get("/path1")(fn)) assert app.openapi_schema.paths is not None assert app.openapi_schema.paths.get("/path1") app.register(get("/path2")(fn)) assert app.openapi_schema.paths.get("/path1") assert app.openapi_schema.paths.get("/path2") def test_plugin_properties() -> None: class FooPlugin(CLIPluginProtocol): def on_cli_init(self, cli: Group) -> None: return app = Litestar(plugins=[FooPlugin(), SQLAlchemySerializationPlugin()]) assert app.openapi_schema_plugins == list(app.plugins.openapi) assert app.cli_plugins == list(app.plugins.cli) assert app.serialization_plugins == list(app.plugins.serialization) def test_plugin_registry() -> None: class FooPlugin(CLIPluginProtocol): def on_cli_init(self, cli: Group) -> None: return foo = FooPlugin() app = Litestar(plugins=[foo]) assert foo in app.plugins.cli def test_lifespan_context_and_shutdown_hook_execution_order() -> None: events: list[str] = [] counter = {"value": 0} @asynccontextmanager async def lifespan_context_1(app: Litestar) -> AsyncGenerator[None, None]: try: yield finally: events.append("ctx_1") counter["value"] += 1 @asynccontextmanager async def lifespan_context_2(app: Litestar) -> AsyncGenerator[None, None]: try: yield finally: events.append("ctx_2") counter["value"] += 1 async def hook_a(app: Litestar) -> None: events.append("hook_a") counter["value"] += 1 async def hook_b(app: Litestar) -> None: events.append("hook_b") counter["value"] += 1 with create_test_client( route_handlers=[], lifespan=[ lifespan_context_1, lifespan_context_2, ], on_shutdown=[hook_a, hook_b], ): assert counter["value"] == 0 assert counter["value"] == 4 assert events[0] == "ctx_2" assert events[1] == "ctx_1" assert events[2] == "hook_a" assert events[3] == "hook_b" def test_use_dto_codegen_feature_flag_warns() -> None: with pytest.warns(LitestarWarning, match="Use of redundant experimental feature flag DTO_CODEGEN"): Litestar(experimental_features=[ExperimentalFeatures.DTO_CODEGEN]) def test_use_future_feature_flag_warns() -> None: app = Litestar(experimental_features=[ExperimentalFeatures.FUTURE]) assert app.experimental_features == frozenset([ExperimentalFeatures.FUTURE]) def test_using_custom_path_parameter() -> None: @get() def my_route_handler() -> None: ... with create_test_client(my_route_handler, path="/abc") as client: response = client.get("/abc") assert response.status_code == HTTP_200_OK def test_from_scope() -> None: mock = MagicMock() @get() def handler(scope: Scope) -> None: mock(Litestar.from_scope(scope)) return app = Litestar(route_handlers=[handler]) with TestClient(app) as client: client.get("/") mock.assert_called_once_with(app) litestar-2.16.0/tests/unit/test_asgi/000077500000000000000000000000001500564371300175365ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_asgi/__init__.py000066400000000000000000000000001500564371300216350ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_asgi/test_asgi_router.py000066400000000000000000000220431500564371300234730ustar00rootroot00000000000000from __future__ import annotations from contextlib import asynccontextmanager from typing import TYPE_CHECKING, AsyncGenerator, Callable from unittest.mock import AsyncMock, MagicMock, call import anyio import pytest from pytest_mock import MockerFixture from litestar import Litestar, asgi, get from litestar._asgi.asgi_router import ASGIRouter from litestar.exceptions import ImproperlyConfiguredException, NotFoundException from litestar.testing import TestClient, create_test_client from litestar.types.empty import Empty from litestar.utils.helpers import get_exception_group from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from contextlib import AbstractAsyncContextManager from litestar.types import Receive, Scope, Send _ExceptionGroup = get_exception_group() def test_add_mount_route_disallow_path_parameter() -> None: async def handler(scope: Scope, receive: Receive, send: Send) -> None: return None with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[asgi("/mount-path", is_static=True)(handler), asgi("/mount-path/{id:str}")(handler)]) class _LifeSpanCallable: def __init__(self, should_raise: bool = False) -> None: self.called = False self.should_raise = should_raise def __call__(self) -> None: self.called = True if self.should_raise: raise RuntimeError("damn") def test_life_span_startup() -> None: life_span_callable = _LifeSpanCallable() with create_test_client([], on_startup=[life_span_callable]): assert life_span_callable.called def test_life_span_startup_error_handling() -> None: life_span_callable = _LifeSpanCallable(should_raise=True) with pytest.raises(_ExceptionGroup), create_test_client([], on_startup=[life_span_callable]): pass def test_life_span_shutdown() -> None: life_span_callable = _LifeSpanCallable() with create_test_client([], on_shutdown=[life_span_callable]): pass assert life_span_callable.called def test_life_span_shutdown_error_handling() -> None: life_span_callable = _LifeSpanCallable(should_raise=True) with pytest.raises(RuntimeError), create_test_client([], on_shutdown=[life_span_callable]): pass @pytest.fixture def startup_mock() -> AsyncMock: return AsyncMock() @pytest.fixture def shutdown_mock() -> AsyncMock: return AsyncMock() LifeSpanManager = Callable[[Litestar], "AbstractAsyncContextManager[None]"] def create_lifespan_manager(startup_mock: AsyncMock, shutdown_mock: AsyncMock) -> LifeSpanManager: @asynccontextmanager async def lifespan(app: Litestar) -> AsyncGenerator[None, None]: try: await startup_mock(app) yield finally: await shutdown_mock() return lifespan @pytest.fixture() def lifespan_manager(startup_mock: AsyncMock, shutdown_mock: AsyncMock) -> LifeSpanManager: return create_lifespan_manager(startup_mock, shutdown_mock) def test_lifespan_context_manager( lifespan_manager: LifeSpanManager, startup_mock: AsyncMock, shutdown_mock: AsyncMock ) -> None: with create_test_client(lifespan=[lifespan_manager]): assert startup_mock.call_count == 1 assert shutdown_mock.call_count == 0 assert shutdown_mock.call_count == 1 def test_lifespan_context_manager_with_hooks( lifespan_manager: LifeSpanManager, startup_mock: AsyncMock, shutdown_mock: AsyncMock ) -> None: on_startup_hook_mock = AsyncMock() on_shutdown_hook_mock = AsyncMock() async def on_startup() -> None: await on_startup_hook_mock() async def on_shutdown() -> None: await on_shutdown_hook_mock() with create_test_client(lifespan=[lifespan_manager], on_startup=[on_startup], on_shutdown=[on_shutdown]): assert startup_mock.call_count == 1 assert on_startup_hook_mock.call_count == 1 assert shutdown_mock.call_count == 0 assert on_shutdown_hook_mock.call_count == 0 assert shutdown_mock.call_count == 1 assert on_shutdown_hook_mock.call_count == 1 def test_multiple_lifespan_managers() -> None: managers: list[Callable[[Litestar], AbstractAsyncContextManager] | AbstractAsyncContextManager] = [] startup_mocks: list[AsyncMock] = [] shutdown_mocks: list[AsyncMock] = [] for _ in range(3): startup_mock = AsyncMock() shutdown_mock = AsyncMock() managers.append(create_lifespan_manager(startup_mock, shutdown_mock)) startup_mocks.append(startup_mock) shutdown_mocks.append(shutdown_mock) app = Litestar(lifespan=managers) with TestClient(app=app): for m in startup_mocks: m.assert_called_once_with(app) assert all(m.call_count == 0 for m in shutdown_mocks) assert all(m.call_count == 1 for m in startup_mocks) assert all(m.call_count == 1 for m in shutdown_mocks) @pytest.fixture() def mock_format_exc(mocker: MockerFixture) -> MagicMock: return mocker.patch("litestar._asgi.asgi_router.format_exc") async def test_lifespan_startup_failure(mock_format_exc: MagicMock) -> None: receive = AsyncMock() receive.return_value = {"type": "lifespan.startup"} send = AsyncMock() exception = ValueError("foo") mock_format_exc.return_value = str(exception) mock_on_startup = AsyncMock(side_effect=exception) async def on_startup() -> None: await mock_on_startup() router = ASGIRouter(app=Litestar(on_startup=[on_startup])) with pytest.raises(_ExceptionGroup): await router.lifespan(receive, send) assert send.call_count == 1 send.assert_called_once_with({"type": "lifespan.startup.failed", "message": mock_format_exc.return_value}) async def test_lifespan_shutdown_failure(mock_format_exc: MagicMock) -> None: receive = AsyncMock() receive.return_value = {"type": "lifespan.shutdown"} send = AsyncMock() exception = ValueError("foo") mock_format_exc.return_value = str(exception) mock_on_shutdown = AsyncMock(side_effect=exception) async def on_shutdown() -> None: await mock_on_shutdown() router = ASGIRouter(app=Litestar(on_shutdown=[on_shutdown])) with pytest.raises(ValueError): await router.lifespan(receive, send) assert send.call_count == 2 assert send.call_args_list[1][0][0] == {"type": "lifespan.shutdown.failed", "message": mock_format_exc.return_value} async def test_lifespan_context_exception_after_startup(mock_format_exc: MagicMock) -> None: receive = AsyncMock() receive.return_value = {"type": "lifespan.startup"} send = AsyncMock() mock_format_exc.return_value = "foo" async def sleep_and_raise() -> None: await anyio.sleep(0) raise RuntimeError("An error occurred") @asynccontextmanager async def lifespan(_: Litestar) -> AsyncGenerator[None, None]: async with anyio.create_task_group() as tg: tg.start_soon(sleep_and_raise) yield router = ASGIRouter(app=Litestar(lifespan=[lifespan])) with pytest.raises(_ExceptionGroup): await router.lifespan(receive, send) assert receive.call_count == 2 send.assert_has_calls( [ call({"type": "lifespan.startup.complete"}), call({"type": "lifespan.shutdown.failed", "message": mock_format_exc.return_value}), ] ) async def test_asgi_router_set_exception_handlers_in_scope_routing_error(scope: Scope) -> None: # If routing fails, the exception handlers that are set in scope should be the ones # defined on the app instance. app_exception_handlers_mock = MagicMock() handler_exception_handlers_mock = MagicMock() @get(exception_handlers={RuntimeError: handler_exception_handlers_mock}) async def handler() -> None: return None app = Litestar(route_handlers=[handler], exception_handlers={RuntimeError: app_exception_handlers_mock}) scope["path"] = "/nowhere-to-be-found" with pytest.raises(NotFoundException): await app.asgi_router(scope, AsyncMock(), AsyncMock()) state = ScopeState.from_scope(scope) assert state.exception_handlers is not Empty assert state.exception_handlers[RuntimeError] is app_exception_handlers_mock async def test_asgi_router_set_exception_handlers_in_scope_successful_routing( scope: Scope, monkeypatch: pytest.MonkeyPatch ) -> None: # if routing is successful, the exception handlers that are set in scope should be the ones # resolved from the handler. app_exception_handlers_mock = MagicMock() handler_exception_handlers_mock = MagicMock() @get(exception_handlers={TypeError: handler_exception_handlers_mock}) async def handler() -> None: return None app = Litestar(route_handlers=[handler], exception_handlers={RuntimeError: app_exception_handlers_mock}) router = app.asgi_router scope["path"] = "/" await router(scope, AsyncMock(), AsyncMock()) state = ScopeState.from_scope(scope) assert state.exception_handlers is not Empty assert state.exception_handlers[RuntimeError] is app_exception_handlers_mock assert state.exception_handlers[TypeError] is handler_exception_handlers_mock litestar-2.16.0/tests/unit/test_asgi/test_routing_trie/000077500000000000000000000000001500564371300233075ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_asgi/test_routing_trie/__init__.py000066400000000000000000000000001500564371300254060ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_asgi/test_routing_trie/test_mapping.py000066400000000000000000000056111500564371300263560ustar00rootroot00000000000000from __future__ import annotations from typing import Any, Iterator from unittest.mock import MagicMock import pytest from litestar import Litestar, get from litestar._asgi.routing_trie.mapping import build_route_middleware_stack from litestar.middleware._internal.exceptions import ExceptionHandlerMiddleware from litestar.routes import HTTPRoute def test_build_route_middleware_stack_no_middleware(monkeypatch: pytest.MonkeyPatch) -> None: # if there is no middleware for the route, then we don't need to wrap route.handle in # exception handling middleware. Exceptions can safely be caught by the outermost exception # handling middleware. @get("/") async def handler() -> None: pass app = Litestar(route_handlers=[handler], openapi_config=None) route = app.routes[0] handle_mock = MagicMock() monkeypatch.setattr(type(route), "handle", handle_mock) asgi_app = build_route_middleware_stack(app=app, route=route, route_handler=handler) assert asgi_app is handle_mock def test_build_route_middleware_stack_with_middleware(monkeypatch: pytest.MonkeyPatch) -> None: # proves that if there is middleware, the route handler is wrapped in the exception handling # middleware, before being wrapped in the middleware stack. mock_middleware = MagicMock() del mock_middleware.__iter__ @get("/", middleware=[mock_middleware]) async def handler() -> None: pass route = HTTPRoute(path="/", route_handlers=[handler]) build_route_middleware_stack(app=Litestar(), route=route, route_handler=handler) mock_middleware.assert_called_once() ((_, kw_args),) = mock_middleware.call_args_list assert isinstance(kw_args["app"], ExceptionHandlerMiddleware) def test_build_route_middleware_stack_with_starlette_middleware(monkeypatch: pytest.MonkeyPatch) -> None: # test our support for starlette's Middleware class class Middleware: """A Starlette ``Middleware`` class. See https://github.com/encode/starlette/blob/23c81da94b57701eabd43f582093442e6811f81d/starlette/middleware/__init__.py#L4-L17 """ def __init__(self, cls: Any, **options: Any) -> None: self.cls = cls self.options = options def __iter__(self) -> Iterator[Any]: as_tuple = (self.cls, self.options) return iter(as_tuple) mock_middleware = MagicMock() mock_middleware_arg = MagicMock() del mock_middleware.__iter__ @get("/", middleware=[Middleware(mock_middleware, arg=mock_middleware_arg)]) # type: ignore[list-item] async def handler() -> None: pass route = HTTPRoute(path="/", route_handlers=[handler]) build_route_middleware_stack(app=Litestar(), route=route, route_handler=handler) ((_, kw_args),) = mock_middleware.call_args_list assert isinstance(kw_args["app"], ExceptionHandlerMiddleware) assert kw_args["arg"] is mock_middleware_arg litestar-2.16.0/tests/unit/test_asgi/test_routing_trie/test_traversal.py000066400000000000000000000055721500564371300267340ustar00rootroot00000000000000from typing import Any from litestar import Router, asgi, get from litestar.response.base import ASGIResponse from litestar.status_codes import HTTP_404_NOT_FOUND from litestar.testing import create_test_client def test_parse_path_to_route_mounted_app_path_root() -> None: # test that paths are correctly dispatched to handlers when mounting an app # and other handlers to root path / @asgi("/foobar", is_mount=True) async def mounted_handler(scope: Any, receive: Any, send: Any) -> None: response = ASGIResponse(body="mounted") await response(scope, receive, send) @get("/{number:int}/foobar/") async def parametrized_handler() -> str: return "parametrized" @get("/static/foobar/") async def static_handler() -> str: return "static" with create_test_client( [ mounted_handler, parametrized_handler, static_handler, ] ) as client: response = client.get("/foobar") assert response.text == "mounted" response = client.get("/foobar/123/") assert response.text == "mounted" response = client.get("/123/foobar/") assert response.text == "parametrized" response = client.get("/static/foobar/") assert response.text == "static" response = client.get("/unknown/foobar/") assert response.status_code == HTTP_404_NOT_FOUND def test_parse_path_to_route_mounted_app_path_router() -> None: # test that paths are correctly dispatched to handlers when mounting an app # and other handlers inside subrouter @asgi("/foobar", is_mount=True) async def mounted_handler(scope: Any, receive: Any, send: Any) -> None: response = ASGIResponse(body="mounted") await response(scope, receive, send) @get("/{number:int}/foobar/") async def parametrized_handler() -> str: return "parametrized" @get("/static/foobar/") async def static_handler() -> str: return "static" sub_router = Router( path="/sub", route_handlers=[ mounted_handler, parametrized_handler, static_handler, ], ) base_router = Router(path="/base", route_handlers=[sub_router]) with create_test_client([base_router]) as client: response = client.get("/foobar") assert response.status_code == HTTP_404_NOT_FOUND response = client.get("/base/sub/foobar") assert response.text == "mounted" response = client.get("/base/sub/foobar/123/") assert response.text == "mounted" response = client.get("/base/sub/123/foobar/") assert response.text == "parametrized" response = client.get("/base/sub/static/foobar/") assert response.text == "static" response = client.get("/base/sub/unknown/foobar/") assert response.status_code == HTTP_404_NOT_FOUND litestar-2.16.0/tests/unit/test_background_tasks.py000066400000000000000000000026471500564371300225220ustar00rootroot00000000000000from typing import List from litestar import get from litestar.background_tasks import BackgroundTask, BackgroundTasks from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client async def test_background_tasks_regular_execution() -> None: values: List[int] = [] def extend_values(values_to_extend: List[int]) -> None: values.extend(values_to_extend) tasks = BackgroundTasks( [BackgroundTask(extend_values, [1, 2, 3]), BackgroundTask(extend_values, values_to_extend=[4, 5, 6])] ) @get("/", background=tasks) def handler() -> None: return None with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert values == [1, 2, 3, 4, 5, 6] async def test_background_tasks_task_group_execution() -> None: values: List[int] = [] def extend_values(values_to_extend: List[int]) -> None: values.extend(values_to_extend) tasks = BackgroundTasks( [BackgroundTask(extend_values, [1, 2, 3]), BackgroundTask(extend_values, values_to_extend=[4, 5, 6])], run_in_task_group=True, ) @get("/", background=tasks) def handler() -> None: return None with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert set(values) == {1, 2, 3, 4, 5, 6} litestar-2.16.0/tests/unit/test_channels/000077500000000000000000000000001500564371300204065ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_channels/__init__.py000066400000000000000000000000001500564371300225050ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_channels/conftest.py000066400000000000000000000045331500564371300226120ustar00rootroot00000000000000from __future__ import annotations import pytest from redis.asyncio import Redis as AsyncRedis from litestar.channels.backends.asyncpg import AsyncPgChannelsBackend from litestar.channels.backends.memory import MemoryChannelsBackend from litestar.channels.backends.psycopg import PsycoPgChannelsBackend from litestar.channels.backends.redis import RedisChannelsPubSubBackend, RedisChannelsStreamBackend def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None: """Adds a timeout marker of 30 seconds to all test items in the 'tests/unit/test_channels' module and its submodules. """ # This is an interim measure to diagnose stuck tests # Should be removed once the problem has been identified # Below are a few examples that displayed this behavior # https://github.com/litestar-org/litestar/actions/runs/5629765460/job/15255093668 # https://github.com/litestar-org/litestar/actions/runs/5647890525/job/15298927200 test_module_path = config.rootpath / "tests/unit/test_channels" for item in items: if test_module_path in item.path.parents: item.add_marker(pytest.mark.timeout(30)) @pytest.fixture() def redis_stream_backend(redis_client: AsyncRedis) -> RedisChannelsStreamBackend: return RedisChannelsStreamBackend(redis=redis_client, cap_streams_approximate=False, history=1) @pytest.fixture() def redis_stream_backend_with_history(redis_client: AsyncRedis) -> RedisChannelsStreamBackend: return RedisChannelsStreamBackend(redis=redis_client, cap_streams_approximate=False, history=10) @pytest.fixture() def redis_pub_sub_backend(redis_client: AsyncRedis) -> RedisChannelsPubSubBackend: return RedisChannelsPubSubBackend(redis=redis_client) @pytest.fixture() def memory_backend() -> MemoryChannelsBackend: return MemoryChannelsBackend() @pytest.fixture() def memory_backend_with_history() -> MemoryChannelsBackend: return MemoryChannelsBackend(history=10) @pytest.fixture() def postgres_asyncpg_backend(postgres_service: None, docker_ip: str) -> AsyncPgChannelsBackend: return AsyncPgChannelsBackend(f"postgres://postgres:super-secret@{docker_ip}:5423") @pytest.fixture() def postgres_psycopg_backend(postgres_service: None, docker_ip: str) -> PsycoPgChannelsBackend: return PsycoPgChannelsBackend(f"postgres://postgres:super-secret@{docker_ip}:5423") litestar-2.16.0/tests/unit/test_channels/test_backends.py000066400000000000000000000207401500564371300235740ustar00rootroot00000000000000from __future__ import annotations import asyncio from datetime import timedelta from typing import AsyncGenerator, cast from unittest.mock import AsyncMock, MagicMock import pytest from _pytest.fixtures import FixtureRequest from redis.asyncio.client import Redis from litestar.channels import ChannelsBackend from litestar.channels.backends.asyncpg import AsyncPgChannelsBackend from litestar.channels.backends.memory import MemoryChannelsBackend from litestar.channels.backends.psycopg import PsycoPgChannelsBackend from litestar.channels.backends.redis import RedisChannelsStreamBackend from litestar.exceptions import ImproperlyConfiguredException from litestar.utils.compat import async_next @pytest.fixture( params=[ pytest.param("redis_pub_sub_backend", id="redis:pubsub", marks=pytest.mark.xdist_group("redis")), pytest.param("redis_stream_backend", id="redis:stream", marks=pytest.mark.xdist_group("redis")), pytest.param("postgres_asyncpg_backend", id="postgres:asyncpg", marks=pytest.mark.xdist_group("postgres")), pytest.param("postgres_psycopg_backend", id="postgres:psycopg", marks=pytest.mark.xdist_group("postgres")), pytest.param("memory_backend", id="memory"), ] ) def channels_backend_instance(request: FixtureRequest) -> ChannelsBackend: return cast(ChannelsBackend, request.getfixturevalue(request.param)) @pytest.fixture( params=[ pytest.param( "redis_stream_backend_with_history", id="redis:stream+history", marks=pytest.mark.xdist_group("redis") ), pytest.param("memory_backend_with_history", id="memory+history"), ] ) def channels_backend_instance_with_history(request: FixtureRequest) -> ChannelsBackend: return cast(ChannelsBackend, request.getfixturevalue(request.param)) @pytest.fixture() async def channels_backend(channels_backend_instance: ChannelsBackend) -> AsyncGenerator[ChannelsBackend, None]: await channels_backend_instance.on_startup() yield channels_backend_instance await channels_backend_instance.on_shutdown() @pytest.fixture() async def channels_backend_with_history( channels_backend_instance_with_history: ChannelsBackend, ) -> AsyncGenerator[ChannelsBackend, None]: await channels_backend_instance_with_history.on_startup() yield channels_backend_instance_with_history await channels_backend_instance_with_history.on_shutdown() @pytest.mark.parametrize("channels", [{"foo"}, {"foo", "bar"}]) async def test_pub_sub(channels_backend: ChannelsBackend, channels: set[str]) -> None: await channels_backend.subscribe(channels) await channels_backend.publish(b"something", channels) event_generator = channels_backend.stream_events() received = set() for _ in channels: received.add(await async_next(event_generator)) assert received == {(c, b"something") for c in channels} async def test_pub_sub_unsubscribe(channels_backend: ChannelsBackend) -> None: await channels_backend.subscribe(["foo", "bar"]) await channels_backend.publish(b"something", ["foo"]) event_generator = channels_backend.stream_events() await channels_backend.unsubscribe(["foo"]) await channels_backend.publish(b"something", ["bar"]) assert await asyncio.wait_for(async_next(event_generator), timeout=0.01) == ("bar", b"something") async def test_pub_sub_no_subscriptions(channels_backend: ChannelsBackend) -> None: await channels_backend.publish(b"something", ["foo"]) event_generator = channels_backend.stream_events() with pytest.raises((asyncio.TimeoutError, TimeoutError)): await asyncio.wait_for(async_next(event_generator), timeout=0.01) @pytest.mark.flaky(reruns=5) # this should not really happen but just in case, we retry async def test_pub_sub_no_subscriptions_by_unsubscribes(channels_backend: ChannelsBackend) -> None: await channels_backend.subscribe(["foo", "bar"]) await channels_backend.publish(b"something", ["foo"]) event_generator = channels_backend.stream_events() await asyncio.wait_for(async_next(event_generator), timeout=0.01) await channels_backend.unsubscribe(["foo"]) await channels_backend.publish(b"something", ["foo"]) with pytest.raises((asyncio.TimeoutError, TimeoutError)): await asyncio.wait_for(async_next(event_generator), timeout=0.01) async def test_pub_sub_shutdown_leftover_messages(channels_backend_instance: ChannelsBackend) -> None: await channels_backend_instance.on_startup() await channels_backend_instance.publish(b"something", {"foo"}) await asyncio.wait_for(channels_backend_instance.on_shutdown(), timeout=0.1) async def test_unsubscribe_without_subscription(channels_backend: ChannelsBackend) -> None: await channels_backend.unsubscribe(["foo"]) @pytest.mark.parametrize("history_limit,expected_history_length", [(None, 10), (1, 1), (5, 5), (10, 10)]) async def test_get_history( channels_backend_with_history: ChannelsBackend, history_limit: int | None, expected_history_length: int ) -> None: messages = [str(i).encode() for i in range(100)] for message in messages: await channels_backend_with_history.publish(message, {"something"}) history = await channels_backend_with_history.get_history("something", history_limit) expected_messages = messages[-expected_history_length:] assert len(history) == expected_history_length assert history == expected_messages async def test_discards_history_entries(channels_backend_with_history: ChannelsBackend) -> None: for _ in range(20): await channels_backend_with_history.publish(b"foo", {"bar"}) assert len(await channels_backend_with_history.get_history("bar")) == 10 @pytest.mark.xdist_group("redis") async def test_redis_streams_backend_flushall(redis_stream_backend: RedisChannelsStreamBackend) -> None: await redis_stream_backend.publish(b"something", ["foo", "bar", "baz"]) result = await redis_stream_backend.flush_all() assert result == 3 @pytest.mark.flaky(reruns=5) # this should not really happen but just in case, we retry @pytest.mark.xdist_group("redis") async def test_redis_stream_backend_expires(redis_client: Redis) -> None: backend = RedisChannelsStreamBackend(redis=redis_client, stream_ttl=timedelta(milliseconds=10), history=2) await backend.publish(b"something", ["foo"]) await asyncio.sleep(0.1) await backend.publish(b"something", ["bar"]) assert not await backend._redis.xrange(backend._make_key("foo")) assert await backend._redis.xrange(backend._make_key("bar")) async def test_memory_publish_not_initialized_raises() -> None: backend = MemoryChannelsBackend() with pytest.raises(RuntimeError): await backend.publish(b"foo", ["something"]) @pytest.mark.xdist_group("postgres") async def test_asyncpg_get_history(postgres_asyncpg_backend: AsyncPgChannelsBackend) -> None: with pytest.raises(NotImplementedError): await postgres_asyncpg_backend.get_history("something") @pytest.mark.xdist_group("postgres") async def test_psycopg_get_history(postgres_psycopg_backend: PsycoPgChannelsBackend) -> None: with pytest.raises(NotImplementedError): await postgres_psycopg_backend.get_history("something") async def test_asyncpg_make_connection() -> None: make_connection = AsyncMock() backend = AsyncPgChannelsBackend(make_connection=make_connection) await backend.on_startup() make_connection.assert_awaited_once() async def test_asyncpg_no_make_conn_or_dsn_passed_raises() -> None: with pytest.raises(ImproperlyConfiguredException): AsyncPgChannelsBackend() # type: ignore[call-overload] def test_asyncpg_listener_raises_on_non_string_payload() -> None: backend = AsyncPgChannelsBackend(make_connection=AsyncMock()) with pytest.raises(RuntimeError): backend._listener(connection=MagicMock(), pid=1, payload=b"abc", channel="foo") async def test_asyncpg_backend_publish_before_startup_raises() -> None: backend = AsyncPgChannelsBackend(make_connection=AsyncMock()) with pytest.raises(RuntimeError): await backend.publish(b"foo", ["bar"]) async def test_asyncpg_backend_stream_before_startup_raises() -> None: backend = AsyncPgChannelsBackend(make_connection=AsyncMock()) with pytest.raises(RuntimeError): await asyncio.wait_for(async_next(backend.stream_events()), timeout=0.01) async def test_memory_backend_stream_before_startup_raises() -> None: backend = MemoryChannelsBackend() with pytest.raises(RuntimeError): await asyncio.wait_for(async_next(backend.stream_events()), timeout=0.01) litestar-2.16.0/tests/unit/test_channels/test_plugin.py000066400000000000000000000402051500564371300233160ustar00rootroot00000000000000from __future__ import annotations import asyncio import time from secrets import token_hex from typing import cast from unittest.mock import AsyncMock, MagicMock import pytest from _pytest.fixtures import FixtureRequest from pytest_mock import MockerFixture from litestar import Litestar, get from litestar.channels import ChannelsBackend, ChannelsPlugin from litestar.channels.backends.memory import MemoryChannelsBackend from litestar.channels.subscriber import BacklogStrategy from litestar.exceptions import ImproperlyConfiguredException, LitestarException from litestar.testing import TestClient, create_test_client from litestar.types.asgi_types import WebSocketMode from tests.unit.test_channels.util import get_from_stream @pytest.fixture( params=[ pytest.param("redis_pub_sub_backend", id="redis:pubsub", marks=pytest.mark.xdist_group("redis")), pytest.param("redis_stream_backend", id="redis:stream", marks=pytest.mark.xdist_group("redis")), pytest.param("postgres_asyncpg_backend", id="postgres:asyncpg", marks=pytest.mark.xdist_group("postgres")), pytest.param("postgres_psycopg_backend", id="postgres:psycopg", marks=pytest.mark.xdist_group("postgres")), pytest.param("memory_backend", id="memory"), ] ) def channels_backend(request: FixtureRequest) -> ChannelsBackend: return cast(ChannelsBackend, request.getfixturevalue(request.param)) @pytest.fixture( params=[ pytest.param( "redis_stream_backend_with_history", id="redis:stream+history", marks=pytest.mark.xdist_group("redis") ), pytest.param("memory_backend_with_history", id="memory+history"), ] ) def channels_backend_with_history(request: FixtureRequest) -> ChannelsBackend: return cast(ChannelsBackend, request.getfixturevalue(request.param)) def test_channels_no_channels_arbitrary_not_allowed_raises(memory_backend: MemoryChannelsBackend) -> None: with pytest.raises(ImproperlyConfiguredException): ChannelsPlugin(backend=memory_backend) def test_broadcast_not_initialized_raises(memory_backend: MemoryChannelsBackend) -> None: plugin = ChannelsPlugin(backend=memory_backend, arbitrary_channels_allowed=True) with pytest.raises(RuntimeError): plugin.publish("foo", "bar") def test_plugin_dependency(mock: MagicMock, memory_backend: MemoryChannelsBackend) -> None: @get() def handler(channels: ChannelsPlugin) -> None: mock(channels) channels_plugin = ChannelsPlugin(backend=memory_backend, arbitrary_channels_allowed=True) with create_test_client(handler, plugins=[channels_plugin]) as client: res = client.get("/") assert res.status_code == 200 assert mock.call_count == 1 assert mock.call_args[0][0] is channels_plugin def test_plugin_dependency_signature_namespace(memory_backend: MemoryChannelsBackend) -> None: channels_plugin = ChannelsPlugin(backend=memory_backend, arbitrary_channels_allowed=True) app = Litestar(plugins=[channels_plugin]) assert app.signature_namespace["ChannelsPlugin"] is ChannelsPlugin @pytest.mark.flaky(reruns=5) async def test_pub_sub_wait_published(channels_backend: ChannelsBackend) -> None: async with ChannelsPlugin(backend=channels_backend, channels=["something"]) as plugin: subscriber = await plugin.subscribe("something") await plugin.wait_published(b"foo", "something") res = await get_from_stream(subscriber, 1) assert res == [b"foo"] @pytest.mark.flaky(reruns=10) @pytest.mark.parametrize("channel", ["something", ["something"]]) async def test_pub_sub_non_blocking(channels_backend: ChannelsBackend, channel: str | list[str]) -> None: async with ChannelsPlugin(backend=channels_backend, channels=["something"]) as plugin: subscriber = await plugin.subscribe(channel) plugin.publish(b"foo", channel) await asyncio.sleep(0.1) # give the worker time to process things res = await get_from_stream(subscriber, 1) assert res == [b"foo"] @pytest.mark.flaky(reruns=10) async def test_pub_sub_run_in_background(channels_backend: ChannelsBackend, async_mock: AsyncMock) -> None: async with ChannelsPlugin(backend=channels_backend, channels=["something"]) as plugin: subscriber = await plugin.subscribe("something") async with subscriber.run_in_background(async_mock): plugin.publish(b"foo", "something") await asyncio.sleep(0.1) assert async_mock.call_count == 1 @pytest.mark.flaky(reruns=5) @pytest.mark.parametrize("socket_send_mode", ["text", "binary"]) @pytest.mark.parametrize("handler_base_path", [None, "/ws"]) def test_create_ws_route_handlers( channels_backend: ChannelsBackend, handler_base_path: str | None, socket_send_mode: WebSocketMode ) -> None: channels_plugin = ChannelsPlugin( backend=channels_backend, create_ws_route_handlers=True, channels=["something"], ws_handler_base_path=handler_base_path or "/", ws_send_mode=socket_send_mode, ) app = Litestar(plugins=[channels_plugin]) with TestClient(app) as client, client.websocket_connect(f"{handler_base_path or ''}/something") as ws: channels_plugin.publish(["foo"], "something") assert ws.receive_json(mode=socket_send_mode, timeout=2) == ["foo"] @pytest.mark.flaky(reruns=5) async def test_ws_route_handlers_receive_arbitrary_message(channels_backend: ChannelsBackend) -> None: """The websocket handlers await `WebSocket.receive()` to detect disconnection and stop the subscription. This test ensures that the subscription is only stopped in the case of receiving a `websocket.disconnect` message. """ channels_plugin = ChannelsPlugin( backend=channels_backend, create_ws_route_handlers=True, channels=["something"], ) app = Litestar(plugins=[channels_plugin]) with TestClient(app) as client, client.websocket_connect("/something") as ws: channels_plugin.publish(["foo"], "something") # send some arbitrary message ws.send("bar") # the subscription should still be alive assert ws.receive_json(timeout=2) == ["foo"] @pytest.mark.flaky(reruns=15) def test_create_ws_route_handlers_arbitrary_channels_allowed(channels_backend: ChannelsBackend) -> None: channels_plugin = ChannelsPlugin( backend=channels_backend, arbitrary_channels_allowed=True, create_ws_route_handlers=True, ws_handler_base_path="/ws", ) app = Litestar(plugins=[channels_plugin]) with TestClient(app) as client: with client.websocket_connect("/ws/foo") as ws: channels_plugin.publish("something", "foo") assert ws.receive_text(timeout=2) == "something" time.sleep(0.4) with client.websocket_connect("/ws/bar") as ws: channels_plugin.publish("something else", "bar") assert ws.receive_text(timeout=2) == "something else" @pytest.mark.parametrize("arbitrary_channels_allowed", [True, False]) @pytest.mark.parametrize("channels", ["foo", ["foo", "bar"]]) async def test_subscribe( async_mock: AsyncMock, memory_backend: MemoryChannelsBackend, channels: str | list[str], arbitrary_channels_allowed: bool, ) -> None: plugin = ChannelsPlugin( backend=memory_backend, channels=None if arbitrary_channels_allowed else ["foo", "bar"], arbitrary_channels_allowed=arbitrary_channels_allowed, ) memory_backend.subscribe = async_mock # type: ignore[method-assign] subscriber = await plugin.subscribe(channels) if isinstance(channels, str): channels = [channels] for channel in channels: assert subscriber in plugin._channels[channel] async_mock.assert_called_once_with(set(channels)) @pytest.mark.parametrize("arbitrary_channels_allowed", [True, False]) @pytest.mark.parametrize("channels", ["foo", ["foo", "bar"]]) async def test_start_subscription( async_mock: AsyncMock, memory_backend: MemoryChannelsBackend, channels: str | list[str], arbitrary_channels_allowed: bool, ) -> None: plugin = ChannelsPlugin( backend=memory_backend, channels=None if arbitrary_channels_allowed else ["foo", "bar"], arbitrary_channels_allowed=arbitrary_channels_allowed, ) memory_backend.subscribe = async_mock # type: ignore[method-assign] async with plugin.start_subscription(channels) as subscriber: if isinstance(channels, str): channels = [channels] for channel in channels: assert subscriber in plugin._channels[channel] async_mock.assert_called_once_with(set(channels)) assert subscriber not in plugin._channels.get("foo", []) assert subscriber not in plugin._channels.get("bar", []) @pytest.mark.parametrize("history", [1, 2]) @pytest.mark.parametrize("channels", [["foo"], ["foo", "bar"]]) async def test_subscribe_with_history( async_mock: AsyncMock, memory_backend_with_history: MemoryChannelsBackend, channels: list[str], history: int ) -> None: async with ChannelsPlugin(backend=memory_backend_with_history, channels=channels) as plugin: expected_messages = set() for channel in channels: messages = await _populate_channels_backend( message_count=4, backend=memory_backend_with_history, channel=channel ) expected_messages.update(messages[-history:]) subscriber = await plugin.subscribe(channels, history=history) assert set(await get_from_stream(subscriber, history * len(channels))) == expected_messages @pytest.mark.flaky(reruns=5) @pytest.mark.parametrize("history", [1, 2]) @pytest.mark.parametrize("channels", [["foo"], ["foo", "bar"]]) async def test_start_subscription_with_history( async_mock: AsyncMock, memory_backend_with_history: MemoryChannelsBackend, channels: list[str], history: int ) -> None: async with ChannelsPlugin(backend=memory_backend_with_history, channels=channels) as plugin: expected_messages = set() for channel in channels: messages = await _populate_channels_backend( message_count=4, backend=memory_backend_with_history, channel=channel ) expected_messages.update(messages[-history:]) async with plugin.start_subscription(channels, history=history) as subscriber: assert set(await get_from_stream(subscriber, history * len(channels))) == expected_messages async def test_subscribe_non_existent_channel_raises(memory_backend: MemoryChannelsBackend) -> None: plugin = ChannelsPlugin(backend=memory_backend, channels=["foo"]) with pytest.raises(LitestarException): await plugin.subscribe("bar") @pytest.mark.parametrize("unsubscribe_all", [False, True]) @pytest.mark.parametrize("channels", ["foo", ["foo", "bar"]]) async def test_unsubscribe( async_mock: AsyncMock, memory_backend: MemoryChannelsBackend, channels: str | list[str], unsubscribe_all: bool ) -> None: plugin = ChannelsPlugin(backend=memory_backend, channels=["foo", "bar"]) memory_backend.unsubscribe = async_mock # type: ignore[method-assign] subscriber_1 = await plugin.subscribe(channels=channels) subscriber_2 = await plugin.subscribe(channels=channels) await plugin.unsubscribe(subscriber_1, channels=None if unsubscribe_all else channels) if isinstance(channels, str): channels = [channels] assert async_mock.call_count == 0 for channel in channels: assert channel in plugin._channels assert subscriber_1 not in plugin._channels[channel] assert subscriber_2 in plugin._channels[channel] async def test_subscribe_after_unsubscribe(memory_backend: MemoryChannelsBackend) -> None: plugin = ChannelsPlugin(backend=memory_backend, channels=["foo"]) subscriber = await plugin.subscribe("foo") await plugin.unsubscribe(subscriber) await plugin.subscribe("foo") async def test_unsubscribe_last_subscriber_unsubscribes_backend( memory_backend: MemoryChannelsBackend, async_mock: AsyncMock ) -> None: plugin = ChannelsPlugin(backend=memory_backend, channels=["foo"]) memory_backend.unsubscribe = async_mock # type: ignore[method-assign] subscriber_1 = await plugin.subscribe(channels="foo") subscriber_2 = await plugin.subscribe(channels="foo") await plugin.unsubscribe(subscriber=subscriber_1, channels="foo") await plugin.unsubscribe(subscriber=subscriber_2, channels="foo") assert async_mock.call_count == 1 assert not plugin._channels.get("foo") async def _populate_channels_backend(*, message_count: int, channel: str, backend: ChannelsBackend) -> list[bytes]: messages = [f"{channel} - message {i}".encode() for i in range(message_count)] for message in messages: await backend.publish(message, [channel]) await backend.publish(b"some other message", [token_hex()]) return messages @pytest.mark.parametrize( "message_count,handler_send_history,expected_history_count", [ (2, -1, 2), (2, 1, 1), (2, 2, 2), (3, 2, 2), (2, 0, 0), ], ) async def test_handler_sends_history( memory_backend_with_history: MemoryChannelsBackend, message_count: int, handler_send_history: int, expected_history_count: int, mocker: MockerFixture, ) -> None: mock_socket_send = mocker.patch("litestar.connection.websocket.WebSocket.send_data") plugin = ChannelsPlugin( backend=memory_backend_with_history, arbitrary_channels_allowed=True, ws_handler_send_history=handler_send_history, create_ws_route_handlers=True, ) app = Litestar([], plugins=[plugin]) with TestClient(app) as client: await memory_backend_with_history.subscribe(["foo"]) messages = await _populate_channels_backend( message_count=message_count, channel="foo", backend=memory_backend_with_history ) with client.websocket_connect("/foo"): pass assert mock_socket_send.call_count == expected_history_count if expected_history_count: expected_messages = messages[-expected_history_count:] assert [call.kwargs.get("data") for call in mock_socket_send.call_args_list] == expected_messages @pytest.mark.parametrize("channels,expected_entry_count", [("foo", 1), (["foo", "bar"], 2)]) async def test_set_subscriber_history( channels: str | list[str], memory_backend_with_history: MemoryChannelsBackend, expected_entry_count: int ) -> None: async with ChannelsPlugin(backend=memory_backend_with_history, arbitrary_channels_allowed=True) as plugin: subscriber = await plugin.subscribe(channels) await memory_backend_with_history.publish(b"something", channels if isinstance(channels, list) else [channels]) await plugin.put_subscriber_history(subscriber, channels) assert subscriber.qsize == expected_entry_count assert await get_from_stream(subscriber, 2) == [b"something", b"something"] @pytest.mark.parametrize("backlog_strategy", ["backoff", "dropleft"]) async def test_backlog( memory_backend: MemoryChannelsBackend, backlog_strategy: BacklogStrategy, async_mock: AsyncMock ) -> None: plugin = ChannelsPlugin( backend=memory_backend, arbitrary_channels_allowed=True, subscriber_max_backlog=2, subscriber_backlog_strategy=backlog_strategy, ) messages = [b"foo", b"bar", b"baz"] async with plugin: subscriber = await plugin.subscribe(channels=["something"]) async with subscriber.run_in_background(async_mock): for message in messages: await plugin.wait_published(message, channels=["something"]) await plugin._on_shutdown() # force a flush of all buffers here expected_messages = messages[:-1] if backlog_strategy == "backoff" else messages[1:] assert async_mock.call_count == 2 assert [call.args[0] for call in async_mock.call_args_list] == expected_messages async def test_shutdown_idempotent(memory_backend: MemoryChannelsBackend) -> None: # calling shutdown repeatedly or before startup shouldn't cause any issues plugin = ChannelsPlugin(backend=memory_backend, arbitrary_channels_allowed=True) await plugin._on_shutdown() await plugin._on_startup() await plugin._on_shutdown() await plugin._on_shutdown() litestar-2.16.0/tests/unit/test_channels/test_subscriber.py000066400000000000000000000067711500564371300241750ustar00rootroot00000000000000from __future__ import annotations import asyncio from unittest.mock import AsyncMock, MagicMock import pytest from litestar.channels import Subscriber from litestar.channels.subscriber import BacklogStrategy from litestar.utils.compat import async_next from tests.unit.test_channels.util import get_from_stream async def test_subscriber_backlog_backoff() -> None: subscriber = Subscriber(plugin=MagicMock(), max_backlog=2, backlog_strategy="backoff") assert subscriber.put_nowait(b"foo") assert subscriber.put_nowait(b"bar") assert not subscriber.put_nowait(b"baz") assert subscriber.qsize == 2 assert [subscriber._queue.get_nowait(), subscriber._queue.get_nowait()] == [b"foo", b"bar"] async def test_subscriber_backlog_dropleft() -> None: subscriber = Subscriber(plugin=MagicMock(), max_backlog=2, backlog_strategy="dropleft") assert subscriber.put_nowait(b"foo") assert subscriber.put_nowait(b"bar") assert subscriber.put_nowait(b"baz") assert subscriber.qsize == 2 assert [subscriber._queue.get_nowait(), subscriber._queue.get_nowait()] == [b"bar", b"baz"] async def test_iter_events_none_breaks() -> None: subscriber = Subscriber(MagicMock()) mock_callback = MagicMock() subscriber.put_nowait(b"foo") subscriber.put_nowait(None) async def consume() -> None: async for event in subscriber.iter_events(): mock_callback(event) await asyncio.wait_for(consume(), timeout=0.1) mock_callback.assert_called_once_with(b"foo") @pytest.mark.parametrize("join", [False, True]) async def test_stop(join: bool) -> None: subscriber = Subscriber(AsyncMock()) async with subscriber.run_in_background(AsyncMock()): assert subscriber._task assert subscriber.is_running subscriber.put_nowait(b"foo") await subscriber.stop(join=join) assert subscriber._task is None async def test_stop_with_task_done() -> None: subscriber = Subscriber(AsyncMock()) async with subscriber.run_in_background(AsyncMock()): assert subscriber._task assert subscriber.is_running subscriber.put_nowait(None) await subscriber.stop(join=True) assert subscriber._task is None @pytest.mark.parametrize("join", [False, True]) async def test_stop_no_task(join: bool) -> None: subscriber = Subscriber(AsyncMock()) await subscriber.stop(join=join) async def test_qsize() -> None: subscriber = Subscriber(AsyncMock()) assert not subscriber.qsize subscriber.put_nowait(b"foo") assert subscriber.qsize == 1 await async_next(subscriber.iter_events()) assert not subscriber.qsize @pytest.mark.parametrize("backlog_strategy", ["backoff", "dropleft"]) async def test_backlog(backlog_strategy: BacklogStrategy) -> None: messages = [b"foo", b"bar", b"baz"] subscriber = Subscriber(AsyncMock(), backlog_strategy=backlog_strategy, max_backlog=2) expected_messages = messages[:-1] if backlog_strategy == "backoff" else messages[1:] for message in messages: subscriber.put_nowait(message) assert subscriber.qsize == 2 enqueued_items = await get_from_stream(subscriber, 2) assert expected_messages == enqueued_items async def tests_run_in_background_run_in_background_called_while_running_raises() -> None: subscriber = Subscriber(AsyncMock()) async with subscriber.run_in_background(AsyncMock()): with pytest.raises(RuntimeError): async with subscriber.run_in_background(AsyncMock()): pass litestar-2.16.0/tests/unit/test_channels/util.py000066400000000000000000000007011500564371300217330ustar00rootroot00000000000000from __future__ import annotations import asyncio from litestar.channels import Subscriber async def get_from_stream(subscriber: Subscriber, count: int) -> list[bytes]: async def getter() -> list[bytes]: items = [] async for item in subscriber.iter_events(): items.append(item) if len(items) == count: break return items return await asyncio.wait_for(getter(), timeout=1) litestar-2.16.0/tests/unit/test_cli/000077500000000000000000000000001500564371300173625ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_cli/__init__.py000066400000000000000000000036711500564371300215020ustar00rootroot00000000000000APP_FILE_CONTENT = """ from litestar import Litestar app = Litestar([]) """ CREATE_APP_FILE_CONTENT = """ from litestar import Litestar def create_app(): return Litestar([]) def func(): return False """ GENERIC_APP_FACTORY_FILE_CONTENT = """ from litestar import Litestar def any_name() -> Litestar: return Litestar([]) def func(): return False """ GENERIC_APP_FACTORY_FILE_CONTENT_STRING_ANNOTATION = """ from litestar import Litestar def any_name() -> "Litestar": return Litestar([]) def func(): return False """ GENERIC_APP_FACTORY_FILE_CONTENT_FUTURE_ANNOTATIONS = """ from __future__ import annotations from litestar import Litestar def any_name() -> Litestar: return Litestar([]) def func(): return False """ APP_FACTORY_FILE_CONTENT_SERVER_LIFESPAN_PLUGIN = """ from contextlib import contextmanager from typing import Generator from litestar import Litestar from litestar.config.app import AppConfig from litestar.plugins.base import CLIPlugin class StartupPrintPlugin(CLIPlugin): @contextmanager def server_lifespan(self, app: Litestar) -> Generator[None, None, None]: print("i_run_before_startup_plugin") # noqa: T201 try: yield finally: print("i_run_after_shutdown_plugin") # noqa: T201 def create_app() -> Litestar: return Litestar(route_handlers=[], plugins=[StartupPrintPlugin()]) """ APP_FILE_CONTENT_ROUTES_EXAMPLE = """ from litestar import Litestar, get from litestar.openapi import OpenAPIConfig from typing import Dict @get("/") def hello_world() -> Dict[str, str]: return {"hello": "world"} @get("/foo") def foo() -> str: return "bar" @get("/schema/all/foo/bar/schema/") def long_api() -> Dict[str, str]: return {"test": "api"} app = Litestar( openapi_config=OpenAPIConfig( title="test_app", version="0", path="/api-docs", ), route_handlers=[hello_world, foo, long_api] ) """ litestar-2.16.0/tests/unit/test_cli/conftest.py000066400000000000000000000115371500564371300215700ustar00rootroot00000000000000from __future__ import annotations import importlib.util import sys from pathlib import Path from shutil import rmtree from typing import TYPE_CHECKING, Callable, Generator, Protocol, cast import pytest from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch from click.testing import CliRunner from pytest_mock import MockerFixture from litestar.cli._utils import _path_to_dotted_path from . import ( APP_FILE_CONTENT, CREATE_APP_FILE_CONTENT, GENERIC_APP_FACTORY_FILE_CONTENT, GENERIC_APP_FACTORY_FILE_CONTENT_FUTURE_ANNOTATIONS, GENERIC_APP_FACTORY_FILE_CONTENT_STRING_ANNOTATION, ) if TYPE_CHECKING: from unittest.mock import MagicMock from litestar.cli._utils import LitestarExtensionGroup @pytest.fixture(autouse=True) def reset_litestar_app_env(monkeypatch: MonkeyPatch) -> None: monkeypatch.delenv("LITESTAR_APP", raising=False) @pytest.fixture() def root_command() -> LitestarExtensionGroup: import litestar.cli.main return cast("LitestarExtensionGroup", importlib.reload(litestar.cli.main).litestar_group) @pytest.fixture def patch_autodiscovery_paths(request: FixtureRequest) -> Callable[[list[str]], None]: def patcher(paths: list[str]) -> None: from litestar.cli._utils import AUTODISCOVERY_FILE_NAMES old_paths = AUTODISCOVERY_FILE_NAMES[::] AUTODISCOVERY_FILE_NAMES[:] = paths def finalizer() -> None: AUTODISCOVERY_FILE_NAMES[:] = old_paths request.addfinalizer(finalizer) return patcher @pytest.fixture def tmp_project_dir(monkeypatch: MonkeyPatch, tmp_path: Path) -> Path: path = tmp_path / "project_dir" path.mkdir(exist_ok=True) monkeypatch.chdir(path) return path class CreateAppFileFixture(Protocol): def __call__( self, file: str | Path, directory: str | Path | None = None, content: str | None = None, init_content: str = "", subdir: str | None = None, ) -> Path: ... def _purge_module(module_names: list[str], path: str | Path) -> None: for name in module_names: if name in sys.modules: del sys.modules[name] Path(importlib.util.cache_from_source(path)).unlink(missing_ok=True) # type: ignore[arg-type] @pytest.fixture def create_app_file(tmp_project_dir: Path, request: FixtureRequest) -> CreateAppFileFixture: def _create_app_file( file: str | Path, directory: str | Path | None = None, content: str | None = None, init_content: str = "", subdir: str | None = None, ) -> Path: base = tmp_project_dir if directory: base /= Path(Path(directory) / subdir) if subdir else Path(directory) base.mkdir(parents=True) base.joinpath("__init__.py").write_text(init_content) tmp_app_file = base / file tmp_app_file.write_text(content or APP_FILE_CONTENT) if directory: request.addfinalizer(lambda: rmtree(directory)) request.addfinalizer( lambda: _purge_module( [directory, _path_to_dotted_path(tmp_app_file.relative_to(Path.cwd()))], # type: ignore[list-item] tmp_app_file, ) ) else: request.addfinalizer(tmp_app_file.unlink) request.addfinalizer(lambda: _purge_module([str(file).replace(".py", "")], tmp_app_file)) return tmp_app_file return _create_app_file @pytest.fixture def app_file(create_app_file: CreateAppFileFixture) -> Path: return create_app_file("app.py") @pytest.fixture def runner() -> CliRunner: return CliRunner() @pytest.fixture def mock_uvicorn_run(mocker: MockerFixture) -> MagicMock: return mocker.patch("uvicorn.run") @pytest.fixture() def mock_subprocess_run(mocker: MockerFixture) -> MagicMock: return mocker.patch("subprocess.run") @pytest.fixture def mock_confirm_ask(mocker: MockerFixture) -> Generator[MagicMock, None, None]: yield mocker.patch("rich.prompt.Confirm.ask", return_value=True) @pytest.fixture( params=[ pytest.param((APP_FILE_CONTENT, "app"), id="app_obj"), pytest.param((CREATE_APP_FILE_CONTENT, "create_app"), id="create_app"), pytest.param((GENERIC_APP_FACTORY_FILE_CONTENT, "any_name"), id="app_factory"), pytest.param((GENERIC_APP_FACTORY_FILE_CONTENT_STRING_ANNOTATION, "any_name"), id="app_factory_str_annot"), pytest.param((GENERIC_APP_FACTORY_FILE_CONTENT_FUTURE_ANNOTATIONS, "any_name"), id="app_factory_future_annot"), ] ) def _app_file_content(request: FixtureRequest) -> tuple[str, str]: return cast("tuple[str, str]", request.param) @pytest.fixture def app_file_content(_app_file_content: tuple[str, str]) -> str: return _app_file_content[0] @pytest.fixture def app_file_app_name(_app_file_content: tuple[str, str]) -> str: return _app_file_content[1] litestar-2.16.0/tests/unit/test_cli/test_cli.py000066400000000000000000000105351500564371300215460ustar00rootroot00000000000000import importlib import sys from typing import TYPE_CHECKING from unittest.mock import MagicMock import pytest from tests.unit.test_cli import CREATE_APP_FILE_CONTENT from tests.unit.test_cli.conftest import CreateAppFileFixture try: from rich_click import group except ImportError: from click import group # type:ignore[no-redef] import litestar.cli._utils import litestar.cli.main from litestar import Litestar from litestar.cli._utils import _format_is_enabled from litestar.cli.main import litestar_group as cli_command if TYPE_CHECKING: from pathlib import Path from click.testing import CliRunner from pytest_mock import MockerFixture def test_format_is_enabled() -> None: assert _format_is_enabled(0) == "[red]Disabled[/]" assert _format_is_enabled(False) == "[red]Disabled[/]" assert _format_is_enabled("") == "[red]Disabled[/]" assert _format_is_enabled(1) == "[green]Enabled[/]" assert _format_is_enabled(True) == "[green]Enabled[/]" assert _format_is_enabled("a") == "[green]Enabled[/]" @pytest.mark.xdist_group("cli_autodiscovery") def test_info_command(mocker: "MockerFixture", runner: "CliRunner", app_file: "Path") -> None: mock = mocker.patch("litestar.cli.commands.core.show_app_info") result = runner.invoke(cli_command, ["info"]) assert result.exception is None mock.assert_called_once() @pytest.mark.xdist_group("cli_autodiscovery") def test_info_command_with_app_dir( mocker: "MockerFixture", runner: "CliRunner", create_app_file: CreateAppFileFixture ) -> None: app_file = "main.py" app_file_without_extension = app_file.split(".")[0] create_app_file( file=app_file, directory="src", content=CREATE_APP_FILE_CONTENT, subdir="info_with_app_dir", init_content=f"from .{app_file_without_extension} import create_app", ) mock = mocker.patch("litestar.cli.commands.core.show_app_info") result = runner.invoke(cli_command, ["--app", "info_with_app_dir:create_app", "--app-dir", "src", "info"]) assert result.exception is None mock.assert_called_once() @pytest.mark.xdist_group("cli_autodiscovery") def test_register_commands_from_entrypoint(mocker: "MockerFixture", runner: "CliRunner", app_file: "Path") -> None: mock_command_callback = MagicMock() @group() def custom_group() -> None: pass @custom_group.command() def custom_command(app: Litestar) -> None: mock_command_callback() mock_entry_point = MagicMock() mock_entry_point.load = lambda: custom_group if sys.version_info < (3, 10): mocker.patch("importlib_metadata.entry_points", return_value=[mock_entry_point]) else: mocker.patch("importlib.metadata.entry_points", return_value=[mock_entry_point]) importlib.reload(litestar.cli._utils) cli_command = importlib.reload(litestar.cli.main).litestar_group result = runner.invoke(cli_command, f"--app={app_file.stem}:app custom-group custom-command") assert result.exit_code == 0 mock_command_callback.assert_called_once() @pytest.mark.xdist_group("cli_autodiscovery") @pytest.mark.parametrize("invalid_app", ["invalid", "info_with_app_dir"]) def test_incorrect_app_argument( invalid_app: str, mocker: "MockerFixture", runner: "CliRunner", create_app_file: CreateAppFileFixture ) -> None: app_file = "main.py" app_file_without_extension = app_file.split(".")[0] create_app_file( file=app_file, directory="src", content=CREATE_APP_FILE_CONTENT, subdir="info_with_app_dir", init_content=f"from .{app_file_without_extension} import create_app", ) mock = mocker.patch("litestar.cli.commands.core.show_app_info") result = runner.invoke(cli_command, ["--app", invalid_app, "--app-dir", "src", "info"]) assert result.exit_code == 1 mock.assert_not_called() @pytest.mark.xdist_group("cli_autodiscovery") def test_invalid_import_in_app_argument( runner: "CliRunner", create_app_file: CreateAppFileFixture, tmp_project_dir: "Path" ) -> None: app_file = "main.py" create_app_file( file=app_file, content="from something import bar\n" + CREATE_APP_FILE_CONTENT, ) app_dir = str(tmp_project_dir.absolute()) result = runner.invoke(cli_command, ["--app", "main:create_app", "--app-dir", app_dir, "info"]) assert isinstance(result.exception, ModuleNotFoundError) litestar-2.16.0/tests/unit/test_cli/test_cli_plugin.py000066400000000000000000000026341500564371300231250ustar00rootroot00000000000000from click.testing import CliRunner from litestar.cli._utils import LitestarExtensionGroup from tests.unit.test_cli.conftest import CreateAppFileFixture APPLICATION_WITH_CLI_PLUGIN = """ from litestar import Litestar from litestar.plugins import CLIPluginProtocol class CLIPlugin(CLIPluginProtocol): def on_cli_init(self, cli): @cli.command() def mycommand(app: Litestar): \"\"\"Description of plugin command\"\"\" print(f"App is loaded: {app is not None}") app = Litestar(plugins=[CLIPlugin()]) """ def test_basic_command( runner: CliRunner, create_app_file: CreateAppFileFixture, root_command: LitestarExtensionGroup, ) -> None: app_file = create_app_file("command_test_app.py", content=APPLICATION_WITH_CLI_PLUGIN) result = runner.invoke(root_command, ["--app", f"{app_file.stem}:app", "mycommand"]) assert not result.exception assert "App is loaded: True" in result.output def test_plugin_command_appears_in_help_message( runner: CliRunner, create_app_file: CreateAppFileFixture, root_command: LitestarExtensionGroup, ) -> None: app_file = create_app_file("command_test_app.py", content=APPLICATION_WITH_CLI_PLUGIN) result = runner.invoke(root_command, ["--app", f"{app_file.stem}:app", "--help"]) assert not result.exception assert "mycommand" in result.output assert "Description of plugin command" in result.output litestar-2.16.0/tests/unit/test_cli/test_core_commands.py000066400000000000000000000550521500564371300236130ustar00rootroot00000000000000import io import os import re import sys from pathlib import Path from typing import Callable, Generator, List, Literal, Optional, Tuple, Union from unittest.mock import MagicMock import pytest from _pytest.monkeypatch import MonkeyPatch from click.testing import CliRunner from pytest_mock import MockerFixture from rich.console import Console from litestar import __version__ as litestar_version from litestar.cli import _utils from litestar.cli.commands import core from litestar.cli.main import litestar_group as cli_command from litestar.exceptions import LitestarWarning from . import ( APP_FACTORY_FILE_CONTENT_SERVER_LIFESPAN_PLUGIN, APP_FILE_CONTENT_ROUTES_EXAMPLE, CREATE_APP_FILE_CONTENT, GENERIC_APP_FACTORY_FILE_CONTENT, GENERIC_APP_FACTORY_FILE_CONTENT_STRING_ANNOTATION, ) from .conftest import CreateAppFileFixture project_base = Path(__file__).parent.parent.parent @pytest.fixture() def mock_show_app_info(mocker: MockerFixture) -> MagicMock: return mocker.patch("litestar.cli.commands.core.show_app_info") @pytest.mark.parametrize("set_in_env", [True, False]) @pytest.mark.parametrize( "host, port, uds, fd", [("0.0.0.0", 8081, "/run/uvicorn/litestar_test.sock", 0), (None, None, None, None)], ) @pytest.mark.parametrize("custom_app_file,", [Path("my_app.py"), None]) @pytest.mark.parametrize("app_dir", ["custom_subfolder", None]) @pytest.mark.parametrize( "reload, reload_dir, reload_include, reload_exclude, web_concurrency", [ (None, None, None, None, None), (True, None, None, None, None), (False, None, None, None, None), (True, [".", "../somewhere_else"], None, None, None), (False, [".", "../somewhere_else"], None, None, None), (True, None, ["*.rst", "*.yml"], None, None), (False, None, None, ["*.py"], None), (False, None, ["*.yml", "*.rst"], None, None), (None, None, None, None, 2), (True, None, None, None, 2), (False, None, None, None, 2), ], ) @pytest.mark.parametrize("tty_enabled", [True, False]) @pytest.mark.parametrize("quiet_console", [True, False]) def test_run_command( mock_show_app_info: MagicMock, mocker: MockerFixture, runner: CliRunner, monkeypatch: MonkeyPatch, reload: Optional[bool], port: Optional[int], host: Optional[str], fd: Optional[int], uds: Optional[str], web_concurrency: Optional[int], app_dir: Optional[str], reload_dir: Optional[List[str]], reload_include: Optional[List[str]], reload_exclude: Optional[List[str]], custom_app_file: Optional[Path], create_app_file: CreateAppFileFixture, set_in_env: bool, tty_enabled: bool, quiet_console: bool, mock_subprocess_run: MagicMock, mock_uvicorn_run: MagicMock, tmp_project_dir: Path, ) -> None: monkeypatch.delenv("LITESTAR_QUIET_CONSOLE", raising=False) if quiet_console: monkeypatch.setenv("LITESTAR_QUIET_CONSOLE", "true") mocker.patch.object(core, "isatty", return_value=tty_enabled) mocker.patch.object(_utils, "isatty", return_value=tty_enabled) args = [] if custom_app_file: args.extend(["--app", f"{custom_app_file.stem}:app"]) if app_dir is not None: args.extend(["--app-dir", str(Path(tmp_project_dir / app_dir))]) args.extend(["run"]) if reload: if set_in_env: monkeypatch.setenv("LITESTAR_RELOAD", "true") else: args.append("--reload") else: reload = False if port: if set_in_env: monkeypatch.setenv("LITESTAR_PORT", str(port)) else: args.extend(["--port", str(port)]) else: port = 8000 if host: if set_in_env: monkeypatch.setenv("LITESTAR_HOST", host) else: args.extend(["--host", host]) else: host = "127.0.0.1" if uds: if set_in_env: monkeypatch.setenv("LITESTAR_UNIX_DOMAIN_SOCKET", uds) else: args.extend(["--uds", uds]) else: uds = None if fd is not None: if set_in_env: monkeypatch.setenv("LITESTAR_FILE_DESCRIPTOR", str(fd)) else: args.extend(["--fd", str(fd)]) else: fd = None if web_concurrency is None: web_concurrency = 1 elif set_in_env: monkeypatch.setenv("LITESTAR_WEB_CONCURRENCY", str(web_concurrency)) else: args.extend(["--web-concurrency", str(web_concurrency)]) if reload_dir is not None: if set_in_env: monkeypatch.setenv("LITESTAR_RELOAD_DIRS", ",".join(reload_dir)) else: args.extend([f"--reload-dir={s}" for s in reload_dir]) if reload_include is not None: if set_in_env: monkeypatch.setenv("LITESTAR_RELOAD_INCLUDES", ",".join(reload_include)) else: args.extend([f"--reload-include={s}" for s in reload_include]) if reload_exclude is not None: if set_in_env: monkeypatch.setenv("LITESTAR_RELOAD_EXCLUDES", ",".join(reload_exclude)) else: args.extend([f"--reload-exclude={s}" for s in reload_exclude]) path = create_app_file(custom_app_file or "app.py", directory=app_dir) result = runner.invoke(cli_command, args) assert result.exception is None assert result.exit_code == 0 if reload or reload_dir or reload_include or reload_exclude or web_concurrency > 1: expected_args = [ sys.executable, "-m", "uvicorn", f"{path.stem}:app", f"--host={host}", f"--port={port}", ] if fd is not None: expected_args.append(f"--fd={fd}") if uds is not None: expected_args.append(f"--uds={uds}") if reload or reload_dir or reload_include or reload_exclude: expected_args.append("--reload") if web_concurrency: expected_args.append(f"--workers={web_concurrency}") if reload_dir: expected_args.extend([f"--reload-dir={s}" for s in reload_dir]) if reload_include: expected_args.extend([f"--reload-include={s}" for s in reload_include]) if reload_exclude: expected_args.extend([f"--reload-exclude={s}" for s in reload_exclude]) mock_subprocess_run.assert_called_once() assert sorted(mock_subprocess_run.call_args_list[0].args[0]) == sorted(expected_args) else: mock_subprocess_run.assert_not_called() mock_uvicorn_run.assert_called_once_with( app=f"{path.stem}:app", host=host, port=port, uds=uds, fd=fd, factory=False, ssl_certfile=None, ssl_keyfile=None, ) if tty_enabled and not quiet_console: mock_show_app_info.assert_called_once() else: mock_show_app_info.assert_not_called() @pytest.mark.parametrize("quiet_console", [True, False]) @pytest.mark.parametrize("tty_enabled", [True, False]) @pytest.mark.parametrize( "file_name,file_content,factory_name", [ ("_create_app.py", CREATE_APP_FILE_CONTENT, "create_app"), ("_generic_app_factory.py", GENERIC_APP_FACTORY_FILE_CONTENT, "any_name"), ("_generic_app_factory_string_ann.py", GENERIC_APP_FACTORY_FILE_CONTENT_STRING_ANNOTATION, "any_name"), ], ids=["create-app", "generic", "generic-string-annotated"], ) def test_run_command_with_autodiscover_app_factory( runner: CliRunner, mock_uvicorn_run: MagicMock, file_name: str, file_content: str, factory_name: str, patch_autodiscovery_paths: Callable[[List[str]], None], tty_enabled: bool, quiet_console: bool, create_app_file: CreateAppFileFixture, mocker: MockerFixture, monkeypatch: MonkeyPatch, ) -> None: monkeypatch.delenv("LITESTAR_QUIET_CONSOLE", raising=False) if quiet_console: monkeypatch.setenv("LITESTAR_QUIET_CONSOLE", "true") mocker.patch.object(core, "isatty", return_value=tty_enabled) mocker.patch.object(_utils, "isatty", return_value=tty_enabled) patch_autodiscovery_paths([file_name]) path = create_app_file(file_name, content=file_content) result = runner.invoke(cli_command, "run") assert result.exception is None assert result.exit_code == 0 mock_uvicorn_run.assert_called_once_with( app=f"{path.stem}:{factory_name}", host="127.0.0.1", port=8000, factory=True, uds=None, fd=None, ssl_certfile=None, ssl_keyfile=None, ) if tty_enabled and not quiet_console: assert len(result.output) > 0 else: assert len(result.output) == 0 @pytest.mark.parametrize("quiet_console", [True, False]) @pytest.mark.parametrize("tty_enabled", [True, False]) def test_run_command_with_app_factory( runner: CliRunner, mock_uvicorn_run: MagicMock, create_app_file: CreateAppFileFixture, tty_enabled: bool, quiet_console: bool, mocker: MockerFixture, monkeypatch: MonkeyPatch, ) -> None: monkeypatch.delenv("LITESTAR_QUIET_CONSOLE", raising=False) if quiet_console: monkeypatch.setenv("LITESTAR_QUIET_CONSOLE", "true") mocker.patch.object(core, "isatty", return_value=tty_enabled) mocker.patch.object(_utils, "isatty", return_value=tty_enabled) path = create_app_file("_create_app_with_path.py", content=CREATE_APP_FILE_CONTENT) app_path = f"{path.stem}:create_app" result = runner.invoke(cli_command, ["--app", app_path, "run"]) assert result.exception is None assert result.exit_code == 0 mock_uvicorn_run.assert_called_once_with( app=str(app_path), host="127.0.0.1", port=8000, factory=True, uds=None, fd=None, ssl_certfile=None, ssl_keyfile=None, ) if tty_enabled and not quiet_console: assert len(result.output) > 0 else: assert len(result.output) == 0 @pytest.mark.parametrize( "cli, env, expected", ( ( ("--reload", True), ("LITESTAR_RELOAD", False), "--reload", ), ( ("--reload-dir", [".", "../somewhere_else"]), ("LITESTAR_RELOAD_DIRS", ["../somewhere_else3", "../somewhere_else2"]), ["--reload-dir=.", "--reload-dir=../somewhere_else"], ), ( ("--reload-include", ["*.rst", "*.yml"]), ("LITESTAR_RELOAD_INCLUDES", ["*.rst2", "*.yml2"]), ["--reload-include=*.rst", "--reload-include=*.yml"], ), ( ("--reload-exclude", ["*.rst", "*.yml"]), ("LITESTAR_RELOAD_EXCLUDES", ["*.rst2", "*.yml2"]), ["--reload-exclude=*.rst", "--reload-exclude=*.yml"], ), ( ("--wc", 2), ("LITESTAR_WEB_CONCURRENCY", 4), "--workers=2", ), ( ("--fd", 0), ("LITESTAR_FILE_DESCRIPTOR", 1), "--fd=0", ), ( ("--uds", "/run/uvicorn/litestar_test.sock"), ("LITESTAR_UNIX_DOMAIN_SOCKET", "/run/uvicorn/litestar_test2.sock"), "--uds=/run/uvicorn/litestar_test.sock", ), ( ("-d", True), ("LITESTAR_DEBUG", False), ("LITESTAR_DEBUG", "1"), ), ( ("--pdb", True), ("LITESTAR_PDB", False), ("LITESTAR_PDB", "1"), ), ), ) def test_run_command_arguments_precedence( cli: Tuple[str, Union[Literal[True], List[str], str]], env: Tuple[str, Union[Literal[True], List[str], str]], expected: str, runner: CliRunner, monkeypatch: MonkeyPatch, mock_subprocess_run: MagicMock, tmp_project_dir: Path, create_app_file: CreateAppFileFixture, mock_uvicorn_run: MagicMock, ) -> None: args = [] args.extend(["--app", f"{Path('my_app.py').stem}:app"]) args.extend(["--app-dir", str(Path(tmp_project_dir / "custom_subfolder"))]) args.extend(["run"]) create_app_file("my_app.py", directory="custom_subfolder") env_name, env_value = env cli_name, cli_value = cli if env_name: if isinstance(env_value, list): monkeypatch.setenv(env_name, "".join(env_value)) else: monkeypatch.setenv(env_name, str(env_value)) if cli_name: if cli_value is True: args.append(cli_name) elif isinstance(cli_value, list): for value in cli_value: args.extend([cli_name, value]) else: args.extend([cli_name, cli_value]) result = runner.invoke(cli_command, args) assert result.exception is None assert result.exit_code == 0 if cli_name in ["--fd", "--uds"]: mock_subprocess_run.assert_not_called() if isinstance(expected, list): # type: ignore[unreachable] assert all(_ in mock_uvicorn_run.call_args_list[0].args[0] for _ in expected) # type: ignore[unreachable] else: assert mock_uvicorn_run.call_args_list[0].kwargs.get(cli_name.strip("--")) == cli_value elif cli_name in ["-d", "--pdb"]: assert os.environ.get(expected[0]) == expected[1] else: mock_subprocess_run.assert_called_once() if isinstance(expected, list): # type: ignore[unreachable] assert all(_ in mock_subprocess_run.call_args_list[0].args[0] for _ in expected) # type: ignore[unreachable] else: assert expected in mock_subprocess_run.call_args_list[0].args[0] @pytest.fixture() def unset_env() -> Generator[None, None, None]: initial_env = {**os.environ} yield for key in os.environ.keys() - initial_env.keys(): del os.environ[key] os.environ.update(initial_env) @pytest.mark.usefixtures("mock_uvicorn_run", "unset_env") def test_run_command_debug( app_file: Path, runner: CliRunner, monkeypatch: MonkeyPatch, create_app_file: CreateAppFileFixture ) -> None: monkeypatch.delenv("LITESTAR_DEBUG", raising=False) path = create_app_file("_create_app_with_path.py", content=CREATE_APP_FILE_CONTENT) app_path = f"{path.stem}:create_app" result = runner.invoke(cli_command, ["--app", app_path, "run", "--debug"]) assert result.exit_code == 0 assert os.getenv("LITESTAR_DEBUG") == "1" @pytest.mark.usefixtures("mock_uvicorn_run", "unset_env") def test_run_command_quiet_console( app_file: Path, mocker: MockerFixture, runner: CliRunner, monkeypatch: MonkeyPatch, create_app_file: CreateAppFileFixture, ) -> None: mocker.patch.object(core, "isatty", return_value=True) mocker.patch.object(_utils, "isatty", return_value=True) console = Console(file=io.StringIO(), force_interactive=True) monkeypatch.setattr(_utils, "console", console) path = create_app_file("_create_app_with_path.py", content=CREATE_APP_FILE_CONTENT) app_path = f"{path.stem}:create_app" monkeypatch.delenv("LITESTAR_QUIET_CONSOLE", raising=False) result = runner.invoke(cli_command, ["--app", app_path, "run"]) assert result.exit_code == 0 normal_output = console.file.getvalue() # type: ignore[attr-defined] assert "Using Litestar app from env:" in normal_output assert "Starting server process" in result.stdout del result console = Console(file=io.StringIO(), force_interactive=True) monkeypatch.setattr(_utils, "console", console) monkeypatch.setenv("LITESTAR_QUIET_CONSOLE", "1") assert os.getenv("LITESTAR_QUIET_CONSOLE") == "1" result = runner.invoke(cli_command, ["--app", app_path, "run"]) assert result.exit_code == 0 quiet_output = console.file.getvalue() # type: ignore[attr-defined] assert "Starting server process" not in result.stdout assert "Using Litestar app from env:" not in quiet_output console.clear() @pytest.mark.usefixtures("mock_uvicorn_run", "unset_env") def test_run_command_custom_app_name( app_file: Path, runner: CliRunner, monkeypatch: MonkeyPatch, create_app_file: CreateAppFileFixture, mocker: MockerFixture, ) -> None: mocker.patch.object(core, "isatty", return_value=True) mocker.patch.object(_utils, "isatty", return_value=True) console = Console(file=io.StringIO(), force_interactive=True) monkeypatch.setattr(_utils, "console", console) path = create_app_file("_create_app_with_path.py", content=CREATE_APP_FILE_CONTENT) app_path = f"{path.stem}:create_app" monkeypatch.delenv("LITESTAR_APP_NAME", raising=False) result = runner.invoke(cli_command, ["--app", app_path, "run"]) assert result.exit_code == 0 _output = console.file.getvalue() # type: ignore[attr-defined] assert "Using Litestar app from env:" in _output console = Console(file=io.StringIO(), force_interactive=True) monkeypatch.setattr(_utils, "console", console) monkeypatch.setenv("LITESTAR_APP_NAME", "My Stuff") assert os.getenv("LITESTAR_APP_NAME") == "My Stuff" result = runner.invoke(cli_command, ["--app", app_path, "run"]) assert result.exit_code == 0 _output = console.file.getvalue() # type: ignore[attr-defined] assert "Using My Stuff app from env:" in _output @pytest.mark.usefixtures("mock_uvicorn_run", "unset_env") def test_run_command_pdb( app_file: Path, runner: CliRunner, monkeypatch: MonkeyPatch, create_app_file: CreateAppFileFixture, ) -> None: monkeypatch.delenv("LITESTAR_PDB", raising=False) path = create_app_file("_create_app_with_path.py", content=CREATE_APP_FILE_CONTENT) app_path = f"{path.stem}:create_app" with pytest.warns(LitestarWarning): result = runner.invoke(cli_command, ["--app", app_path, "run", "--pdb"]) assert result.exit_code == 0 assert os.getenv("LITESTAR_PDB") == "1" @pytest.mark.usefixtures("mock_uvicorn_run", "unset_env") def test_run_command_without_uvicorn_installed( app_file: Path, runner: CliRunner, monkeypatch: MonkeyPatch, create_app_file: CreateAppFileFixture, mocker: MockerFixture, ) -> None: mocker.patch("litestar.cli.commands.core.UVICORN_INSTALLED", False) console_print_mock = mocker.patch("litestar.cli.commands.core.console.print") path = create_app_file("_create_app_with_path.py", content=CREATE_APP_FILE_CONTENT) app_path = f"{path.stem}:create_app" result = runner.invoke(cli_command, ["--app", app_path, "run"]) assert result.exit_code == 1 assert any("uvicorn is not installed" in arg for args, kwargs in console_print_mock.call_args_list for arg in args) @pytest.mark.parametrize("short", [True, False]) def test_version_command(short: bool, runner: CliRunner) -> None: result = runner.invoke(cli_command, "version --short" if short else "version") assert result.output.strip() == litestar_version.formatted(short=short) @pytest.mark.usefixtures("mock_uvicorn_run", "unset_env") def test_run_command_with_server_lifespan_plugin( runner: CliRunner, mock_uvicorn_run: MagicMock, create_app_file: CreateAppFileFixture ) -> None: path = create_app_file("_create_app_with_path.py", content=APP_FACTORY_FILE_CONTENT_SERVER_LIFESPAN_PLUGIN) app_path = f"{path.stem}:create_app" result = runner.invoke(cli_command, ["--app", app_path, "run"]) assert result.exception is None assert result.exit_code == 0 assert "i_run_before_startup_plugin" in result.stdout assert "i_run_after_shutdown_plugin" in result.stdout assert result.stdout.find("i_run_before_startup_plugin") < result.stdout.find("i_run_after_shutdown_plugin") mock_uvicorn_run.assert_called_once_with( app=str(app_path), host="127.0.0.1", port=8000, fd=None, uds=None, factory=True, ssl_certfile=None, ssl_keyfile=None, ) @pytest.mark.parametrize( "app_content, schema_enabled, exclude_pattern_list, expected_result_routes_count", [ pytest.param(APP_FILE_CONTENT_ROUTES_EXAMPLE, False, (), 3, id="schema-disabled_no-exclude"), pytest.param( APP_FILE_CONTENT_ROUTES_EXAMPLE, False, ("/foo", "/destroy/.*", "/java", "/haskell"), 2, id="schema-disabled_exclude", ), pytest.param(APP_FILE_CONTENT_ROUTES_EXAMPLE, True, (), 13, id="schema-enabled_no-exclude"), pytest.param( APP_FILE_CONTENT_ROUTES_EXAMPLE, True, ("/foo", "/destroy/.*", "/java", "/haskell"), 12, id="schema-enabled_exclude", ), ], ) @pytest.mark.xdist_group("cli_autodiscovery") def test_routes_command_options( runner: CliRunner, app_content: str, schema_enabled: bool, exclude_pattern_list: Tuple[str, ...], create_app_file: CreateAppFileFixture, expected_result_routes_count: int, ) -> None: create_app_file("app.py", content=app_content) command = "routes" if schema_enabled: command += " --schema " if exclude_pattern_list: for pattern in exclude_pattern_list: command += f" --exclude={pattern}" result = runner.invoke(cli_command, command) assert result.exception is None assert result.exit_code == 0 result_routes = [line for line in result.output.splitlines() if "(HTTP)" in line] for route in result_routes: root_dir = route.split(" ")[0] if not schema_enabled: assert root_dir != "/api-docs" assert root_dir not in exclude_pattern_list assert expected_result_routes_count == len(result_routes) def test_remove_default_schema_routes() -> None: routes = [ "/", "/schema", "/schema/elements", "/schema/oauth2-redirect.html", "/schema/openapi.json", "/schema/openapi.yaml", "/schema/openapi.yml", "/schema/rapidoc", "/schema/redoc", "/schema/swagger", "/destroy/all/foo/bar/schema", "/foo", ] http_routes = [] for route in routes: http_route = MagicMock() http_route.path = route http_routes.append(http_route) api_config = MagicMock() api_config.openapi_controller.path = "/schema" results = _utils.remove_default_schema_routes(http_routes, api_config) # type: ignore[arg-type] assert len(results) == 3 for result in results: words = re.split(r"(^\/[a-z]+)", result.path) assert "/schema" not in words def test_remove_routes_with_patterns() -> None: routes = ["/", "/destroy/all/foo/bar/schema", "/foo"] http_routes = [] for route in routes: http_route = MagicMock() http_route.path = route http_routes.append(http_route) patterns = ("/destroy", "/pizza", "[]") results = _utils.remove_routes_with_patterns(http_routes, patterns) # type: ignore[arg-type] paths = [route.path for route in results] assert len(paths) == 2 for route in ["/", "/foo"]: assert route in paths litestar-2.16.0/tests/unit/test_cli/test_env_resolution.py000066400000000000000000000103061500564371300240460ustar00rootroot00000000000000from pathlib import Path import pytest from _pytest.monkeypatch import MonkeyPatch from click import ClickException from litestar import Litestar from litestar.cli._utils import LitestarEnv, _path_to_dotted_path from .conftest import CreateAppFileFixture pytestmark = pytest.mark.xdist_group("cli_autodiscovery") def test_litestar_env_from_env_port(monkeypatch: MonkeyPatch, app_file: Path) -> None: env = LitestarEnv.from_env(f"{app_file.stem}:app") assert env.port is None monkeypatch.setenv("LITESTAR_PORT", "7000") env = LitestarEnv.from_env(f"{app_file.stem}:app") assert env.port == 7000 def test_litestar_env_from_env_host(monkeypatch: MonkeyPatch, app_file: Path) -> None: env = LitestarEnv.from_env(f"{app_file.stem}:app") assert env.host is None monkeypatch.setenv("LITESTAR_HOST", "0.0.0.0") env = LitestarEnv.from_env(f"{app_file.stem}:app") assert env.host == "0.0.0.0" @pytest.mark.parametrize( "path", [ pytest.param("app.py", id="app_file"), pytest.param("application.py", id="application_file"), pytest.param("app/main.py", id="app_module"), pytest.param("app/any_name.py", id="app_module_random"), pytest.param("application/another_random_name.py", id="application_module_random"), ], ) def test_env_from_env_autodiscover_from_files( path: str, app_file_content: str, app_file_app_name: str, create_app_file: CreateAppFileFixture ) -> None: directory = None if "/" in path: directory, path = path.split("/", 1) tmp_file_path = create_app_file(file=path, directory=directory, content=app_file_content) env = LitestarEnv.from_env(None) dotted_path = _path_to_dotted_path(tmp_file_path.relative_to(Path.cwd())) assert isinstance(env.app, Litestar) print("parent directory content: %s", list(tmp_file_path.parent.iterdir())) # noqa: T201 assert env.app_path == f"{dotted_path}:{app_file_app_name}" @pytest.mark.parametrize( "module_name,app_file", [ ("app", "main.py"), ("application", "main.py"), ("app", "anything.py"), ("application", "anything.py"), ], ) def test_env_from_env_autodiscover_from_module( module_name: str, app_file: str, app_file_content: str, app_file_app_name: str, create_app_file: CreateAppFileFixture, ) -> None: create_app_file( file=app_file, directory=module_name, content=app_file_content, init_content=f"from .{app_file.split('.')[0]} import {app_file_app_name}", ) env = LitestarEnv.from_env(None) assert isinstance(env.app, Litestar) assert env.app_path == f"{module_name}:{app_file_app_name}" @pytest.mark.parametrize("path", [".app.py", "_app.py", ".application.py", "_application.py"]) def test_env_from_env_autodiscover_from_files_ignore_paths( path: str, app_file_content: str, create_app_file: CreateAppFileFixture ) -> None: create_app_file(file=path, directory=None, content=app_file_content) with pytest.raises(ClickException): LitestarEnv.from_env(None) @pytest.mark.parametrize("use_file_in_app_path", [True, False]) def test_env_using_app_dir( app_file_content: str, app_file_app_name: str, create_app_file: CreateAppFileFixture, use_file_in_app_path: bool ) -> None: app_file = "main.py" app_file_without_extension = app_file.split(".")[0] tmp_file_path = create_app_file( file=app_file, directory="src", content=app_file_content, subdir=f"litestar_test_{app_file_app_name}", init_content=f"from .{app_file_without_extension} import {app_file_app_name}", ) app_path_components = [f"litestar_test_{app_file_app_name}"] if use_file_in_app_path: app_path_components.append(app_file_without_extension) app_path = f"{'.'.join(app_path_components)}:{app_file_app_name}" env = LitestarEnv.from_env(app_path, app_dir=Path().cwd() / "src") dotted_path = _path_to_dotted_path(tmp_file_path.relative_to(Path.cwd())) assert isinstance(env.app, Litestar) dotted_path = dotted_path.replace("src.", "") if not use_file_in_app_path: dotted_path = dotted_path.replace(".main", "") assert env.app_path == f"{dotted_path}:{app_file_app_name}" litestar-2.16.0/tests/unit/test_cli/test_schema_commands.py000066400000000000000000000070211500564371300241140ustar00rootroot00000000000000from __future__ import annotations from json import dumps as json_dumps from typing import TYPE_CHECKING, Callable import pytest from yaml import dump as dump_yaml from litestar.cli.commands.schema import _generate_openapi_schema from litestar.cli.main import litestar_group as cli_command if TYPE_CHECKING: from pathlib import Path from types import ModuleType from click.testing import CliRunner from pytest import MonkeyPatch from pytest_mock import MockerFixture @pytest.mark.parametrize("filename", ("", "custom.json", "custom.yaml", "custom.yml")) def test_openapi_schema_command( runner: CliRunner, mocker: MockerFixture, monkeypatch: MonkeyPatch, filename: str ) -> None: monkeypatch.setenv("LITESTAR_APP", "test_apps.openapi_test_app.main:app") mock_path_write_bytes = mocker.patch("pathlib.Path.write_bytes") command = "schema openapi" from test_apps.openapi_test_app.main import app as openapi_test_app assert openapi_test_app.openapi_schema schema = openapi_test_app.openapi_schema.to_schema() expected_content = json_dumps(schema, indent=4).encode() if filename: command += f" --output {filename}" if filename.endswith(("yaml", "yml")): expected_content = dump_yaml(schema, default_flow_style=False, encoding="utf-8") result = runner.invoke(cli_command, command) assert result.exit_code == 0 mock_path_write_bytes.assert_called_once_with(expected_content) @pytest.mark.parametrize("suffix", ("json", "yaml", "yml")) def test_schema_export_with_examples(suffix: str, create_module: Callable[[str], ModuleType], tmp_path: Path) -> None: module = create_module( """ from datetime import datetime from litestar import Litestar, get from litestar.openapi import OpenAPIConfig @get() async def something(date: datetime) -> None: return None app = Litestar([something], openapi_config=OpenAPIConfig('example', '0.0.1', True)) """ ) pth = tmp_path / f"openapi.{suffix}" _generate_openapi_schema(module.app, pth) assert pth.read_text() @pytest.mark.parametrize( "namespace, filename", (("Custom", ""), ("", "custom_specs.ts"), ("Custom", "custom_specs.ts")) ) def test_openapi_typescript_command( runner: CliRunner, mocker: MockerFixture, monkeypatch: MonkeyPatch, filename: str, namespace: str ) -> None: monkeypatch.setenv("LITESTAR_APP", "test_apps.openapi_test_app.main:app") mock_path_write_text = mocker.patch("pathlib.Path.write_text") command = "schema typescript" if namespace: command += f" --namespace {namespace}" if filename: command += f" --output {filename}" result = runner.invoke(cli_command, command) assert result.exit_code == 0 assert mock_path_write_text.called @pytest.mark.parametrize( "namespace, filename", (("Custom", ""), ("", "custom_specs.ts"), ("Custom", "custom_specs.ts")) ) def test_openapi_typescript_command_without_jsbeautifier( runner: CliRunner, mocker: MockerFixture, monkeypatch: MonkeyPatch, filename: str, namespace: str ) -> None: monkeypatch.setenv("LITESTAR_APP", "test_apps.openapi_test_app.main:app") mocker.patch("litestar.cli.commands.schema.JSBEAUTIFIER_INSTALLED", False) mock_path_write_text = mocker.patch("pathlib.Path.write_text") command = "schema typescript" if namespace: command += f" --namespace {namespace}" if filename: command += f" --output {filename}" result = runner.invoke(cli_command, command) assert result.exit_code == 0 assert mock_path_write_text.called litestar-2.16.0/tests/unit/test_cli/test_session_commands.py000066400000000000000000000062271500564371300243460ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar import Litestar from litestar.cli.commands.sessions import get_session_backend from litestar.cli.main import litestar_group as cli_command from litestar.middleware.rate_limit import RateLimitConfig from litestar.middleware.session.server_side import ServerSideSessionConfig if TYPE_CHECKING: from unittest.mock import MagicMock from _pytest.monkeypatch import MonkeyPatch from click.testing import CliRunner from pytest_mock import MockerFixture def test_get_session_backend() -> None: session_middleware = ServerSideSessionConfig().middleware app = Litestar([], middleware=[RateLimitConfig(rate_limit=("second", 1)).middleware, session_middleware]) assert get_session_backend(app) is session_middleware.kwargs["backend"] def test_delete_session_no_backend(runner: CliRunner, monkeypatch: MonkeyPatch) -> None: monkeypatch.setenv("LITESTAR_APP", "docs.examples.hello_world:app") result = runner.invoke(cli_command, "sessions delete foo") assert result.exit_code == 1 assert "Session middleware not installed" in result.output def test_delete_session_cookie_backend(runner: CliRunner, monkeypatch: MonkeyPatch) -> None: monkeypatch.setenv("LITESTAR_APP", "docs.examples.middleware.session.cookie_backend:app") result = runner.invoke(cli_command, "sessions delete foo") assert result.exit_code == 1 assert "Only server-side backends are supported" in result.output def test_delete_session( runner: CliRunner, monkeypatch: MonkeyPatch, mocker: MockerFixture, mock_confirm_ask: MagicMock ) -> None: monkeypatch.setenv("LITESTAR_APP", "docs.examples.middleware.session.file_store:app") mock_delete = mocker.patch("litestar.stores.file.FileStore.delete") result = runner.invoke(cli_command, ["sessions", "delete", "foo"]) mock_confirm_ask.assert_called_once_with("Delete session 'foo'?") assert not result.exception mock_delete.assert_called_once_with("foo") def test_clear_sessions_no_backend(runner: CliRunner, monkeypatch: MonkeyPatch) -> None: monkeypatch.setenv("LITESTAR_APP", "docs.examples.hello_world:app") result = runner.invoke(cli_command, "sessions clear") assert result.exit_code == 1 assert "Session middleware not installed" in result.output def test_clear_sessions_cookie_backend(runner: CliRunner, monkeypatch: MonkeyPatch) -> None: monkeypatch.setenv("LITESTAR_APP", "docs.examples.middleware.session.cookie_backend:app") result = runner.invoke(cli_command, "sessions clear") assert result.exit_code == 1 assert "Only server-side backends are supported" in result.output def test_clear_sessions( runner: CliRunner, monkeypatch: MonkeyPatch, mocker: MockerFixture, mock_confirm_ask: MagicMock ) -> None: monkeypatch.setenv("LITESTAR_APP", "docs.examples.middleware.session.file_store:app") mock_delete = mocker.patch("litestar.stores.file.FileStore.delete_all") result = runner.invoke(cli_command, ["sessions", "clear"]) mock_confirm_ask.assert_called_once_with("[red]Delete all sessions?") assert not result.exception mock_delete.assert_called_once() litestar-2.16.0/tests/unit/test_cli/test_ssl.py000066400000000000000000000230701500564371300215760ustar00rootroot00000000000000import sys from pathlib import Path from typing import Optional, Protocol, cast from unittest.mock import MagicMock import pytest from click import ClickException from click.testing import CliRunner from pytest_mock import MockerFixture from litestar.cli.main import litestar_group as cli_command class GetClickExceptionFixture(Protocol): def __call__(self, exception: SystemExit) -> ClickException: ... @pytest.fixture def get_click_exception() -> GetClickExceptionFixture: def _get_click_exception(exception: SystemExit) -> ClickException: exc = exception while exc.__context__ is not None: if isinstance(exc, ClickException): break exc = exc.__context__ # type: ignore[assignment] return cast(ClickException, exc) return _get_click_exception @pytest.mark.parametrize("create_self_signed_cert", (True, False)) @pytest.mark.usefixtures("mock_uvicorn_run") def test_both_files_provided(app_file: Path, runner: CliRunner, create_self_signed_cert: bool) -> None: path = app_file app_path = f"{path.stem}:app" cert_path = path.parent / "cert.pem" with cert_path.open("wb") as certfile: certfile.write(b"certfile") key_path = path.parent / "key.pem" with key_path.open("wb") as keyfile: keyfile.write(b"keyfile") args = ["--app", app_path, "run", "--ssl-certfile", str(cert_path), "--ssl-keyfile", str(key_path)] if create_self_signed_cert: args.append("--create-self-signed-cert") result = runner.invoke(cli_command, args) assert result.exception is None assert result.exit_code == 0 if create_self_signed_cert: with cert_path.open("rb") as certfile: assert certfile.read() == b"certfile" with key_path.open("rb") as keyfile: assert keyfile.read() == b"keyfile" @pytest.mark.parametrize("create_self_signed_cert", (True, False)) @pytest.mark.parametrize( "ssl_certfile, ssl_keyfile", [("directory", "exists.pem"), ("exists.pem", "directory")], ) def test_path_is_a_directory( app_file: Path, runner: CliRunner, ssl_certfile: str, ssl_keyfile: str, create_self_signed_cert: bool, get_click_exception: GetClickExceptionFixture, ) -> None: path = app_file app_path = f"{path.stem}:app" (path.parent / "exists.pem").touch() (path.parent / "directory").mkdir(exist_ok=True) args = ["--app", app_path, "run", "--ssl-certfile", ssl_certfile, "--ssl-keyfile", ssl_keyfile] if create_self_signed_cert: args.append("--create-self-signed-cert") result = runner.invoke(cli_command, args) assert result.exit_code == 1 assert isinstance(result.exception, SystemExit) exc = get_click_exception(result.exception) assert "Path provided for" in exc.message assert "is a directory" in exc.message @pytest.mark.parametrize("create_self_signed_cert", (True, False)) @pytest.mark.parametrize( "ssl_certfile, ssl_keyfile", [("exists.pem", None), (None, "exists.pem")], ) def test_one_file_provided( app_file: Path, runner: CliRunner, ssl_certfile: Optional[str], ssl_keyfile: Optional[str], create_self_signed_cert: bool, get_click_exception: GetClickExceptionFixture, ) -> None: path = app_file app_path = f"{path.stem}:app" (path.parent / "exists.pem").touch() args = ["--app", app_path, "run"] if ssl_certfile is not None: args.extend(["--ssl-certfile", str(ssl_certfile)]) if ssl_keyfile is not None: args.extend(["--ssl-keyfile", str(ssl_keyfile)]) if create_self_signed_cert: args.append("--create-self-signed-cert") result = runner.invoke(cli_command, args) assert result.exit_code == 1 assert isinstance(result.exception, SystemExit) exc = get_click_exception(result.exception) assert "No value provided for" in exc.message @pytest.mark.parametrize("create_self_signed_cert", (True, False)) @pytest.mark.parametrize( "ssl_certfile, ssl_keyfile", [("not_exists.pem", "exists.pem"), ("exists.pem", "not_exists.pem")], ) def test_one_file_not_found( app_file: Path, runner: CliRunner, ssl_certfile: str, ssl_keyfile: str, create_self_signed_cert: bool, get_click_exception: GetClickExceptionFixture, ) -> None: path = app_file app_path = f"{path.stem}:app" (path.parent / "exists.pem").touch() args = ["--app", app_path, "run"] if ssl_certfile is not None: args.extend(["--ssl-certfile", ssl_certfile]) if ssl_keyfile is not None: args.extend(["--ssl-keyfile", ssl_keyfile]) if create_self_signed_cert: args.append("--create-self-signed-cert") result = runner.invoke(cli_command, args) assert result.exit_code == 1 assert isinstance(result.exception, SystemExit) exc = get_click_exception(result.exception) if create_self_signed_cert: assert ( "Both certificate and key file must exists or both must not exists when using --create-self-signed-cert" in exc.message ) else: assert "File provided for" in exc.message assert "was not found" in exc.message @pytest.mark.parametrize( "ssl_certfile, ssl_keyfile", [("dir_exists/file.pem", "dir_not_exists/file.pem"), ("dir_not_exists/file.pem", "dir_exists/file.pem")], ) def test_file_parent_doesnt_exist( app_file: Path, runner: CliRunner, ssl_certfile: str, ssl_keyfile: str, get_click_exception: GetClickExceptionFixture, ) -> None: path = app_file app_path = f"{path.stem}:app" (path.parent / "dir_exists").mkdir(exist_ok=True) args = [ "--app", app_path, "run", "--ssl-certfile", ssl_certfile, "--ssl-keyfile", ssl_keyfile, "--create-self-signed-cert", ] result = runner.invoke(cli_command, args) assert result.exit_code == 1 assert isinstance(result.exception, SystemExit) exc = get_click_exception(result.exception) assert "Could not create file, parent directory for" in exc.message assert "doesn't exist" in exc.message def test_without_cryptography_installed( app_file: Path, runner: CliRunner, get_click_exception: GetClickExceptionFixture, mocker: MockerFixture, ) -> None: mocker.patch.dict("sys.modules", {"cryptography": None}) path = app_file app_path = f"{path.stem}:app" args = [ "--app", app_path, "run", "--ssl-certfile", "certfile.pem", "--ssl-keyfile", "keyfile.pem", "--create-self-signed-cert", ] result = runner.invoke(cli_command, args) assert result.exit_code == 1 assert isinstance(result.exception, SystemExit) exc = get_click_exception(result.exception) assert "Cryptography must be installed when using --create-self-signed-cert" in exc.message @pytest.mark.usefixtures("mock_uvicorn_run") def test_create_certificates(app_file: Path, runner: CliRunner) -> None: path = app_file app_path = f"{path.stem}:app" certfile_path = path.parent / "certificate.pem" keyfile_path = path.parent / "key.pem" args = [ "--app", app_path, "run", "--ssl-certfile", str(certfile_path), "--ssl-keyfile", str(keyfile_path), "--create-self-signed-cert", ] result = runner.invoke(cli_command, args) assert result.exit_code == 0 assert result.exception is None assert certfile_path.exists() assert keyfile_path.exists() @pytest.mark.parametrize( "ssl_certfile, ssl_keyfile, create_self_signed_cert", [(None, None, False), ("cert.pem", "key.pem", True)] ) @pytest.mark.parametrize("run_as_subprocess", (True, False)) def test_arguments_passed( app_file: Path, runner: CliRunner, mock_subprocess_run: MagicMock, mock_uvicorn_run: MagicMock, ssl_certfile: Optional[str], ssl_keyfile: Optional[str], create_self_signed_cert: bool, run_as_subprocess: bool, ) -> None: path = app_file app_path = f"{path.stem}:app" project_path = path.parent args = ["--app", app_path, "run"] if run_as_subprocess: args.extend(["--web-concurrency", "2"]) if ssl_certfile is not None: args.extend(["--ssl-certfile", str(ssl_certfile)]) if ssl_keyfile is not None: args.extend(["--ssl-keyfile", str(ssl_keyfile)]) if create_self_signed_cert: args.append("--create-self-signed-cert") result = runner.invoke(cli_command, args) assert result.exit_code == 0 assert result.exception is None if run_as_subprocess: expected_args = [ sys.executable, "-m", "uvicorn", f"{path.stem}:app", "--host=127.0.0.1", "--port=8000", "--workers=2", ] if ssl_certfile is not None: expected_args.append(f"--ssl-certfile={project_path / ssl_certfile}") if ssl_keyfile is not None: expected_args.append(f"--ssl-keyfile={project_path / ssl_keyfile}") mock_subprocess_run.assert_called_once() assert sorted(mock_subprocess_run.call_args_list[0].args[0]) == sorted(expected_args) else: mock_subprocess_run.assert_not_called() mock_uvicorn_run.assert_called_once_with( app=f"{path.stem}:app", host="127.0.0.1", port=8000, factory=False, fd=None, uds=None, ssl_certfile=(None if ssl_certfile is None else str(project_path / ssl_certfile)), ssl_keyfile=(None if ssl_keyfile is None else str(project_path / ssl_keyfile)), ) litestar-2.16.0/tests/unit/test_concurrency.py000066400000000000000000000057441500564371300215310ustar00rootroot00000000000000from __future__ import annotations import asyncio from concurrent.futures import ThreadPoolExecutor from typing import Generator from unittest.mock import AsyncMock import pytest import trio from pytest_mock import MockerFixture from litestar.concurrency import ( _State, get_asyncio_executor, get_trio_capacity_limiter, set_asyncio_executor, set_trio_capacity_limiter, sync_to_thread, ) @pytest.fixture(autouse=True) def reset_state() -> Generator[None, None, None]: _State.LIMITER = None _State.EXECUTOR = None yield _State.LIMITER = None _State.EXECUTOR = None def func() -> int: return 1 def test_sync_to_thread_asyncio() -> None: loop = asyncio.new_event_loop() assert loop.run_until_complete(sync_to_thread(func)) == 1 def test_sync_to_thread_trio() -> None: assert trio.run(sync_to_thread, func) == 1 def test_get_set_asyncio_executor() -> None: assert get_asyncio_executor() is None executor = ThreadPoolExecutor() set_asyncio_executor(executor) assert get_asyncio_executor() is executor def test_get_set_trio_capacity_limiter() -> None: limiter = trio.CapacityLimiter(10) assert get_trio_capacity_limiter() is None set_trio_capacity_limiter(limiter) assert get_trio_capacity_limiter() is limiter def test_asyncio_uses_executor(mocker: MockerFixture) -> None: executor = ThreadPoolExecutor() mocker.patch("litestar.concurrency.get_asyncio_executor", return_value=executor) mock_run_in_executor = AsyncMock() mocker.patch("litestar.concurrency.asyncio.get_running_loop").return_value.run_in_executor = mock_run_in_executor loop = asyncio.new_event_loop() loop.run_until_complete(sync_to_thread(func)) assert mock_run_in_executor.call_args_list[0].args[0] is executor def test_set_asyncio_executor_from_running_loop_raises() -> None: async def main() -> None: set_asyncio_executor(ThreadPoolExecutor()) with pytest.raises(RuntimeError): asyncio.new_event_loop().run_until_complete(main()) assert get_asyncio_executor() is None def test_trio_uses_limiter(mocker: MockerFixture) -> None: limiter = trio.CapacityLimiter(10) mocker.patch("litestar.concurrency.get_trio_capacity_limiter", return_value=limiter) mock_run_sync = mocker.patch("trio.to_thread.run_sync", new_callable=AsyncMock) trio.run(sync_to_thread, func) assert mock_run_sync.call_args_list[0].kwargs["limiter"] is limiter def test_set_trio_capacity_limiter_from_async_context_raises() -> None: async def main() -> None: set_trio_capacity_limiter(trio.CapacityLimiter(1)) with pytest.raises(RuntimeError): trio.run(main) assert get_trio_capacity_limiter() is None def test_sync_to_thread_unsupported_lib(mocker: MockerFixture) -> None: mocker.patch("litestar.concurrency.sniffio.current_async_library", return_value="something") with pytest.raises(RuntimeError): asyncio.new_event_loop().run_until_complete(sync_to_thread(func)) litestar-2.16.0/tests/unit/test_connection/000077500000000000000000000000001500564371300207525ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_connection/__init__.py000066400000000000000000000000001500564371300230510ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_connection/test_base.py000066400000000000000000000033441500564371300233010ustar00rootroot00000000000000from typing import Any from litestar import Litestar, get from litestar.connection import ASGIConnection from litestar.logging.config import LoggingConfig from litestar.testing import RequestFactory from litestar.types.empty import Empty from litestar.utils.scope.state import ScopeState def test_connection_base_properties() -> None: @get("/") def handler() -> None: return None app = Litestar(route_handlers=[handler], logging_config=LoggingConfig()) user = {"name": "moishe"} auth = {"key": "value"} session = {"session": "abc"} scope = RequestFactory(app=app).get(route_handler=handler, user=user, auth=auth, session=session).scope connection = ASGIConnection[Any, Any, Any, Any](scope) connection_state = ScopeState.from_scope(scope) assert connection.app assert connection.app is app assert connection.route_handler is handler assert connection.state is not None assert connection_state.url is Empty assert connection.url assert connection_state.url is not Empty assert connection_state.base_url is Empty # type:ignore[unreachable] assert connection.base_url assert connection_state.base_url is not Empty assert connection_state.headers is Empty assert connection.headers is not None assert connection_state.headers is not Empty assert connection_state.parsed_query is Empty assert connection.query_params is not None assert connection_state.parsed_query is not Empty assert connection_state.cookies is Empty assert connection.cookies is not None assert connection_state.cookies is not Empty assert connection.client assert connection.user is user assert connection.auth is auth assert connection.session is session litestar-2.16.0/tests/unit/test_connection/test_connection_caching.py000066400000000000000000000166201500564371300262030ustar00rootroot00000000000000from __future__ import annotations from typing import Any, Awaitable, Callable from unittest.mock import ANY, MagicMock, call import pytest from litestar import Request, post from litestar.testing import RequestFactory from litestar.types import Empty, HTTPReceiveMessage, Scope from litestar.utils.scope.state import ScopeState async def test_multiple_request_object_data_caching(create_scope: Callable[..., Scope], mock: MagicMock) -> None: """Test that accessing the request data on multiple request objects only attempts to await `receive()` once. https://github.com/litestar-org/litestar/issues/2727 """ @post("/", request_max_body_size=None) async def handler() -> None: pass async def test_receive() -> HTTPReceiveMessage: mock() return {"type": "http.request", "body": b"abc", "more_body": False} scope = create_scope(route_handler=handler) request_1 = Request[Any, Any, Any](scope, test_receive) request_2 = Request[Any, Any, Any](scope, test_receive) assert (await request_1.body()) == b"abc" assert (await request_2.body()) == b"abc" assert mock.call_count == 1 @pytest.fixture(name="get_mock") def get_mock_fixture() -> MagicMock: return MagicMock() @pytest.fixture(name="set_mock") def set_mock_fixture() -> MagicMock: return MagicMock() @pytest.fixture(name="create_connection") def create_connection_fixture( get_mock: MagicMock, set_mock: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> Callable[..., Request]: class MockScopeState(ScopeState): def __getattribute__(self, key: str) -> Any: get_mock(key) return object.__getattribute__(self, key) def __setattr__(self, key: str, value: Any) -> None: set_mock(key, value) super().__setattr__(key, value) def create_connection(body_type: str = "json") -> Request: monkeypatch.setattr("litestar.connection.base.ScopeState", MockScopeState) connection = RequestFactory().get() async def fake_receive() -> HTTPReceiveMessage: if body_type == "msgpack": return {"type": "http.request", "body": b"\x81\xa3abc\xa3def", "more_body": False} return {"type": "http.request", "body": b'{"abc":"def"}', "more_body": False} monkeypatch.setattr(connection, "receive", fake_receive) return connection return create_connection @pytest.fixture(name="get_value") def get_value_fixture() -> Callable[[Request, str, bool], Awaitable[Any]]: """Fixture to get the value of a connection cached property. Returns: A function to get the value of a connection cached property. """ async def get_value_(connection: Request, prop_name: str, is_coro: bool) -> Any: """Helper to get the value of the tested cached property.""" value = getattr(connection, prop_name) return await value() if is_coro else value return get_value_ caching_tests = [ ("url", "url", "_url", False), ("base_url", "base_url", "_base_url", False), ("parsed_query", "query_params", "_parsed_query", False), ("cookies", "cookies", "_cookies", False), ("body", "body", "_body", True), ("form", "form", "_form", True), ("msgpack", "msgpack", "_msgpack", True), ("json", "json", "_json", True), ("accept", "accept", "_accept", False), ("content_type", "content_type", "_content_type", False), ] @pytest.mark.parametrize(("state_key", "prop_name", "cache_attr_name", "is_coro"), caching_tests) async def test_connection_cached_properties_no_scope_or_connection_caching( state_key: str, prop_name: str, cache_attr_name: str, is_coro: bool, create_connection: Callable[..., Request], get_mock: MagicMock, set_mock: MagicMock, get_value: Callable[[Request, str, bool], Awaitable[Any]], ) -> None: def check_get_mock() -> None: """Helper to check the get mock. For certain properties, we call `get_litestar_scope_state()` twice, once for the property and once for the body. For these cases, we check that the mock was called twice. """ if state_key in {"json", "msgpack"}: get_mock.assert_has_calls([call(state_key), call("body")]) elif state_key in {"accept", "cookies", "content_type"}: get_mock.assert_has_calls([call(state_key), call("headers")]) elif state_key == "form": get_mock.assert_has_calls([call(state_key), call("content_type")]) elif state_key == "body": get_mock.assert_has_calls([call(state_key), call("headers")]) else: get_mock.assert_called_once_with(state_key) def check_set_mock() -> None: """Helper to check the set mock. For certain properties, we call `set_litestar_scope_state()` twice, once for the property and once for the body. For these cases, we check that the mock was called twice. """ if state_key in {"json", "msgpack"}: set_mock.assert_has_calls([call("body", ANY), call(state_key, ANY)]) elif state_key == "form": set_mock.assert_has_calls([call("content_type", ANY), call(state_key, ANY)]) elif state_key in {"accept", "cookies", "content_type"}: set_mock.assert_has_calls([call("headers", ANY), call(state_key, ANY)]) elif state_key == "body": set_mock.assert_has_calls([call("headers", ANY), call(state_key, ANY)]) else: set_mock.assert_called_once_with(state_key, ANY) connection = create_connection("msgpack" if state_key == "msgpack" else "json") connection_state = connection._connection_state assert getattr(connection_state, state_key) is Empty setattr(connection, cache_attr_name, Empty) get_mock.reset_mock() set_mock.reset_mock() await get_value(connection, prop_name, is_coro) check_get_mock() check_set_mock() @pytest.mark.parametrize(("state_key", "prop_name", "cache_attr_name", "is_coro"), caching_tests) async def test_connection_cached_properties_cached_in_scope( state_key: str, prop_name: str, cache_attr_name: str, is_coro: bool, create_connection: Callable[..., Request], get_mock: MagicMock, set_mock: MagicMock, get_value: Callable[[Request, str, bool], Awaitable[Any]], ) -> None: # set the value in the scope and ensure empty on connection connection = create_connection() connection_state = ScopeState.from_scope(connection.scope) setattr(connection_state, state_key, {"not": "empty"}) setattr(connection, cache_attr_name, Empty) get_mock.reset_mock() set_mock.reset_mock() await get_value(connection, prop_name, is_coro) get_mock.assert_called_once_with(state_key) set_mock.assert_not_called() @pytest.mark.parametrize(("state_key", "prop_name", "cache_attr_name", "is_coro"), caching_tests) async def test_connection_cached_properties_cached_on_connection( state_key: str, prop_name: str, cache_attr_name: str, is_coro: bool, create_connection: Callable[..., Request], get_mock: MagicMock, set_mock: MagicMock, get_value: Callable[[Request, str, bool], Awaitable[Any]], ) -> None: connection = create_connection() # set the value on the connection setattr(connection, cache_attr_name, {"not": "empty"}) get_mock.reset_mock() set_mock.reset_mock() await get_value(connection, prop_name, is_coro) get_mock.assert_not_called() set_mock.assert_not_called() litestar-2.16.0/tests/unit/test_connection/test_request.py000066400000000000000000000551111500564371300240560ustar00rootroot00000000000000"""A large part of the tests in this file were adapted from: https://github.com/encode/starlette/blob/master/tests/test_requests.py. And are meant to ensure our compatibility with their API. """ from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable, Dict, Generator from unittest.mock import patch import pytest from litestar import MediaType, Request, get, post from litestar.connection.base import AuthT, StateT, UserT, empty_send from litestar.datastructures import Address, Cookie, State from litestar.exceptions import ( InternalServerException, LitestarException, LitestarWarning, SerializationException, ) from litestar.middleware import MiddlewareProtocol from litestar.response.base import ASGIResponse from litestar.serialization import encode_json, encode_msgpack from litestar.static_files.config import StaticFilesConfig from litestar.status_codes import HTTP_400_BAD_REQUEST, HTTP_413_REQUEST_ENTITY_TOO_LARGE from litestar.testing import TestClient, create_test_client if TYPE_CHECKING: from pathlib import Path from litestar.types import ASGIApp, Receive, Scope, Send @get("/", sync_to_thread=False, request_max_body_size=None) def _route_handler() -> None: pass @pytest.fixture(name="scope") def scope_fixture(create_scope: Callable[..., Scope]) -> Scope: return create_scope(type="http", route_handler=_route_handler) async def test_request_empty_body_to_json(anyio_backend: str, scope: Scope) -> None: with patch.object(Request, "body", return_value=b""): request_empty_payload: Request[Any, Any, State] = Request(scope=scope) request_json = await request_empty_payload.json() assert request_json is None async def test_request_invalid_body_to_json(anyio_backend: str, scope: Scope) -> None: with patch.object(Request, "body", return_value=b"invalid"), pytest.raises(SerializationException): request_empty_payload: Request[Any, Any, State] = Request(scope=scope) await request_empty_payload.json() async def test_request_valid_body_to_json(anyio_backend: str, scope: Scope) -> None: with patch.object(Request, "body", return_value=b'{"test": "valid"}'): request_empty_payload: Request[Any, Any, State] = Request(scope=scope) request_json = await request_empty_payload.json() assert request_json == {"test": "valid"} async def test_request_empty_body_to_msgpack(anyio_backend: str, scope: Scope) -> None: with patch.object(Request, "body", return_value=b""): request_empty_payload: Request[Any, Any, State] = Request(scope=scope) request_msgpack = await request_empty_payload.msgpack() assert request_msgpack is None async def test_request_invalid_body_to_msgpack(anyio_backend: str, scope: Scope) -> None: with patch.object(Request, "body", return_value=b"invalid"), pytest.raises(SerializationException): request_empty_payload: Request[Any, Any, State] = Request(scope=scope) await request_empty_payload.msgpack() async def test_request_valid_body_to_msgpack(anyio_backend: str, scope: Scope) -> None: with patch.object(Request, "body", return_value=encode_msgpack({"test": "valid"})): request_empty_payload: Request[Any, Any, State] = Request(scope=scope) request_msgpack = await request_empty_payload.msgpack() assert request_msgpack == {"test": "valid"} def test_request_url_for() -> None: @get(path="/proxy", name="proxy") def proxy() -> None: pass @get(path="/test", signature_namespace={"dict": Dict}) def root(request: Request[Any, Any, State]) -> dict[str, str]: return {"url": request.url_for("proxy")} @get(path="/test-none", signature_namespace={"dict": Dict}) def test_none(request: Request[Any, Any, State]) -> dict[str, str]: return {"url": request.url_for("none")} with create_test_client(route_handlers=[proxy, root, test_none]) as client: response = client.get("/test") assert response.json() == {"url": "http://testserver.local/proxy"} response = client.get("/test-none") assert response.status_code == 500 def test_request_asset_url(tmp_path: Path) -> None: @get(path="/resolver", signature_namespace={"dict": Dict}) def resolver(request: Request[Any, Any, State]) -> dict[str, str]: return {"url": request.url_for_static_asset("js", "main.js")} @get(path="/resolver-none", signature_namespace={"dict": Dict}) def resolver_none(request: Request[Any, Any, State]) -> dict[str, str]: return {"url": request.url_for_static_asset("none", "main.js")} with create_test_client( route_handlers=[resolver, resolver_none], static_files_config=[StaticFilesConfig(path="/static/js", directories=[tmp_path], name="js")], ) as client: response = client.get("/resolver") assert response.json() == {"url": "http://testserver.local/static/js/main.js"} response = client.get("/resolver-none") assert response.status_code == 500 def test_route_handler_property() -> None: value: Any = {} @get("/") def handler(request: Request[Any, Any, State]) -> None: value["handler"] = request.route_handler with create_test_client(route_handlers=[handler]) as client: client.get("/") assert str(value["handler"]) == str(handler) def test_custom_request_class() -> None: value: Any = {} class MyRequest(Request[UserT, AuthT, StateT]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.scope["called"] = True # type: ignore[typeddict-unknown-key] @get("/", signature_types=[MyRequest]) def handler(request: MyRequest[Any, Any, State]) -> None: value["called"] = request.scope.get("called") with create_test_client(route_handlers=[handler], request_class=MyRequest) as client: client.get("/") assert value["called"] def test_request_url() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: request = Request[Any, Any, State](scope, receive) data = {"method": request.method, "url": str(request.url)} response = ASGIResponse(body=encode_json(data)) await response(scope, receive, send) client = TestClient(app) response = client.get("/123?a=abc") assert response.json() == {"method": "GET", "url": "http://testserver.local/123?a=abc"} response = client.get("https://example.org:123/") assert response.json() == {"method": "GET", "url": "https://example.org:123/"} def test_request_query_params() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: request = Request[Any, Any, State](scope, receive) params = dict(request.query_params) response = ASGIResponse(body=encode_json({"params": params})) await response(scope, receive, send) client = TestClient(app) response = client.get("/?a=123&b=456") assert response.json() == {"params": {"a": "123", "b": "456"}} def test_request_headers() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: request = Request[Any, Any, State](scope, receive) headers = dict(request.headers) response = ASGIResponse(body=encode_json({"headers": headers})) await response(scope, receive, send) client = TestClient(app) response = client.get("/", headers={"host": "example.org"}) assert response.json() == { "headers": { "host": "example.org", "user-agent": "testclient", "accept-encoding": "gzip, deflate, br", "accept": "*/*", "connection": "keep-alive", } } def test_request_accept_header() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: request = Request[Any, Any, State](scope, receive) response = ASGIResponse(body=encode_json({"accepted_types": list(request.accept)})) await response(scope, receive, send) client = TestClient(app) response = client.get("/", headers={"Accept": "text/plain, application/xml;q=0.7, text/html;p=test"}) assert response.json() == {"accepted_types": ["text/html;p=test", "text/plain", "application/xml;q=0.7"]} @pytest.mark.parametrize( "scope_values,expected_client", ( ({"type": "http", "route_handler": _route_handler, "client": ["client", 42]}, Address("client", 42)), ({"type": "http", "route_handler": _route_handler, "client": None}, None), ({"type": "http", "route_handler": _route_handler}, None), ), ) def test_request_client( scope_values: dict[str, Any], expected_client: Address | None, create_scope: Callable[..., Scope] ) -> None: scope = create_scope() scope.update(scope_values) # type: ignore[typeddict-item] if "client" not in scope_values: del scope["client"] # type: ignore[misc] client = Request[Any, Any, State](scope).client assert client == expected_client def test_request_body() -> None: @post("/") async def handler(request: Request) -> bytes: body = await request.body() return encode_json({"body": body.decode()}) with create_test_client([handler]) as client: response = client.post("/") assert response.json() == {"body": ""} response = client.post("/", json={"a": "123"}) assert response.json() == {"body": '{"a":"123"}'} response = client.post("/", content="abc") assert response.json() == {"body": "abc"} def test_request_stream() -> None: @post("/") async def handler(request: Request) -> bytes: body = b"" async for chunk in request.stream(): body += chunk return encode_json({"body": body.decode()}) with create_test_client([handler]) as client: response = client.post("/") assert response.json() == {"body": ""} response = client.post("/", json={"a": "123"}) assert response.json() == {"body": '{"a":"123"}'} response = client.post("/", content="abc") assert response.json() == {"body": "abc"} def test_request_form_urlencoded() -> None: @post("/") async def handler(request: Request) -> bytes: form = await request.form() return encode_json({"form": dict(form)}) with create_test_client([handler]) as client: response = client.post("/", data={"abc": "123 @"}) assert response.json() == {"form": {"abc": "123 @"}} def test_request_form_urlencoded_multi_keys() -> None: @post("/") async def handler(request: Request) -> Any: return (await request.form()).getall("foo") with create_test_client(handler) as client: assert client.post("/", data={"foo": ["1", "2"]}).json() == ["1", "2"] def test_request_form_multipart_multi_keys() -> None: @post("/") async def handler(request: Request) -> int: return len((await request.form()).getall("foo")) with create_test_client(handler) as client: assert client.post("/", data={"foo": "1"}, files={"foo": b"a"}).json() == 2 def test_request_body_then_stream() -> None: @post("/") async def handler(request: Request) -> bytes: body = await request.body() chunks = b"" async for chunk in request.stream(): chunks += chunk return encode_json({"body": body.decode(), "stream": chunks.decode()}) with create_test_client([handler]) as client: response = client.post("/", content="abc") assert response.json() == {"body": "abc", "stream": "abc"} def test_request_stream_then_body() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: request = Request[Any, Any, State](scope, receive) chunks = b"" async for chunk in request.stream(): chunks += chunk try: body = await request.body() except InternalServerException: body = b"" response = ASGIResponse(body=encode_json({"body": body.decode(), "stream": chunks.decode()})) await response(scope, receive, send) @post("/") async def handler(request: Request) -> bytes: chunks = b"" async for chunk in request.stream(): chunks += chunk try: body = await request.body() except InternalServerException: body = b"" return encode_json({"body": body.decode(), "stream": chunks.decode()}) with create_test_client([handler]) as client: response = client.post("/", content="abc") assert response.json() == {"body": "", "stream": "abc"} def test_request_json() -> None: @post("/") async def handler(request: Request) -> bytes: data = await request.json() return encode_json({"json": data}) with create_test_client(handler) as client: response = client.post("/", json={"a": "123"}) assert response.json() == {"json": {"a": "123"}} def test_request_raw_path() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: request = Request[Any, Any, State](scope, receive) path = str(request.scope["path"]) raw_path = str(request.scope["raw_path"]) response = ASGIResponse(body=f"{path}, {raw_path}".encode(), media_type=MediaType.TEXT) await response(scope, receive, send) client = TestClient(app) response = client.get("/he%2Fllo") assert response.text == "/he/llo, b'/he%2Fllo'" def test_request_without_setting_receive(create_scope: Callable[..., Scope]) -> None: """If Request is instantiated without the 'receive' channel, then .body() is not available.""" async def app(scope: Scope, receive: Receive, send: Send) -> None: scope.update(create_scope(route_handler=_route_handler)) # type: ignore[typeddict-item] request = Request[Any, Any, State](scope) try: data = await request.json() except RuntimeError: data = "Receive channel not available" response = ASGIResponse(body=encode_json({"json": data})) await response(scope, receive, send) client = TestClient(app) response = client.post("/", json={"a": "123"}) assert response.json() == {"json": "Receive channel not available"} async def test_request_disconnect(create_scope: Callable[..., Scope]) -> None: """If a client disconnect occurs while reading request body then InternalServerException should be raised.""" async def app(scope: Scope, receive: Receive, send: Send) -> None: request = Request[Any, Any, State](scope, receive) await request.body() async def receiver() -> dict[str, str]: return {"type": "http.disconnect"} with pytest.raises(InternalServerException): await app( create_scope(type="http", route_handler=_route_handler, method="POST", path="/"), receiver, # type: ignore[arg-type] empty_send, ) def test_request_state() -> None: @get("/", signature_namespace={"dict": Dict}) def handler(request: Request[Any, Any, State]) -> dict[Any, Any]: request.state.test = 1 assert request.state.test == 1 return request.state.dict() with create_test_client(handler) as client: response = client.get("/") assert response.json()["test"] == 1 def test_request_cookies() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: request = Request[Any, Any, State](scope, receive) mycookie = request.cookies.get("mycookie") if mycookie: asgi_response = ASGIResponse(body=mycookie.encode("utf-8"), media_type="text/plain") else: asgi_response = ASGIResponse( body=b"Hello, world!", media_type="text/plain", cookies=[Cookie(key="mycookie", value="Hello, cookies!")], ) await asgi_response(scope, receive, send) client = TestClient(app) response = client.get("/") assert response.text == "Hello, world!" response = client.get("/") assert response.text == "Hello, cookies!" def test_chunked_encoding() -> None: @post("/") async def handler(request: Request) -> bytes: body = await request.body() return encode_json({"body": body.decode()}) with create_test_client([handler]) as client: def post_body() -> Generator[bytes, None, None]: yield b"foo" yield b"bar" response = client.post("/", content=post_body()) assert response.json() == {"body": "foobar"} def test_request_send_push_promise() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: # the server is push-enabled scope["extensions"]["http.response.push"] = {} # type: ignore[index] request = Request[Any, Any, State](scope, receive, send) await request.send_push_promise("/style.css") response = ASGIResponse(body=encode_json({"json": "OK"})) await response(scope, receive, send) client = TestClient(app) response = client.get("/") assert response.json() == {"json": "OK"} def test_request_send_push_promise_without_push_extension() -> None: """If server does not support the `http.response.push` extension, .send_push_promise() does nothing. """ async def app(scope: Scope, receive: Receive, send: Send) -> None: request = Request[Any, Any, State](scope) with pytest.warns(LitestarWarning, match="Attempted to send a push promise"): await request.send_push_promise("/style.css") response = ASGIResponse(body=encode_json({"json": "OK"})) await response(scope, receive, send) client = TestClient(app) response = client.get("/") assert response.json() == {"json": "OK"} def test_request_send_push_promise_without_push_extension_raises() -> None: """If server does not support the `http.response.push` extension, .send_push_promise() does nothing. """ async def app(scope: Scope, receive: Receive, send: Send) -> None: request = Request[Any, Any, State](scope) with pytest.raises(LitestarException, match="Attempted to send a push promise"): await request.send_push_promise("/style.css", raise_if_unavailable=True) response = ASGIResponse(body=encode_json({"json": "OK"})) await response(scope, receive, send) TestClient(app).get("/") def test_request_send_push_promise_without_setting_send() -> None: """If Request is instantiated without the send channel, then. .send_push_promise() is not available. """ async def app(scope: Scope, receive: Receive, send: Send) -> None: # the server is push-enabled scope["extensions"]["http.response.push"] = {} # type: ignore[index] data = "OK" request = Request[Any, Any, State](scope) try: await request.send_push_promise("/style.css") except RuntimeError: data = "Send channel not available" response = ASGIResponse(body=encode_json({"json": data})) await response(scope, receive, send) client = TestClient(app) response = client.get("/") assert response.json() == {"json": "Send channel not available"} class BeforeRequestMiddleWare(MiddlewareProtocol): def __init__(self, app: ASGIApp) -> None: self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: scope["state"]["main"] = 1 await self.app(scope, receive, send) def test_state() -> None: def before_request(request: Request[Any, Any, State]) -> None: assert request.state.main == 1 request.state.main = 2 @get(path="/", signature_namespace={"dict": Dict}) async def get_state(request: Request[Any, Any, State]) -> dict[str, str]: return {"state": request.state.main} with create_test_client( route_handlers=[get_state], middleware=[BeforeRequestMiddleWare], before_request=before_request ) as client: response = client.get("/") assert response.json() == {"state": 2} def test_request_body_exceeds_content_length() -> None: @post("/") def handler(body: bytes) -> None: pass with create_test_client([handler]) as client: response = client.post("/", headers={"content-length": "1"}, content=b"ab") assert response.status_code == HTTP_400_BAD_REQUEST assert response.json() == {"status_code": 400, "detail": "Malformed request"} def test_request_body_exceeds_max_request_body_size() -> None: @post("/one", request_max_body_size=1) async def handler_one(request: Request) -> None: await request.body() @post("/two", request_max_body_size=1) async def handler_two(body: bytes) -> None: pass with create_test_client([handler_one, handler_two]) as client: response = client.post("/one", headers={"content-length": "2"}, content=b"ab") assert response.status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE response = client.post("/two", headers={"content-length": "2"}, content=b"ab") assert response.status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE def test_request_body_exceeds_max_request_body_size_chunked() -> None: @post("/one", request_max_body_size=1) async def handler_one(request: Request) -> None: assert request.headers["transfer-encoding"] == "chunked" await request.body() @post("/two", request_max_body_size=1) async def handler_two(body: bytes, request: Request) -> None: assert request.headers["transfer-encoding"] == "chunked" await request.body() def generator() -> Generator[bytes, None, None]: yield b"1" yield b"2" with create_test_client([handler_one, handler_two]) as client: response = client.post("/one", content=generator()) assert response.status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE response = client.post("/two", content=generator()) assert response.status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE def test_request_content_length() -> None: @post("/") def handler(request: Request) -> dict: return {"content-length": request.content_length} with create_test_client([handler]) as client: assert client.post("/", content=b"1").json() == {"content-length": 1} def test_request_invalid_content_length() -> None: @post("/") def handler(request: Request) -> dict: return {"content-length": request.content_length} with create_test_client([handler]) as client: response = client.post("/", content=b"1", headers={"content-length": "a"}) assert response.status_code == HTTP_400_BAD_REQUEST assert response.json() == {"detail": "Invalid content-length: 'a'", "status_code": 400} litestar-2.16.0/tests/unit/test_connection/test_websocket.py000066400000000000000000000367351500564371300243670ustar00rootroot00000000000000""" Some tests in this file were adapted from: https://github.com/encode/starlette/blob/master/tests/test_websockets.py And were meant to ensure our compatibility with their API. """ from __future__ import annotations from typing import TYPE_CHECKING, Any, AsyncGenerator, Literal from unittest.mock import MagicMock import anyio import pytest from litestar.connection import WebSocket from litestar.datastructures import State from litestar.datastructures.headers import Headers from litestar.exceptions import WebSocketDisconnect, WebSocketException from litestar.handlers.websocket_handlers import websocket from litestar.status_codes import WS_1001_GOING_AWAY from litestar.testing import TestClient, create_test_client from litestar.types.asgi_types import WebSocketMode from litestar.utils.compat import async_next if TYPE_CHECKING: from litestar.types import Receive, Scope, Send @pytest.mark.parametrize("mode", ["text", "binary"]) def test_websocket_send_receive_json(mode: Literal["text", "binary"]) -> None: @websocket(path="/") async def websocket_handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() recv = await socket.receive_json(mode=mode) await socket.send_json({"message": recv}, mode=mode) await socket.close() with create_test_client(route_handlers=[websocket_handler]).websocket_connect("/") as ws: ws.send_json({"hello": "world"}, mode=mode) data = ws.receive_json(mode=mode) assert data == {"message": {"hello": "world"}} def test_route_handler_property() -> None: value: Any = {} @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() value["handler"] = socket.route_handler await socket.close() with create_test_client(route_handlers=[handler]).websocket_connect("/"): assert str(value["handler"]) == str(handler) @pytest.mark.parametrize( "headers", [[(b"test", b"hello-world")], {"test": "hello-world"}, Headers(headers={"test": "hello-world"})] ) async def test_accept_set_headers(headers: Any) -> None: @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept(headers=headers) await socket.send_text("abc") await socket.close() with create_test_client(route_handlers=[handler]).websocket_connect("/") as ws: assert dict(ws.scope["headers"])[b"test"] == b"hello-world" async def test_custom_request_class() -> None: value: Any = {} class MyWebSocket(WebSocket[Any, Any, State]): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.scope["called"] = True # type: ignore[typeddict-unknown-key] @websocket("/", signature_types=[MyWebSocket]) async def handler(socket: MyWebSocket) -> None: value["called"] = socket.scope.get("called") await socket.accept() await socket.close() with create_test_client(route_handlers=[handler], websocket_class=MyWebSocket).websocket_connect("/"): assert value["called"] def test_websocket_url() -> None: @websocket("/123") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() await socket.send_json({"url": str(socket.url)}) await socket.close() with create_test_client(handler).websocket_connect("/123?a=abc") as ws: assert ws.receive_json() == {"url": "ws://testserver.local/123?a=abc"} def test_websocket_url_respects_custom_base_url() -> None: @websocket("/123") async def handler(socket: WebSocket) -> None: await socket.accept() await socket.send_json({"url": str(socket.url)}) await socket.close() with create_test_client(handler, base_url="http://example.org").websocket_connect("/123?a=abc") as ws: assert ws.receive_json() == {"url": "ws://example.org/123?a=abc"} def test_websocket_binary_json() -> None: @websocket("/123") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() message = await socket.receive_json(mode="binary") await socket.send_json(message, mode="binary") await socket.close() with create_test_client(handler).websocket_connect("/123?a=abc") as ws: ws.send_json({"test": "data"}, mode="binary") assert ws.receive_json(mode="binary") == {"test": "data"} def test_websocket_query_params() -> None: @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: query_params = dict(socket.query_params) await socket.accept() await socket.send_json({"params": query_params}) await socket.close() with create_test_client(handler).websocket_connect("/?a=abc&b=456") as ws: assert ws.receive_json() == {"params": {"a": "abc", "b": "456"}} def test_websocket_headers() -> None: @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: headers = dict(socket.headers) await socket.accept() await socket.send_json({"headers": headers}) await socket.close() with create_test_client(handler).websocket_connect("/") as ws: expected_headers = { "accept": "*/*", "accept-encoding": "gzip, deflate, br", "connection": "upgrade", "host": "testserver.local", "user-agent": "testclient", "sec-websocket-key": "testserver==", "sec-websocket-version": "13", } assert ws.receive_json() == {"headers": expected_headers} def test_websocket_port() -> None: @websocket("/123") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() await socket.send_json({"port": socket.url.port}) await socket.close() with create_test_client(handler).websocket_connect("ws://example.com:123/123?a=abc") as ws: assert ws.receive_json() == {"port": 123} def test_websocket_send_and_receive_text() -> None: @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() data = await socket.receive_text() await socket.send_text(f"Message was: {data}") await socket.close() with create_test_client(handler).websocket_connect("/") as ws: ws.send_text("Hello, world!") assert ws.receive_text() == "Message was: Hello, world!" def test_websocket_send_and_receive_bytes() -> None: @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() data = await socket.receive_bytes() await socket.send_bytes(b"Message was: " + data) await socket.close() with create_test_client(handler).websocket_connect("/") as ws: ws.send_bytes(b"Hello, world!") assert ws.receive_bytes() == b"Message was: Hello, world!" def test_websocket_send_and_receive_json() -> None: @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() data = await socket.receive_json() await socket.send_json({"message": data}) await socket.close() with create_test_client(handler).websocket_connect("/") as ws: ws.send_json({"hello": "world"}) assert ws.receive_json() == {"message": {"hello": "world"}} def test_send_msgpack() -> None: test_data = {"message": "hello, world"} @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() await socket.send_msgpack(test_data) await socket.close() with create_test_client(handler).websocket_connect("/") as ws: data = ws.receive_msgpack(timeout=1) assert data == test_data def test_receive_msgpack() -> None: test_data = {"message": "hello, world"} callback = MagicMock() @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() data = await socket.receive_msgpack() callback(data) await socket.close() with create_test_client(handler).websocket_connect("/") as ws: ws.send_msgpack(test_data) callback.assert_called_once_with(test_data) async def consume_gen(generator: AsyncGenerator[Any, Any], count: int, timeout: int = 1) -> list[Any]: async def consumer() -> list[Any]: result = [] for _ in range(count): result.append(await async_next(generator)) return result with anyio.fail_after(timeout): return await consumer() @pytest.mark.parametrize("mode,data", [("text", ["foo", "bar"]), ("binary", [b"foo", b"bar"])]) def test_iter_data(mode: WebSocketMode, data: list[str | bytes]) -> None: values = [] @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() values.extend(await consume_gen(socket.iter_data(mode=mode), 2)) await socket.close() with create_test_client(handler).websocket_connect("/") as ws: for message in data: ws.send(message, mode=mode) assert values == data @pytest.mark.parametrize("mode", ["text", "binary"]) def test_iter_json(mode: WebSocketMode) -> None: messages = [{"data": "foo"}, {"data": "bar"}] values = [] @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() values.extend(await consume_gen(socket.iter_json(mode=mode), 2)) await socket.close() with create_test_client(handler).websocket_connect("/") as ws: for message in messages: ws.send_json(message, mode=mode) assert values == messages def test_iter_msgpack() -> None: messages = [{"data": "foo"}, {"data": "bar"}] values = [] @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() values.extend(await consume_gen(socket.iter_msgpack(), 2)) await socket.close() with create_test_client(handler).websocket_connect("/") as ws: for message in messages: ws.send_msgpack(message) assert values == messages def test_websocket_concurrency_pattern() -> None: stream_send, stream_receive = anyio.create_memory_object_stream() # type: ignore[var-annotated] async def reader(socket: WebSocket[Any, Any, State]) -> None: async with stream_send: json_data = await socket.receive_json() await stream_send.send(json_data) async def writer(socket: WebSocket[Any, Any, State]) -> None: async with stream_receive: async for message in stream_receive: await socket.send_json(message) @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() async with anyio.create_task_group() as task_group: task_group.start_soon(reader, socket) await writer(socket) await socket.close() with create_test_client(handler).websocket_connect("/") as ws: ws.send_json({"hello": "world"}) data = ws.receive_json() assert data == {"hello": "world"} def test_client_close() -> None: close_code = None @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: nonlocal close_code await socket.accept() try: await socket.receive_text() except WebSocketException as exc: close_code = exc.code with create_test_client(handler).websocket_connect("/") as ws: ws.close(code=WS_1001_GOING_AWAY) assert close_code == WS_1001_GOING_AWAY def test_application_close() -> None: @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() await socket.close(WS_1001_GOING_AWAY) with create_test_client(handler).websocket_connect("/") as ws, pytest.raises(WebSocketDisconnect) as exc: ws.receive_text() assert exc.value.code == WS_1001_GOING_AWAY def test_rejected_connection() -> None: @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.close(WS_1001_GOING_AWAY) with pytest.raises(WebSocketDisconnect) as exc, create_test_client(handler).websocket_connect("/"): pass assert exc.value.code == WS_1001_GOING_AWAY def test_subprotocol() -> None: @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: assert socket.scope["subprotocols"] == ["soap", "wamp"] await socket.accept(subprotocols="wamp") await socket.close() with create_test_client(handler).websocket_connect("/", subprotocols=["soap", "wamp"]) as ws: assert ws.accepted_subprotocol == "wamp" def test_additional_headers() -> None: @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept(headers=[(b"additional", b"header")]) await socket.close() with create_test_client(handler).websocket_connect("/") as ws: assert ws.extra_headers == [(b"additional", b"header")] def test_no_additional_headers() -> None: @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() await socket.close() with create_test_client(handler).websocket_connect("/") as ws: assert ws.extra_headers == [] def test_websocket_exception() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: raise RuntimeError with pytest.raises(RuntimeError), TestClient(app).websocket_connect("/123?a=abc"): pass def test_duplicate_disconnect() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: socket = WebSocket[Any, Any, State](scope, receive=receive, send=send) await socket.accept() message = await socket.receive() assert message["type"] == "websocket.disconnect" await socket.receive() with pytest.raises(WebSocketException), TestClient(app).websocket_connect("/") as websocket: websocket.close() def test_websocket_close_reason() -> None: @websocket("/") async def handler(socket: WebSocket[Any, Any, State]) -> None: await socket.accept() await socket.close(code=WS_1001_GOING_AWAY, reason="Going Away") with create_test_client(handler).websocket_connect("/") as ws, pytest.raises(WebSocketDisconnect) as exc: ws.receive_text() assert exc.value.code == WS_1001_GOING_AWAY assert exc.value.detail == "Going Away" def test_receive_text_before_accept() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: socket = WebSocket[Any, Any, State](scope, receive=receive, send=send) await socket.receive_text() with pytest.raises(WebSocketException), TestClient(app).websocket_connect("/"): pass def test_receive_bytes_before_accept() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: socket = WebSocket[Any, Any, State](scope, receive=receive, send=send) await socket.receive_bytes() with pytest.raises(WebSocketException), TestClient(app).websocket_connect("/"): pass def test_receive_json_before_accept() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: socket = WebSocket[Any, Any, State](scope, receive=receive, send=send) await socket.receive_json() with pytest.raises(WebSocketException), TestClient(app).websocket_connect("/"): pass litestar-2.16.0/tests/unit/test_contrib/000077500000000000000000000000001500564371300202535ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_contrib/__init__.py000066400000000000000000000000001500564371300223520ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_contrib/conftest.py000066400000000000000000000003261500564371300224530ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from typing import Callable @pytest.fixture def int_factory() -> Callable[[], int]: return lambda: 2 litestar-2.16.0/tests/unit/test_contrib/test_attrs.py000066400000000000000000000031511500564371300230210ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations import sys import warnings from importlib.util import cache_from_source from pathlib import Path import pytest from litestar.contrib import attrs as contrib_attrs from litestar.plugins import attrs as plugin_attrs def purge_module(module_names: list[str], path: str | Path) -> None: for name in module_names: if name in sys.modules: del sys.modules[name] Path(cache_from_source(str(path))).unlink(missing_ok=True) def test_contrib_attrs_deprecation_warning() -> None: """Test that importing from contrib.attrs raises a deprecation warning.""" purge_module(["litestar.contrib.attrs"], __file__) with pytest.warns( DeprecationWarning, match="importing AttrsSchemaPlugin from 'litestar.contrib.attrs' is deprecated" ): from litestar.contrib.attrs import AttrsSchemaPlugin def test_contrib_attrs_schema_deprecation_warning() -> None: """Test that importing from contrib.attrs raises a deprecation warning.""" purge_module(["litestar.contrib.attrs.attrs_schema_plugin"], __file__) with pytest.warns( DeprecationWarning, match="importing AttrsSchemaPlugin from 'litestar.contrib.attrs.attrs_schema_plugin' is deprecated", ): from litestar.contrib.attrs.attrs_schema_plugin import AttrsSchemaPlugin def test_functionality_parity() -> None: """Test that the functionality is identical between contrib and plugin versions.""" assert contrib_attrs.AttrsSchemaPlugin is plugin_attrs.AttrsSchemaPlugin assert contrib_attrs.is_attrs_class is plugin_attrs.is_attrs_class litestar-2.16.0/tests/unit/test_contrib/test_htmx/000077500000000000000000000000001500564371300222725ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_contrib/test_htmx/__init__.py000066400000000000000000000000001500564371300243710ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_contrib/test_htmx/test_htmx_deprecations.py000066400000000000000000000106501500564371300274250ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # pyright: reportUnusedImport=false import importlib import sys from pathlib import Path from typing import List, Union import pytest def purge_module(module_names: List[str], path: Union[str, Path]) -> None: for name in module_names: if name in sys.modules: del sys.modules[name] Path(importlib.util.cache_from_source(path)).unlink(missing_ok=True) # type: ignore[arg-type] def test_deprecated_htmx_request() -> None: purge_module(["litestar.contrib.htmx.request"], __file__) with pytest.warns( DeprecationWarning, match="importing HTMXDetails from 'litestar.contrib.htmx.request' is deprecated" ): from litestar.contrib.htmx.request import HTMXDetails purge_module(["litestar.contrib.htmx.request"], __file__) with pytest.warns( DeprecationWarning, match="importing HTMXDetails from 'litestar.contrib.htmx.request' is deprecated" ): from litestar.contrib.htmx.request import HTMXDetails def test_deprecated_htmx_response() -> None: purge_module(["litestar.contrib.htmx.response"], __file__) with pytest.warns( DeprecationWarning, match="importing HTMXTemplate from 'litestar.contrib.htmx.response' is deprecated" ): from litestar.contrib.htmx.response import HTMXTemplate purge_module(["litestar.contrib.htmx.request"], __file__) with pytest.warns( DeprecationWarning, match="importing HXLocation from 'litestar.contrib.htmx.response' is deprecated" ): from litestar.contrib.htmx.response import HXLocation purge_module(["litestar.contrib.htmx.request"], __file__) with pytest.warns( DeprecationWarning, match="importing HXStopPolling from 'litestar.contrib.htmx.response' is deprecated" ): from litestar.contrib.htmx.response import HXStopPolling purge_module(["litestar.contrib.htmx.request"], __file__) with pytest.warns( DeprecationWarning, match="importing ClientRedirect from 'litestar.contrib.htmx.response' is deprecated" ): from litestar.contrib.htmx.response import ClientRedirect purge_module(["litestar.contrib.htmx.request"], __file__) with pytest.warns( DeprecationWarning, match="importing ClientRefresh from 'litestar.contrib.htmx.response' is deprecated" ): from litestar.contrib.htmx.response import ClientRefresh purge_module(["litestar.contrib.htmx.request"], __file__) with pytest.warns( DeprecationWarning, match="importing PushUrl from 'litestar.contrib.htmx.response' is deprecated" ): from litestar.contrib.htmx.response import PushUrl purge_module(["litestar.contrib.htmx.request"], __file__) with pytest.warns( DeprecationWarning, match="importing ReplaceUrl from 'litestar.contrib.htmx.response' is deprecated" ): from litestar.contrib.htmx.response import ReplaceUrl purge_module(["litestar.contrib.htmx.request"], __file__) with pytest.warns(DeprecationWarning, match="importing Reswap from 'litestar.contrib.htmx.response' is deprecated"): from litestar.contrib.htmx.response import Reswap purge_module(["litestar.contrib.htmx.request"], __file__) with pytest.warns( DeprecationWarning, match="importing Retarget from 'litestar.contrib.htmx.response' is deprecated" ): from litestar.contrib.htmx.response import Retarget purge_module(["litestar.contrib.htmx.request"], __file__) with pytest.warns( DeprecationWarning, match="importing TriggerEvent from 'litestar.contrib.htmx.response' is deprecated" ): from litestar.contrib.htmx.response import TriggerEvent def test_deprecated_htmx_types() -> None: purge_module(["litestar.contrib.htmx.types"], __file__) with pytest.warns( DeprecationWarning, match="importing HtmxHeaderType from 'litestar.contrib.htmx.types' is deprecated" ): from litestar.contrib.htmx.types import HtmxHeaderType purge_module(["litestar.contrib.htmx.types"], __file__) with pytest.warns( DeprecationWarning, match="importing TriggerEventType from 'litestar.contrib.htmx.types' is deprecated" ): from litestar.contrib.htmx.types import TriggerEventType purge_module(["litestar.contrib.htmx.request"], __file__) with pytest.warns( DeprecationWarning, match="importing LocationType from 'litestar.contrib.htmx.types' is deprecated" ): from litestar.contrib.htmx.types import LocationType litestar-2.16.0/tests/unit/test_contrib/test_htmx/test_htmx_request.py000066400000000000000000000242701500564371300264400ustar00rootroot00000000000000from typing import Any, Optional from litestar import MediaType, get from litestar.contrib.htmx.request import HTMXRequest from litestar.plugins.htmx import HTMXHeaders from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client def test_health_check() -> None: @get("/health-check", media_type=MediaType.TEXT) def health_check() -> str: return "healthy" with create_test_client(route_handlers=health_check) as client: response = client.get("/health-check") assert response.status_code == HTTP_200_OK assert response.text == "healthy" async def test_bool_default() -> None: @get("/") def handler(request: HTMXRequest) -> bool: return bool(request.htmx) with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.text == "false" async def test_bool_false() -> None: @get("/") def handler(request: HTMXRequest) -> bool: return bool(request.htmx) with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/", headers={HTMXHeaders.REQUEST.value: "false"}) assert response.text == "false" async def test_bool_true() -> None: @get("/") def handler(request: HTMXRequest) -> bool: return bool(request.htmx) with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/", headers={HTMXHeaders.REQUEST.value: "true"}) assert response.text == "true" async def test_boosted_default() -> None: @get("/") def handler(request: HTMXRequest) -> bool: return request.htmx.boosted with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.text == "false" async def test_boosted_set() -> None: @get("/") def handler(request: HTMXRequest) -> bool: return request.htmx.boosted with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/", headers={HTMXHeaders.BOOSTED.value: "true"}) assert response.text == "true" def test_current_url_default() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: assert request.htmx.current_url is None return request.htmx.current_url with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.text == "null" def test_current_url_set() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: assert request.htmx.current_url == "https://example.com" return request.htmx.current_url with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/", headers={HTMXHeaders.CURRENT_URL.value: "https://example.com"}) assert response.text == "https://example.com" def test_current_url_set_url_encoded() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: return request.htmx.current_url with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get( "/", headers={ HTMXHeaders.CURRENT_URL.value: "https%3A%2F%2Fexample.com%2F%3F", f"{HTMXHeaders.CURRENT_URL.value}-URI-AutoEncoded": "true", }, ) assert response.text == "https://example.com/?" def test_current_url_abs_path_default() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: assert request.htmx.current_url_abs_path is None return request.htmx.current_url_abs_path with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.text == "null" def test_current_url_abs_path_set() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: assert request.htmx.current_url_abs_path == "/duck/?quack=true#h2" return request.htmx.current_url_abs_path with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get( "/", headers={HTMXHeaders.CURRENT_URL.value: "http://testserver.local/duck/?quack=true#h2"} ) assert response.text == "/duck/?quack=true#h2" def test_current_url_abs_path_set_other_domain() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: assert request.htmx.current_url_abs_path is None return request.htmx.current_url_abs_path with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/", headers={HTMXHeaders.CURRENT_URL.value: "http://example.com/duck/?quack=true#h2"}) assert response.text == "null" def test_history_restore_request_false() -> None: @get("/") def handler(request: HTMXRequest) -> bool: assert request.htmx.history_restore_request is False return request.htmx.history_restore_request with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/", headers={HTMXHeaders.HISTORY_RESTORE_REQUEST.value: "false"}) assert response.text == "false" def test_history_restore_request_true() -> None: @get("/") def handler(request: HTMXRequest) -> bool: assert request.htmx.history_restore_request is True return request.htmx.history_restore_request with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/", headers={HTMXHeaders.HISTORY_RESTORE_REQUEST.value: "true"}) assert response.text == "true" def test_prompt_default() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: assert request.htmx.prompt is None return request.htmx.prompt with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.text == "null" def test_prompt_set() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: assert request.htmx.prompt == "Yes" return request.htmx.prompt with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/", headers={HTMXHeaders.PROMPT.value: "Yes"}) assert response.text == "Yes" def test_target_default() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: assert request.htmx.target is None return request.htmx.target with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.text == "null" def test_target_set() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: assert request.htmx.target == "#element" return request.htmx.target with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/", headers={HTMXHeaders.TARGET.value: "#element"}) assert response.text == "#element" def test_trigger_default() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: assert request.htmx.trigger is None return request.htmx.trigger with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.text == "null" def test_trigger_set() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: assert request.htmx.trigger == "#element" return request.htmx.trigger with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/", headers={HTMXHeaders.TRIGGER_ID.value: "#element"}) assert response.text == "#element" def test_trigger_name_default() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: assert request.htmx.trigger_name is None return request.htmx.trigger_name with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.text == "null" def test_trigger_name_set() -> None: @get("/") def handler(request: HTMXRequest) -> Optional[str]: assert request.htmx.trigger_name == "name_of_element" return request.htmx.trigger_name with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/", headers={HTMXHeaders.TRIGGER_NAME.value: "name_of_element"}) assert response.text == "name_of_element" def test_triggering_event_none() -> None: @get("/") def handler(request: HTMXRequest) -> None: assert request.htmx.triggering_event is None return request.htmx.triggering_event with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.text == "null" def test_triggering_event_bad_json() -> None: @get("/") def handler(request: HTMXRequest) -> None: assert request.htmx.triggering_event is None return request.htmx.triggering_event with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/", headers={HTMXHeaders.TRIGGERING_EVENT.value: "{"}) assert response.text == "null" def test_triggering_event_good_json() -> None: @get("/") def handler(request: HTMXRequest) -> Any: return request.htmx.triggering_event with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get( "/", headers={ HTMXHeaders.TRIGGERING_EVENT.value: "%7B%22target%22%3A%20null%7D", f"{HTMXHeaders.TRIGGERING_EVENT.value}-uri-autoencoded": "true", }, ) assert response.text == '{"target":null}' litestar-2.16.0/tests/unit/test_contrib/test_htmx/test_htmx_response.py000066400000000000000000000330601500564371300266030ustar00rootroot00000000000000from pathlib import Path from typing import Any import pytest from litestar import get from litestar.contrib.htmx._utils import HTMXHeaders from litestar.contrib.htmx.request import HTMXRequest from litestar.contrib.htmx.response import ( ClientRedirect, ClientRefresh, HTMXTemplate, HXLocation, HXStopPolling, PushUrl, ReplaceUrl, Reswap, Retarget, TriggerEvent, ) from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine from litestar.status_codes import HTTP_200_OK, HTTP_500_INTERNAL_SERVER_ERROR from litestar.template.config import TemplateConfig from litestar.testing import create_test_client async def test_hx_stop_polling_response() -> None: @get("/") def handler() -> HXStopPolling: return HXStopPolling() with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == 286 async def test_client_redirect_response() -> None: @get("/") def handler() -> ClientRedirect: return ClientRedirect(redirect_to="https://example.com") with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers.get(HTMXHeaders.REDIRECT) == "https://example.com" assert response.headers.get("location") is None async def test_client_refresh_response() -> None: @get("/") def handler() -> ClientRefresh: return ClientRefresh() with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers[HTMXHeaders.REFRESH] == "true" async def test_push_url_false_response() -> None: @get("/") def handler() -> PushUrl: return PushUrl(content="Success!", push_url=False) with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers[HTMXHeaders.PUSH_URL] == "false" async def test_push_url_response() -> None: @get("/") def handler() -> PushUrl: return PushUrl(content="Success!", push_url="/index.html") with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "Success!" assert response.headers[HTMXHeaders.PUSH_URL] == "/index.html" async def test_replace_url_false_response() -> None: @get("/") def handler() -> ReplaceUrl: return ReplaceUrl(content="Success!", replace_url=False) with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers[HTMXHeaders.REPLACE_URL] == "false" async def test_replace_url_response() -> None: @get("/") def handler() -> ReplaceUrl: return ReplaceUrl(content="Success!", replace_url="/index.html") with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "Success!" assert response.headers[HTMXHeaders.REPLACE_URL] == "/index.html" async def test_reswap_response() -> None: @get("/") def handler() -> Reswap: return Reswap(content="Success!", method="beforebegin") with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "Success!" assert response.headers[HTMXHeaders.RE_SWAP] == "beforebegin" async def test_retarget_response() -> None: @get("/") def handler() -> Retarget: return Retarget(content="Success!", target="#element") with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "Success!" assert response.headers[HTMXHeaders.RE_TARGET] == "#element" async def test_trigger_event_response_success() -> None: @get("/") def handler() -> TriggerEvent: return TriggerEvent( content="Success!", name="alert", after="receive", params={"warning": "Confirm your choice!"} ) with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "Success!" assert response.headers[HTMXHeaders.TRIGGER_EVENT] == '{"alert":{"warning":"Confirm your choice!"}}' async def test_trigger_event_response_no_params() -> None: @get("/") def handler() -> TriggerEvent: return TriggerEvent(content="Success!", name="alert", after="receive") with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "Success!" assert response.headers[HTMXHeaders.TRIGGER_EVENT] == '{"alert":{}}' async def test_trigger_event_response_after_settle() -> None: @get("/") def handler() -> TriggerEvent: return TriggerEvent( content="Success!", name="alert", after="settle", params={"warning": "Confirm your choice!"} ) with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "Success!" assert response.headers[HTMXHeaders.TRIGGER_AFTER_SETTLE] == '{"alert":{"warning":"Confirm your choice!"}}' async def test_trigger_event_response_after_swap() -> None: @get("/") def handler() -> TriggerEvent: return TriggerEvent(content="Success!", name="alert", after="swap", params={"warning": "Confirm your choice!"}) with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "Success!" assert response.headers[HTMXHeaders.TRIGGER_AFTER_SWAP] == '{"alert":{"warning":"Confirm your choice!"}}' async def test_trigger_event_response_invalid_after() -> None: @get("/") def handler() -> TriggerEvent: return TriggerEvent( content="Success!", name="alert", after="invalid", # type: ignore[arg-type] params={"warning": "Confirm your choice!"}, ) with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR async def test_hx_location_response_success() -> None: @get("/") def handler() -> HXLocation: return HXLocation(redirect_to="/contact-us") with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") spec = response.headers[HTMXHeaders.LOCATION] assert response.status_code == HTTP_200_OK assert "Location" not in response.headers assert spec == '{"path":"/contact-us"}' async def test_hx_location_response_with_all_parameters() -> None: @get("/") def handler() -> HXLocation: return HXLocation( redirect_to="/contact-us", source="#button", event="click", target="#content", swap="innerHTML", hx_headers={"attribute": "value"}, values={"action": "true"}, ) with create_test_client(route_handlers=[handler], request_class=HTMXRequest) as client: response = client.get("/") spec = response.headers[HTMXHeaders.LOCATION] assert response.status_code == HTTP_200_OK assert "Location" not in response.headers assert spec == ( '{"path":"/contact-us","source":"#button","event":"click","target":"#content","swap":"innerHTML",' '"values":{"action":"true"},"hx_headers":{"attribute":"value"}}' ) @pytest.mark.parametrize( "engine, template, expected", ( ( JinjaTemplateEngine, "path: {{ request.scope['path'] }} custom_key: {{ custom_key }}", "path: / custom_key: custom_value", ), ( MakoTemplateEngine, "path: ${request.scope['path']} custom_key: ${custom_key}", "path: / custom_key: custom_value", ), ), ) def test_HTMXTemplate_response_success(engine: Any, template: str, expected: str, tmp_path: Path) -> None: Path(tmp_path / "abc.html").write_text(template) @get(path="/") def handler() -> HTMXTemplate: return HTMXTemplate( template_name="abc.html", context={"request": {"scope": {"path": "nope"}}, "custom_key": "custom_value"}, push_url="/about", re_swap="beforebegin", re_target="#new-target-id", trigger_event="showMessage", params={"alert": "Confirm your Choice."}, after="receive", ) with create_test_client( route_handlers=[handler], template_config=TemplateConfig( directory=tmp_path, engine=engine, ), ) as client: response = client.get("/") assert response.text == expected assert response.headers.get(HTMXHeaders.PUSH_URL) == "/about" assert response.headers.get(HTMXHeaders.RE_SWAP) == "beforebegin" assert response.headers.get(HTMXHeaders.RE_TARGET) == "#new-target-id" assert response.headers.get(HTMXHeaders.TRIGGER_EVENT) == '{"showMessage":{"alert":"Confirm your Choice."}}' @pytest.mark.parametrize( "engine, template, expected", ( (JinjaTemplateEngine, "path: {{ request.scope['path'] }}", "path: /"), (MakoTemplateEngine, "path: ${request.scope['path']}", "path: /"), ), ) def test_HTMXTemplate_response_no_params(engine: Any, template: str, expected: str, tmp_path: Path) -> None: Path(tmp_path / "abc.html").write_text(template) @get(path="/") def handler() -> HTMXTemplate: return HTMXTemplate( template_name="abc.html", context={"request": {"scope": {"path": "nope"}}}, ) with create_test_client( route_handlers=[handler], template_config=TemplateConfig( directory=tmp_path, engine=engine, ), ) as client: response = client.get("/") assert response.text == expected assert response.headers.get(HTMXHeaders.PUSH_URL) is None assert response.headers.get(HTMXHeaders.RE_SWAP) is None assert response.headers.get(HTMXHeaders.RE_TARGET) is None assert response.headers.get(HTMXHeaders.TRIGGER_EVENT) is None @pytest.mark.parametrize( "engine, template, expected", ( (JinjaTemplateEngine, "path: {{ request.scope['path'] }}", "path: /"), (MakoTemplateEngine, "path: ${request.scope['path']}", "path: /"), ), ) def test_HTMXTemplate_response_push_url_set_to_false(engine: Any, template: str, expected: str, tmp_path: Path) -> None: Path(tmp_path / "abc.html").write_text(template) @get(path="/") def handler() -> HTMXTemplate: return HTMXTemplate( template_name="abc.html", context={"request": {"scope": {"path": "nope"}}}, push_url=False, ) with create_test_client( route_handlers=[handler], template_config=TemplateConfig( directory=tmp_path, engine=engine, ), ) as client: response = client.get("/") assert response.text == expected assert response.headers.get(HTMXHeaders.PUSH_URL) == "false" assert response.headers.get(HTMXHeaders.RE_SWAP) is None assert response.headers.get(HTMXHeaders.RE_TARGET) is None assert response.headers.get(HTMXHeaders.TRIGGER_EVENT) is None @pytest.mark.parametrize( "engine, template, expected", ( (JinjaTemplateEngine, "path: {{ request.scope['path'] }}", "path: /"), (MakoTemplateEngine, "path: ${request.scope['path']}", "path: /"), ), ) def test_htmx_template_response_bad_trigger_params(engine: Any, template: str, expected: str, tmp_path: Path) -> None: Path(tmp_path / "abc.html").write_text(template) @get(path="/") def handler() -> HTMXTemplate: return HTMXTemplate( template_name="abc.html", context={"request": {"scope": {"path": "nope"}}}, trigger_event="showMessage", params={"alert": "Confirm your Choice."}, after="begin", # type: ignore[arg-type] ) with create_test_client( route_handlers=[handler], template_config=TemplateConfig( directory=tmp_path, engine=engine, ), ) as client: response = client.get("/") assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert response.headers.get(HTMXHeaders.PUSH_URL) is None assert response.headers.get(HTMXHeaders.RE_SWAP) is None assert response.headers.get(HTMXHeaders.RE_TARGET) is None assert response.headers.get(HTMXHeaders.TRIGGER_EVENT) is None litestar-2.16.0/tests/unit/test_contrib/test_minijinja.py000066400000000000000000000032001500564371300236270ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import pytest from minijinja import Environment # type: ignore[import-untyped] from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.exceptions import ImproperlyConfiguredException, TemplateNotFoundException if TYPE_CHECKING: from pathlib import Path def test_mini_jinja_template_engine_instantiation_error(tmp_path: Path) -> None: with pytest.raises(ImproperlyConfiguredException): MiniJinjaTemplateEngine(directory=tmp_path, engine_instance=Environment()) with pytest.raises(ImproperlyConfiguredException): MiniJinjaTemplateEngine() def test_mini_jinja_template_engine_instantiated_with_engine() -> None: engine = Environment() template_engine = MiniJinjaTemplateEngine(engine_instance=engine) assert template_engine.engine is engine def test_mini_jinja_template_render_raises_template_not_found(tmp_path: Path) -> None: template_engine = MiniJinjaTemplateEngine(engine_instance=Environment()) with pytest.raises(TemplateNotFoundException): tmpl = template_engine.get_template("not_found.html") tmpl.render() def test_mini_jinja_template_render_string(tmp_path: Path) -> None: template_engine = MiniJinjaTemplateEngine(engine_instance=Environment()) good_template = template_engine.render_string("template as a {{value}}", context={"value": "string"}) assert good_template == "template as a string" def test_from_environment() -> None: engine = Environment() template_engine = MiniJinjaTemplateEngine.from_environment(engine) assert template_engine.engine is engine litestar-2.16.0/tests/unit/test_contrib/test_msgspec.py000066400000000000000000000146351500564371300233360ustar00rootroot00000000000000from __future__ import annotations import itertools from dataclasses import replace from typing import TYPE_CHECKING from unittest.mock import ANY import pytest from msgspec import Meta, Struct, field from typing_extensions import Annotated from litestar import Litestar, post from litestar.dto import DTOField, Mark, MsgspecDTO, dto_field from litestar.dto.data_structures import DTOFieldDefinition from litestar.typing import FieldDefinition if TYPE_CHECKING: from typing import Callable @pytest.fixture def expected_field_defs(int_factory: Callable[[], int]) -> list[DTOFieldDefinition]: return [ DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( annotation=int, name="a", ), model_name=ANY, default_factory=None, dto_field=DTOField(), ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( annotation=int, name="b", ), model_name=ANY, default_factory=None, dto_field=DTOField(mark=Mark.READ_ONLY), ), metadata=ANY, type_wrappers=ANY, raw=ANY, kwarg_definition=ANY, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( annotation=int, name="c", ), model_name=ANY, default_factory=None, dto_field=DTOField(), ), metadata=ANY, type_wrappers=ANY, raw=ANY, kwarg_definition=ANY, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( annotation=int, name="d", default=1, ), model_name=ANY, default_factory=None, dto_field=DTOField(), ), metadata=ANY, type_wrappers=ANY, raw=ANY, kwarg_definition=ANY, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( annotation=int, name="e", ), model_name=ANY, default_factory=int_factory, dto_field=DTOField(), ), metadata=ANY, type_wrappers=ANY, raw=ANY, kwarg_definition=ANY, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( annotation=str, name="computed", ), model_name=ANY, default_factory=None, dto_field=DTOField(mark="read-only"), ), metadata=ANY, type_wrappers=ANY, raw=ANY, kwarg_definition=ANY, ), ] def test_field_definition_generation( int_factory: Callable[[], int], expected_field_defs: list[DTOFieldDefinition] ) -> None: class TestStruct(Struct): a: int b: Annotated[int, Meta(extra=dto_field("read-only"))] c: Annotated[int, Meta(gt=1)] d: int = field(default=1) e: int = field(default_factory=int_factory) @property def computed(self) -> str: return "i am computed" @property def _private_computed(self) -> str: return "" field_defs = list(MsgspecDTO.generate_field_definitions(TestStruct)) assert field_defs[0].model_name == "TestStruct" for field_def, exp in itertools.zip_longest(expected_field_defs, field_defs, fillvalue=None): assert field_def == exp def test_detect_nested_field() -> None: class TestStruct(Struct): a: int class NotStruct: pass assert MsgspecDTO.detect_nested_field(FieldDefinition.from_annotation(TestStruct)) is True assert MsgspecDTO.detect_nested_field(FieldDefinition.from_annotation(NotStruct)) is False ReadOnlyInt = Annotated[int, DTOField("read-only")] def test_msgspec_dto_annotated_dto_field() -> None: class Model(Struct): a: Annotated[int, DTOField("read-only")] b: ReadOnlyInt dto_type = MsgspecDTO[Model] fields = list(dto_type.generate_field_definitions(Model)) assert fields[0].dto_field == DTOField("read-only") assert fields[1].dto_field == DTOField("read-only") def test_tag_field_included_in_schema() -> None: # default tag field, default tag value class Model(Struct, tag=True): regular_field: str # default tag field, custom tag value class Model2(Struct, tag=2): regular_field: str # custom tag field, custom tag value class Model3(Struct, tag_field="foo", tag="bar"): regular_field: str @post("/1") def handler(data: Model) -> None: return None @post("/2") def handler_2(data: Model2) -> None: return None @post("/3") def handler_3(data: Model3) -> None: return None components = Litestar( [handler, handler_2, handler_3], signature_types=[Model, Model2, Model3], ).openapi_schema.components.to_schema()["schemas"] assert components["test_tag_field_included_in_schema.Model"] == { "properties": { "regular_field": {"type": "string"}, "type": {"type": "string", "const": "Model"}, }, "type": "object", "required": ["regular_field", "type"], "title": "Model", } assert components["test_tag_field_included_in_schema.Model2"] == { "properties": { "regular_field": {"type": "string"}, "type": {"type": "integer", "const": 2}, }, "type": "object", "required": ["regular_field", "type"], "title": "Model2", } assert components["test_tag_field_included_in_schema.Model3"] == { "properties": { "regular_field": {"type": "string"}, "foo": {"type": "string", "const": "bar"}, }, "type": "object", "required": ["foo", "regular_field"], "title": "Model3", } litestar-2.16.0/tests/unit/test_contrib/test_opentelemetry.py000066400000000000000000000254231500564371300245660ustar00rootroot00000000000000from typing import Tuple, cast import pytest from _pytest.fixtures import FixtureRequest from opentelemetry.metrics import get_meter_provider, set_meter_provider from opentelemetry.sdk.metrics._internal import MeterProvider from opentelemetry.sdk.metrics._internal.aggregation import ( ExplicitBucketHistogramAggregation, ) from opentelemetry.sdk.metrics._internal.export import InMemoryMetricReader from opentelemetry.sdk.metrics._internal.instrument import Counter from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import Span, TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from litestar import WebSocket, get, websocket from litestar.config.app import AppConfig from litestar.contrib.opentelemetry import OpenTelemetryConfig, OpenTelemetryPlugin from litestar.exceptions import http_exceptions from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from litestar.types.asgi_types import ASGIApp, Receive, Scope, Send @pytest.fixture(scope="session") def resource() -> Resource: return Resource(attributes={SERVICE_NAME: "litestar-test"}) @pytest.fixture(scope="session") def reader() -> InMemoryMetricReader: aggregation_last_value = {Counter: ExplicitBucketHistogramAggregation()} return InMemoryMetricReader(preferred_aggregation=aggregation_last_value) # type: ignore[arg-type] @pytest.fixture(scope="session") def meter_provider(resource: Resource, reader: InMemoryMetricReader) -> MeterProvider: provider = MeterProvider(resource=resource, metric_readers=[reader]) set_meter_provider(provider) return provider @pytest.fixture() def exporter() -> InMemorySpanExporter: return InMemorySpanExporter() @pytest.fixture() def config( resource: Resource, exporter: InMemorySpanExporter, meter_provider: MeterProvider, request: FixtureRequest ) -> OpenTelemetryConfig: tracer_provider = TracerProvider(resource=resource) tracer_provider.add_span_processor(SimpleSpanProcessor(exporter)) meter = get_meter_provider().get_meter(f"litestar-test-{request.node.nodeid}") return OpenTelemetryConfig(tracer_provider=tracer_provider, meter=meter) @pytest.fixture(params=["middleware", "plugin"]) def app_config(request: FixtureRequest, config: OpenTelemetryConfig) -> AppConfig: if request.param == "middleware": return AppConfig(middleware=[config.middleware]) return AppConfig(plugins=[OpenTelemetryPlugin(config)]) def test_open_telemetry_middleware_with_http_route( app_config: AppConfig, reader: InMemoryMetricReader, exporter: InMemorySpanExporter, ) -> None: @get("/") def handler() -> dict: return {"hello": "world"} with create_test_client(handler, middleware=app_config.middleware, plugins=app_config.plugins) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert reader.get_metrics_data() first_span, second_span, third_span = cast("Tuple[Span, Span, Span]", exporter.get_finished_spans()) assert dict(first_span.attributes) == {"http.status_code": 200, "asgi.event.type": "http.response.start"} # type: ignore[arg-type] assert dict(second_span.attributes) == {"asgi.event.type": "http.response.body"} # type: ignore[arg-type] assert dict(third_span.attributes) == { # type: ignore[arg-type] "http.scheme": "http", "http.host": "testserver.local", "net.host.port": 80, "http.flavor": "1.1", "http.target": "/", "http.url": "http://testserver.local/", "http.method": "GET", "http.server_name": "testserver.local", "http.user_agent": "testclient", "net.peer.ip": "testclient", "net.peer.port": 50000, "http.route": "GET /", "http.status_code": 200, } metric_data = reader.get_metrics_data() assert metric_data assert metric_data.resource_metrics resource_metrics = metric_data.resource_metrics[0] assert resource_metrics.scope_metrics scope_metrics = resource_metrics.scope_metrics[0] assert scope_metrics.metrics request_metric = scope_metrics.metrics[0] assert len(list(request_metric.data.data_points)) == 1 def test_open_telemetry_middleware_with_websocket_route( app_config: AppConfig, reader: InMemoryMetricReader, exporter: InMemorySpanExporter, ) -> None: @websocket("/") async def handler(socket: "WebSocket") -> None: await socket.accept() await socket.send_json({"hello": "world"}) await socket.close() with create_test_client(handler, middleware=app_config.middleware, plugins=app_config.plugins).websocket_connect( "/" ) as client: data = client.receive_json() assert data == {"hello": "world"} first_span, second_span, third_span, fourth_span, fifth_span = cast( "Tuple[Span, Span, Span, Span, Span]", exporter.get_finished_spans() ) assert dict(first_span.attributes) == {"asgi.event.type": "websocket.connect"} # type: ignore[arg-type] assert dict(second_span.attributes) == {"asgi.event.type": "websocket.accept"} # type: ignore[arg-type] assert dict(third_span.attributes) == {"asgi.event.type": "websocket.send", "http.status_code": 200} # type: ignore[arg-type] assert dict(fourth_span.attributes) == {"asgi.event.type": "websocket.close"} # type: ignore[arg-type] assert dict(fifth_span.attributes) == { # type: ignore[arg-type] "http.scheme": "ws", "http.host": "testserver.local", "net.host.port": 80, "http.target": "/", "http.url": "ws://testserver.local/", "http.server_name": "testserver.local", "http.user_agent": "testclient", "net.peer.ip": "testclient", "net.peer.port": 50000, "http.route": "/", "http.status_code": 200, } def test_open_telemetry_middleware_handles_route_not_found_under_span_http( app_config: AppConfig, reader: InMemoryMetricReader, exporter: InMemorySpanExporter, ) -> None: @get("/") def handler() -> dict: raise Exception("random Exception") with create_test_client(handler, middleware=app_config.middleware, plugins=app_config.plugins) as client: response = client.get("/route_that_does_not_exist") assert response.status_code first_span, second_span, third_span = cast("Tuple[Span, Span, Span]", exporter.get_finished_spans()) assert dict(first_span.attributes) == { # type: ignore[arg-type] "http.status_code": 404, "asgi.event.type": "http.response.start", } assert dict(second_span.attributes) == {"asgi.event.type": "http.response.body"} # type: ignore[arg-type] assert dict(third_span.attributes) == { # type: ignore[arg-type] "http.scheme": "http", "http.host": "testserver.local", "net.host.port": 80, "http.flavor": "1.1", "http.target": "/route_that_does_not_exist", "http.url": "http://testserver.local/route_that_does_not_exist", "http.method": "GET", "http.server_name": "testserver.local", "http.user_agent": "testclient", "net.peer.ip": "testclient", "net.peer.port": 50000, "http.route": "GET /route_that_does_not_exist", "http.status_code": 404, } def test_open_telemetry_middleware_handles_method_not_allowed_under_span_http( app_config: AppConfig, reader: InMemoryMetricReader, exporter: InMemorySpanExporter, ) -> None: @get("/") def handler() -> dict: raise Exception("random Exception") with create_test_client(handler, middleware=app_config.middleware, plugins=app_config.plugins) as client: response = client.post("/") assert response.status_code first_span, second_span, third_span = cast("Tuple[Span, Span, Span]", exporter.get_finished_spans()) assert dict(first_span.attributes) == { # type: ignore[arg-type] "http.status_code": 405, "asgi.event.type": "http.response.start", } assert dict(second_span.attributes) == {"asgi.event.type": "http.response.body"} # type: ignore[arg-type] assert dict(third_span.attributes) == { # type: ignore[arg-type] "http.scheme": "http", "http.host": "testserver.local", "net.host.port": 80, "http.flavor": "1.1", "http.target": "/", "http.url": "http://testserver.local/", "http.method": "POST", "http.server_name": "testserver.local", "http.user_agent": "testclient", "net.peer.ip": "testclient", "net.peer.port": 50000, "http.route": "POST /", "http.status_code": 405, } def test_open_telemetry_middleware_handles_errors_caused_on_middleware( app_config: AppConfig, reader: InMemoryMetricReader, exporter: InMemorySpanExporter, ) -> None: raise_exception = True def middleware_factory(app: ASGIApp) -> ASGIApp: async def error_middleware(scope: Scope, receive: Receive, send: Send) -> None: if raise_exception: raise http_exceptions.NotAuthorizedException() await app(scope, receive, send) return error_middleware @get("/") def handler() -> dict: raise Exception("random Exception") with create_test_client( handler, middleware=[middleware_factory, *app_config.middleware], plugins=app_config.plugins ) as client: response = client.get("/") assert response.status_code first_span, second_span, third_span = cast("Tuple[Span, Span, Span]", exporter.get_finished_spans()) assert dict(first_span.attributes) == { # type: ignore[arg-type] "http.status_code": 401, "asgi.event.type": "http.response.start", } assert dict(second_span.attributes) == {"asgi.event.type": "http.response.body"} # type: ignore[arg-type] assert dict(third_span.attributes) == { # type: ignore[arg-type] "http.scheme": "http", "http.host": "testserver.local", "net.host.port": 80, "http.flavor": "1.1", "http.target": "/", "http.url": "http://testserver.local/", "http.method": "GET", "http.server_name": "testserver.local", "http.user_agent": "testclient", "net.peer.ip": "testclient", "net.peer.port": 50000, "http.route": "GET /", "http.status_code": 401, } litestar-2.16.0/tests/unit/test_contrib/test_piccolo_orm/000077500000000000000000000000001500564371300236175ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_contrib/test_piccolo_orm/__init__.py000066400000000000000000000002211500564371300257230ustar00rootroot00000000000000import os # this is required to ensure that piccolo discovers its conf without throwing. os.environ["PICCOLO_CONF"] = "tests.unit.piccolo_conf" litestar-2.16.0/tests/unit/test_contrib/test_piccolo_orm/endpoints.py000066400000000000000000000015531500564371300262000ustar00rootroot00000000000000from typing import List from piccolo.testing import ModelBuilder from litestar import MediaType, get, post from litestar.contrib.piccolo import PiccoloDTO from tests.unit.test_contrib.test_piccolo_orm.tables import Concert, RecordingStudio, Venue studio = ModelBuilder.build_sync(RecordingStudio, persist=False) venues = [ModelBuilder.build_sync(Venue, persist=False) for _ in range(3)] @post("/concert", dto=PiccoloDTO[Concert], return_dto=PiccoloDTO[Concert], media_type=MediaType.JSON) async def create_concert(data: Concert) -> Concert: await data.save() await data.refresh() return data @get("/studio", return_dto=PiccoloDTO[RecordingStudio], sync_to_thread=False) def retrieve_studio() -> RecordingStudio: return studio @get("/venues", return_dto=PiccoloDTO[Venue], sync_to_thread=False) def retrieve_venues() -> List[Venue]: return venues litestar-2.16.0/tests/unit/test_contrib/test_piccolo_orm/piccolo_app.py000066400000000000000000000012021500564371300264540ustar00rootroot00000000000000"""The contents of this file were adapted from: https://github.com/piccolo-orm/piccolo/blob/master/tests/example_apps/music/piccolo_app.py """ from pathlib import Path from piccolo.conf.apps import AppConfig from tests.unit.test_contrib.test_piccolo_orm.tables import ( Band, Concert, Manager, RecordingStudio, Venue, ) CURRENT_DIRECTORY = Path(__file__).parent APP_CONFIG = AppConfig( app_name="music", table_classes=[ Manager, Band, Venue, Concert, RecordingStudio, ], migrations_folder_path=str(CURRENT_DIRECTORY / "piccolo_migrations"), commands=[], ) litestar-2.16.0/tests/unit/test_contrib/test_piccolo_orm/tables.py000066400000000000000000000013551500564371300254470ustar00rootroot00000000000000"""The contents of this file were adapted from: https://github.com/piccolo-orm/piccolo/blob/master/tests/example_apps/music/tables.py """ from piccolo.columns.column_types import JSON, JSONB, Array, ForeignKey, Integer, Varchar from piccolo.table import Table class RecordingStudio(Table): facilities = JSON() facilities_b = JSONB() microphones = Array(Varchar()) class Manager(Table): name = Varchar(length=50) class Band(Table): name = Varchar(length=50) manager = ForeignKey(Manager) popularity = Integer() class Venue(Table): name = Varchar(length=100) capacity = Integer(secret=True) class Concert(Table): band_1 = ForeignKey(Band) band_2 = ForeignKey(Band) venue = ForeignKey(Venue) litestar-2.16.0/tests/unit/test_contrib/test_piccolo_orm/test_piccolo_orm_dto.py000066400000000000000000000143631500564371300304120ustar00rootroot00000000000000from __future__ import annotations from decimal import Decimal from typing import AsyncGenerator, Callable import pytest from polyfactory.utils.predicates import is_annotated from typing_extensions import get_args from litestar import Litestar from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client try: import piccolo # noqa: F401 except ImportError: pytest.skip("Piccolo not installed", allow_module_level=True) import pytest from piccolo.columns import Column, column_types from piccolo.columns.column_types import Varchar from piccolo.conf.apps import Finder from piccolo.table import Table, create_db_tables, drop_db_tables from litestar.contrib.piccolo import PiccoloDTO from .endpoints import create_concert, retrieve_studio, retrieve_venues, studio, venues from .tables import RecordingStudio, Venue def test_dto_deprecation() -> None: class Manager(Table): name = Varchar(length=50) with pytest.deprecated_call(): from litestar.contrib.piccolo import PiccoloDTO _ = PiccoloDTO[Manager] @pytest.fixture(autouse=True) async def scaffold_piccolo() -> AsyncGenerator: """Scaffolds Piccolo ORM and performs cleanup.""" tables = Finder().get_table_classes() await drop_db_tables(*tables) await create_db_tables(*tables) yield await drop_db_tables(*tables) def test_serializing_single_piccolo_table(scaffold_piccolo: Callable) -> None: with create_test_client(route_handlers=[retrieve_studio]) as client: response = client.get("/studio") assert response.status_code == HTTP_200_OK assert str(RecordingStudio(**response.json()).querystring) == str(studio.querystring) def test_serializing_multiple_piccolo_tables(scaffold_piccolo: Callable) -> None: with create_test_client(route_handlers=[retrieve_venues]) as client: response = client.get("/venues") sanitized_venues = [] for v in venues: non_secret_data = { column._meta.db_column_name: v[column._meta.db_column_name] for column in v.all_columns() if not column._meta.secret } sanitized_venues.append(Venue(**non_secret_data)) assert response.status_code == HTTP_200_OK assert [str(Venue(**value).querystring) for value in response.json()] == [ str(v.querystring) for v in sanitized_venues ] @pytest.mark.parametrize( "piccolo_type, py_type, meta_data_key", ( (column_types.Decimal, Decimal, None), (column_types.Numeric, Decimal, None), (column_types.Email, str, "max_length"), (column_types.Varchar, str, "max_length"), (column_types.JSON, str, "format"), (column_types.JSONB, str, "format"), (column_types.Text, str, "format"), ), ) def test_piccolo_dto_type_conversion(piccolo_type: type[Column], py_type: type, meta_data_key: str | None) -> None: class _Table(Table): field = piccolo_type(required=True, help_text="my column") field_defs = list(PiccoloDTO.generate_field_definitions(_Table)) assert len(field_defs) == 2 field_def = field_defs[1] assert is_annotated(field_def.raw) assert field_def.annotation is py_type metadata = get_args(field_def.raw)[1] assert metadata.extra.get("description", "") if meta_data_key: assert metadata.extra.get(meta_data_key, "") or getattr(metadata, meta_data_key, None) def test_piccolo_dto_openapi_spec_generation() -> None: app = Litestar(route_handlers=[retrieve_studio, retrieve_venues, create_concert]) schema = app.openapi_schema assert schema.paths assert len(schema.paths) == 3 concert_path = schema.paths["/concert"] assert concert_path studio_path = schema.paths["/studio"] assert studio_path venues_path = schema.paths["/venues"] assert venues_path post_operation = concert_path.post assert ( post_operation.request_body.content["application/json"].schema.ref # type: ignore[union-attr] == "#/components/schemas/CreateConcertConcertRequestBody" ) studio_path_get_operation = studio_path.get assert ( studio_path_get_operation.responses["200"].content["application/json"].schema.ref # type: ignore[index, union-attr] == "#/components/schemas/RetrieveStudioRecordingStudioResponseBody" ) venues_path_get_operation = venues_path.get assert ( venues_path_get_operation.responses["200"].content["application/json"].schema.items.ref # type: ignore[index, union-attr] == "#/components/schemas/RetrieveVenuesVenueResponseBody" ) concert_schema = schema.components.schemas["CreateConcertConcertRequestBody"] assert concert_schema assert concert_schema.to_schema() == { "properties": { "band_1": {"oneOf": [{"type": "integer"}, {"type": "null"}]}, "band_2": { "oneOf": [ {"type": "integer"}, {"type": "null"}, ] }, "venue": {"oneOf": [{"type": "integer"}, {"type": "null"}]}, }, "required": [], "title": "CreateConcertConcertRequestBody", "type": "object", } record_studio_schema = schema.components.schemas["RetrieveStudioRecordingStudioResponseBody"] assert record_studio_schema assert record_studio_schema.to_schema() == { "properties": { "facilities": {"oneOf": [{"type": "string"}, {"type": "null"}]}, "facilities_b": {"oneOf": [{"type": "string"}, {"type": "null"}]}, "microphones": {"oneOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}]}, "id": {"oneOf": [{"type": "integer"}, {"type": "null"}]}, }, "required": [], "title": "RetrieveStudioRecordingStudioResponseBody", "type": "object", } venue_schema = schema.components.schemas["RetrieveVenuesVenueResponseBody"] assert venue_schema assert venue_schema.to_schema() == { "properties": { "id": {"oneOf": [{"type": "integer"}, {"type": "null"}]}, "name": {"oneOf": [{"type": "string"}, {"type": "null"}]}, }, "required": [], "title": "RetrieveVenuesVenueResponseBody", "type": "object", } litestar-2.16.0/tests/unit/test_contrib/test_prometheus.py000066400000000000000000000045301500564371300240610ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations import importlib import sys from importlib.util import cache_from_source from pathlib import Path import pytest def purge_module(module_names: list[str], path: str | Path) -> None: for name in module_names: if name in sys.modules: del sys.modules[name] Path(cache_from_source(str(path))).unlink(missing_ok=True) def test_deprecated_prometheus_imports() -> None: purge_module(["litestar.contrib.prometheus"], __file__) with pytest.warns( DeprecationWarning, match="importing PrometheusMiddleware from 'litestar.contrib.prometheus' is deprecated" ): from litestar.contrib.prometheus import PrometheusMiddleware purge_module(["litestar.contrib.prometheus"], __file__) with pytest.warns( DeprecationWarning, match="importing PrometheusConfig from 'litestar.contrib.prometheus' is deprecated" ): from litestar.contrib.prometheus import PrometheusConfig purge_module(["litestar.contrib.prometheus"], __file__) with pytest.warns( DeprecationWarning, match="importing PrometheusController from 'litestar.contrib.prometheus' is deprecated" ): from litestar.contrib.prometheus import PrometheusController def test_deprecated_prometheus_middleware_imports() -> None: purge_module(["litestar.contrib.prometheus.middleware"], __file__) with pytest.warns( DeprecationWarning, match="importing PrometheusMiddleware from 'litestar.contrib.prometheus.middleware' is deprecated", ): from litestar.contrib.prometheus.middleware import PrometheusMiddleware def test_deprecated_prometheus_config_imports() -> None: purge_module(["litestar.contrib.prometheus.config"], __file__) with pytest.warns( DeprecationWarning, match="importing PrometheusConfig from 'litestar.contrib.prometheus.config' is deprecated", ): from litestar.contrib.prometheus.config import PrometheusConfig def test_deprecated_prometheus_controller_imports() -> None: purge_module(["litestar.contrib.prometheus.controller"], __file__) with pytest.warns( DeprecationWarning, match="importing PrometheusController from 'litestar.contrib.prometheus.controller' is deprecated", ): from litestar.contrib.prometheus.controller import PrometheusController litestar-2.16.0/tests/unit/test_contrib/test_pydantic.py000066400000000000000000000111151500564371300234760ustar00rootroot00000000000000# ruff: noqa: TC004, F401 from __future__ import annotations import importlib import sys from importlib.util import cache_from_source from pathlib import Path import pytest def purge_module(module_names: list[str], path: str | Path) -> None: for name in module_names: if name in sys.modules: del sys.modules[name] Path(cache_from_source(str(path))).unlink(missing_ok=True) def test_deprecated_pydantic_imports() -> None: purge_module(["litestar.contrib.pydantic"], __file__) with pytest.warns(DeprecationWarning, match="importing PydanticDTO from 'litestar.contrib.pydantic' is deprecated"): from litestar.contrib.pydantic import PydanticDTO purge_module(["litestar.contrib.pydantic"], __file__) with pytest.warns( DeprecationWarning, match="importing PydanticInitPlugin from 'litestar.contrib.pydantic' is deprecated" ): from litestar.contrib.pydantic import PydanticInitPlugin purge_module(["litestar.contrib.pydantic"], __file__) with pytest.warns( DeprecationWarning, match="importing PydanticSchemaPlugin from 'litestar.contrib.pydantic' is deprecated" ): from litestar.contrib.pydantic import PydanticSchemaPlugin purge_module(["litestar.contrib.pydantic"], __file__) with pytest.warns( DeprecationWarning, match="importing PydanticPlugin from 'litestar.contrib.pydantic' is deprecated" ): from litestar.contrib.pydantic import PydanticPlugin purge_module(["litestar.contrib.pydantic"], __file__) with pytest.warns( DeprecationWarning, match="importing PydanticDIPlugin from 'litestar.contrib.pydantic' is deprecated" ): from litestar.contrib.pydantic import PydanticDIPlugin def test_deprecated_pydantic_dto_factory_imports() -> None: purge_module(["litestar.contrib.pydantic.pydantic_dto_factory"], __file__) with pytest.warns( DeprecationWarning, match="importing PydanticDTO from 'litestar.contrib.pydantic' is deprecated", ): from litestar.contrib.pydantic import PydanticDTO def test_deprecated_pydantic_init_plugin_imports() -> None: purge_module(["litestar.contrib.pydantic.pydantic_init_plugin"], __file__) with pytest.warns( DeprecationWarning, match="importing PydanticInitPlugin from 'litestar.contrib.pydantic' is deprecated", ): from litestar.contrib.pydantic import PydanticInitPlugin def test_deprecated_pydantic_schema_plugin_imports() -> None: purge_module(["litestar.contrib.pydantic.pydantic_schema_plugin"], __file__) with pytest.warns( DeprecationWarning, match="importing PydanticSchemaPlugin from 'litestar.contrib.pydantic' is deprecated", ): from litestar.contrib.pydantic import PydanticSchemaPlugin def test_deprecated_pydantic_di_plugin_imports() -> None: purge_module(["litestar.contrib.pydantic"], __file__) with pytest.warns( DeprecationWarning, match="importing PydanticDIPlugin from 'litestar.contrib.pydantic' is deprecated", ): from litestar.contrib.pydantic import PydanticDIPlugin def test_deprecated_pydantic_utils_imports() -> None: purge_module(["litestar.contrib.pydantic.utils"], __file__) with pytest.warns( DeprecationWarning, match="importing get_model_info from 'litestar.contrib.pydantic.utils' is deprecated", ): from litestar.contrib.pydantic.utils import get_model_info purge_module(["litestar.contrib.pydantic.utils"], __file__) with pytest.warns( DeprecationWarning, match="importing is_pydantic_constrained_field from 'litestar.contrib.pydantic.utils' is deprecated", ): from litestar.contrib.pydantic.utils import is_pydantic_constrained_field purge_module(["litestar.contrib.pydantic.utils"], __file__) with pytest.warns( DeprecationWarning, match="importing is_pydantic_model_class from 'litestar.contrib.pydantic.utils' is deprecated", ): from litestar.contrib.pydantic.utils import is_pydantic_model_class purge_module(["litestar.contrib.pydantic.utils"], __file__) with pytest.warns( DeprecationWarning, match="importing is_pydantic_undefined from 'litestar.contrib.pydantic.utils' is deprecated", ): from litestar.contrib.pydantic.utils import is_pydantic_undefined purge_module(["litestar.contrib.pydantic.utils"], __file__) with pytest.warns( DeprecationWarning, match="importing is_pydantic_v2 from 'litestar.contrib.pydantic.utils' is deprecated", ): from litestar.contrib.pydantic.utils import is_pydantic_v2 litestar-2.16.0/tests/unit/test_contrib/test_repository.py000066400000000000000000000076041500564371300241120ustar00rootroot00000000000000import pytest from litestar.app import Litestar from litestar.repository import handlers from litestar.repository.filters import ( BeforeAfter, CollectionFilter, FilterTypes, LimitOffset, NotInCollectionFilter, NotInSearchFilter, OnBeforeAfter, OrderBy, SearchFilter, ) def test_app_repository_signature_namespace() -> None: app = Litestar([], on_app_init=[handlers.on_app_init]) assert app.signature_namespace == { "BeforeAfter": BeforeAfter, "OnBeforeAfter": OnBeforeAfter, "CollectionFilter": CollectionFilter, "LimitOffset": LimitOffset, "OrderBy": OrderBy, "SearchFilter": SearchFilter, "NotInCollectionFilter": NotInCollectionFilter, "NotInSearchFilter": NotInSearchFilter, "FilterTypes": FilterTypes, } def test_deprecated_abc_imports() -> None: from litestar.contrib.repository import abc as abc_contrib from litestar.repository import abc assert abc_contrib.AbstractAsyncRepository is abc.AbstractAsyncRepository assert abc_contrib.AbstractSyncRepository is abc.AbstractSyncRepository with pytest.raises(AttributeError): abc_contrib.foo def test_deprecated_exception_imports() -> None: from litestar.contrib.repository import exceptions as contrib_exceptions from litestar.repository import exceptions assert exceptions.RepositoryError is contrib_exceptions.RepositoryError assert exceptions.ConflictError is contrib_exceptions.ConflictError assert exceptions.NotFoundError is contrib_exceptions.NotFoundError with pytest.raises(AttributeError): contrib_exceptions.foo def test_deprecated_filters_imports() -> None: from litestar.contrib.repository import filters as contrib_filter from litestar.repository import filters assert filters.FilterTypes is contrib_filter.FilterTypes assert filters.CollectionFilter is contrib_filter.CollectionFilter assert filters.NotInCollectionFilter is contrib_filter.NotInCollectionFilter assert filters.SearchFilter is contrib_filter.SearchFilter assert filters.NotInSearchFilter is contrib_filter.NotInSearchFilter assert filters.OnBeforeAfter is contrib_filter.OnBeforeAfter assert filters.BeforeAfter is contrib_filter.BeforeAfter assert filters.LimitOffset is contrib_filter.LimitOffset assert filters.OrderBy is contrib_filter.OrderBy with pytest.raises(AttributeError): contrib_filter.foo def test_advanced_alchemy_imports() -> None: from advanced_alchemy import filters from litestar.repository import _filters assert filters.FilterTypes is not _filters.FilterTypes assert filters.CollectionFilter is not _filters.CollectionFilter assert filters.NotInCollectionFilter is not _filters.NotInCollectionFilter assert filters.SearchFilter is not _filters.SearchFilter assert filters.NotInSearchFilter is not _filters.NotInSearchFilter assert filters.OnBeforeAfter is not _filters.OnBeforeAfter assert filters.BeforeAfter is not _filters.BeforeAfter assert filters.LimitOffset is not _filters.LimitOffset assert filters.OrderBy is not _filters.OrderBy def test_deprecated_handlers_imports() -> None: from litestar.contrib.repository import handlers as contrib_handlers from litestar.repository import handlers assert handlers.on_app_init is contrib_handlers.on_app_init with pytest.raises(AttributeError): contrib_handlers.foo def test_deprecated_testing_imports() -> None: from litestar.contrib.repository import testing as contrib_testing from litestar.repository.testing import generic_mock_repository assert generic_mock_repository.GenericAsyncMockRepository is contrib_testing.GenericAsyncMockRepository assert generic_mock_repository.GenericSyncMockRepository is contrib_testing.GenericSyncMockRepository with pytest.raises(AttributeError): contrib_testing.foo litestar-2.16.0/tests/unit/test_contrib/test_sqlalchemy.py000066400000000000000000000436361500564371300240420ustar00rootroot00000000000000# ruff: noqa: TC004, F401 # pyright: reportUnusedImport=false from __future__ import annotations import importlib import sys from pathlib import Path import pytest from advanced_alchemy import exceptions as advanced_alchemy_exceptions from advanced_alchemy import repository as advanced_alchemy_repo from advanced_alchemy import types as advanced_alchemy_types from advanced_alchemy.repository import typing as advanced_alchemy_typing from sqlalchemy import Engine, create_engine from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine def purge_module(module_names: list[str], path: str | Path) -> None: for name in module_names: if name in sys.modules: del sys.modules[name] Path(importlib.util.cache_from_source(path)).unlink(missing_ok=True) # type: ignore[arg-type] def test_create_engine_with_engine_instance() -> None: from litestar.contrib.sqlalchemy.plugins.init.config.sync import SQLAlchemySyncConfig engine = create_engine("sqlite:///:memory:") config = SQLAlchemySyncConfig(engine_instance=engine) with pytest.deprecated_call(): assert engine is config.create_engine() # type: ignore[attr-defined] def test_create_engine_with_connection_string() -> None: from litestar.contrib.sqlalchemy.plugins.init.config.sync import SQLAlchemySyncConfig config = SQLAlchemySyncConfig(connection_string="sqlite:///:memory:") with pytest.deprecated_call(): engine = config.create_engine() # type: ignore[attr-defined] assert isinstance(engine, Engine) def test_async_create_engine_with_engine_instance() -> None: from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import SQLAlchemyAsyncConfig engine = create_async_engine("sqlite+aiosqlite:///:memory:") config = SQLAlchemyAsyncConfig(engine_instance=engine) with pytest.deprecated_call(): assert engine is config.create_engine() # type: ignore[attr-defined] def test_async_create_engine_with_connection_string() -> None: from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import SQLAlchemyAsyncConfig config = SQLAlchemyAsyncConfig(connection_string="sqlite+aiosqlite:///:memory:") with pytest.deprecated_call(): engine = config.create_engine() # type: ignore[attr-defined] assert isinstance(engine, AsyncEngine) def test_repository_re_exports() -> None: from litestar.contrib.sqlalchemy import types from litestar.contrib.sqlalchemy.repository import ( SQLAlchemyAsyncRepository, SQLAlchemySyncRepository, wrap_sqlalchemy_exception, ) from litestar.contrib.sqlalchemy.repository import types as repository_types assert wrap_sqlalchemy_exception is advanced_alchemy_exceptions.wrap_sqlalchemy_exception assert SQLAlchemySyncRepository is advanced_alchemy_repo.SQLAlchemySyncRepository assert SQLAlchemyAsyncRepository is advanced_alchemy_repo.SQLAlchemyAsyncRepository assert repository_types.ModelT is advanced_alchemy_typing.ModelT # pyright: ignore[reportGeneralTypeIssues] assert repository_types.RowT is advanced_alchemy_typing.RowT # pyright: ignore[reportGeneralTypeIssues] assert repository_types.SQLAlchemyAsyncRepositoryT is advanced_alchemy_typing.SQLAlchemyAsyncRepositoryT # pyright: ignore[reportGeneralTypeIssues] assert repository_types.SQLAlchemySyncRepositoryT is advanced_alchemy_typing.SQLAlchemySyncRepositoryT # pyright: ignore[reportGeneralTypeIssues] assert types.GUID is advanced_alchemy_types.GUID assert types.ORA_JSONB is advanced_alchemy_types.ORA_JSONB assert types.BigIntIdentity is advanced_alchemy_types.BigIntIdentity assert types.DateTimeUTC is advanced_alchemy_types.DateTimeUTC assert types.JsonB is advanced_alchemy_types.JsonB def test_deprecated_sqlalchemy_imports() -> None: purge_module(["litestar.contrib.sqlalchemy"], __file__) with pytest.warns( DeprecationWarning, match="importing SQLAlchemyAsyncRepository from 'litestar.contrib.sqlalchemy' is deprecated" ): from litestar.contrib.sqlalchemy import SQLAlchemyAsyncRepository purge_module(["litestar.contrib.sqlalchemy"], __file__) with pytest.warns( DeprecationWarning, match="importing SQLAlchemySyncRepository from 'litestar.contrib.sqlalchemy' is deprecated" ): from litestar.contrib.sqlalchemy import SQLAlchemySyncRepository purge_module(["litestar.contrib.sqlalchemy"], __file__) with pytest.warns(DeprecationWarning, match="importing ModelT from 'litestar.contrib.sqlalchemy' is deprecated"): from litestar.contrib.sqlalchemy import ModelT purge_module(["litestar.contrib.sqlalchemy"], __file__) with pytest.warns( DeprecationWarning, match="importing wrap_sqlalchemy_exception from 'litestar.contrib.sqlalchemy' is deprecated" ): from litestar.contrib.sqlalchemy import wrap_sqlalchemy_exception def test_deprecated_sqlalchemy_plugins_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.plugins"], __file__) with pytest.warns( DeprecationWarning, match="importing AsyncSessionConfig from 'litestar.contrib.sqlalchemy.plugins' is deprecated", ): from litestar.contrib.sqlalchemy.plugins import AsyncSessionConfig purge_module(["litestar.contrib.sqlalchemy.plugins"], __file__) with pytest.warns( DeprecationWarning, match="importing EngineConfig from 'litestar.contrib.sqlalchemy.plugins' is deprecated" ): from litestar.contrib.sqlalchemy.plugins import EngineConfig purge_module(["litestar.contrib.sqlalchemy.plugins"], __file__) with pytest.warns( DeprecationWarning, match="importing GenericSQLAlchemyConfig from 'litestar.contrib.sqlalchemy.plugins' is deprecated", ): from litestar.contrib.sqlalchemy.plugins import GenericSQLAlchemyConfig purge_module(["litestar.contrib.sqlalchemy.plugins"], __file__) with pytest.warns( DeprecationWarning, match="importing SQLAlchemyAsyncConfig from 'litestar.contrib.sqlalchemy.plugins' is deprecated", ): from litestar.contrib.sqlalchemy.plugins import SQLAlchemyAsyncConfig purge_module(["litestar.contrib.sqlalchemy.plugins"], __file__) with pytest.warns( DeprecationWarning, match="importing SQLAlchemyInitPlugin from 'litestar.contrib.sqlalchemy.plugins' is deprecated", ): from litestar.contrib.sqlalchemy.plugins import SQLAlchemyInitPlugin def test_deprecated_sqlalchemy_plugins_init_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.plugins.init"], __file__) with pytest.warns( DeprecationWarning, match="importing AsyncSessionConfig from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init import AsyncSessionConfig purge_module(["litestar.contrib.sqlalchemy.plugins.init"], __file__) with pytest.warns( DeprecationWarning, match="importing EngineConfig from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated" ): from litestar.contrib.sqlalchemy.plugins.init import EngineConfig purge_module(["litestar.contrib.sqlalchemy.plugins.init"], __file__) with pytest.warns( DeprecationWarning, match="importing GenericSQLAlchemyConfig from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init import GenericSQLAlchemyConfig purge_module(["litestar.contrib.sqlalchemy.plugins.init"], __file__) with pytest.warns( DeprecationWarning, match="importing SQLAlchemyAsyncConfig from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init import SQLAlchemyAsyncConfig def test_deprecated_sqlalchemy_plugins_init_config_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.plugins.init.config"], __file__) with pytest.warns( DeprecationWarning, match="importing AsyncSessionConfig from 'litestar.contrib.sqlalchemy.plugins.init.config' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config import AsyncSessionConfig purge_module(["litestar.contrib.sqlalchemy.plugins.init.config"], __file__) with pytest.warns( DeprecationWarning, match="importing EngineConfig from 'litestar.contrib.sqlalchemy.plugins.init.config' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config import EngineConfig purge_module(["litestar.contrib.sqlalchemy.plugins.init.config"], __file__) with pytest.warns( DeprecationWarning, match="importing GenericSQLAlchemyConfig from 'litestar.contrib.sqlalchemy.plugins.init.config' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config import GenericSQLAlchemyConfig purge_module(["litestar.contrib.sqlalchemy.plugins.init.config"], __file__) with pytest.warns( DeprecationWarning, match="importing SQLAlchemyAsyncConfig from 'litestar.contrib.sqlalchemy.plugins.init.config' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config import SQLAlchemyAsyncConfig def test_deprecated_sqlalchemy_plugins_init_config_common_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.common"], __file__) with pytest.warns( DeprecationWarning, match="importing SESSION_SCOPE_KEY from 'litestar.contrib.sqlalchemy.plugins.init.config.common' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.common import SESSION_SCOPE_KEY purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.common"], __file__) with pytest.warns( DeprecationWarning, match="importing SESSION_TERMINUS_ASGI_EVENTS from 'litestar.contrib.sqlalchemy.plugins.init.config.common' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.common import SESSION_TERMINUS_ASGI_EVENTS purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.common"], __file__) with pytest.warns( DeprecationWarning, match="importing GenericSQLAlchemyConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.common' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.common import GenericSQLAlchemyConfig def test_deprecated_sqlalchemy_plugins_init_config_sync_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.sync"], __file__) with pytest.warns( DeprecationWarning, match="importing SQLAlchemySyncConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.sync' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.sync import SQLAlchemySyncConfig purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.sync"], __file__) with pytest.warns( DeprecationWarning, match="importing AlembicSyncConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.sync' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.sync import AlembicSyncConfig purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.sync"], __file__) with pytest.warns( DeprecationWarning, match="importing SyncSessionConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.sync' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.sync import SyncSessionConfig def test_deprecated_sqlalchemy_plugins_init_config_asyncio_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.asyncio"], __file__) with pytest.warns( DeprecationWarning, match="importing SQLAlchemyAsyncConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.asyncio' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import SQLAlchemyAsyncConfig purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.asyncio"], __file__) with pytest.warns( DeprecationWarning, match="importing AlembicAsyncConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.asyncio' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import AlembicAsyncConfig purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.asyncio"], __file__) with pytest.warns( DeprecationWarning, match="importing AsyncSessionConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.asyncio' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import AsyncSessionConfig def test_deprecated_sqlalchemy_plugins_init_config_engine_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.engine"], __file__) with pytest.warns( DeprecationWarning, match="importing EngineConfig from 'litestar.contrib.sqlalchemy.plugins.init.config.engine' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.engine import EngineConfig def test_deprecated_sqlalchemy_dto_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.dto"], __file__) with pytest.warns( DeprecationWarning, match="importing SQLAlchemyDTOConfig from 'litestar.contrib.sqlalchemy.dto' is deprecated", ): from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTOConfig def test_deprecated_sqlalchemy_plugins_init_plugin_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.plugins.init.plugin"], __file__) with pytest.warns( DeprecationWarning, match="importing SQLAlchemyInitPlugin from 'litestar.contrib.sqlalchemy.plugins.init' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.plugin import SQLAlchemyInitPlugin def test_deprecated_sqlalchemy_plugins_serialization_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.plugins.serialization"], __file__) with pytest.warns( DeprecationWarning, match="importing SQLAlchemySerializationPlugin from 'litestar.contrib.sqlalchemy.plugins.serialization' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.serialization import SQLAlchemySerializationPlugin def test_deprecated_sqlalchemy_repository_async_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.repository._async"], __file__) with pytest.warns( DeprecationWarning, match="importing SQLAlchemyAsyncRepository from 'litestar.contrib.sqlalchemy.repository._async' is deprecated", ): from litestar.contrib.sqlalchemy.repository._async import SQLAlchemyAsyncRepository def test_deprecated_sqlalchemy_repository_sync_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.repository._sync"], __file__) with pytest.warns( DeprecationWarning, match="importing SQLAlchemySyncRepository from 'litestar.contrib.sqlalchemy.repository._sync' is deprecated", ): from litestar.contrib.sqlalchemy.repository._sync import SQLAlchemySyncRepository def test_deprecated_sqlalchemy_base_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.base"], __file__) with pytest.warns( DeprecationWarning, match="from 'litestar.contrib.sqlalchemy.base' is deprecated", ): from litestar.contrib.sqlalchemy.base import ( AuditColumns, BigIntAuditBase, BigIntBase, BigIntPrimaryKey, CommonTableAttributes, ModelProtocol, UUIDAuditBase, UUIDBase, UUIDPrimaryKey, create_registry, orm_registry, touch_updated_timestamp, ) def test_deprecated_sqlalchemy_plugins_init_config_asyncio_handlers() -> None: purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.asyncio"], __file__) with pytest.warns( DeprecationWarning, match="importing default_before_send_handler from 'litestar.contrib.sqlalchemy.plugins.init.config.asyncio' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import default_before_send_handler purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.asyncio"], __file__) with pytest.warns( DeprecationWarning, match="importing autocommit_before_send_handler from 'litestar.contrib.sqlalchemy.plugins.init.config.asyncio' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import autocommit_before_send_handler def test_deprecated_sqlalchemy_plugins_init_config_sync_handlers() -> None: purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.sync"], __file__) with pytest.warns( DeprecationWarning, match="importing default_before_send_handler from 'litestar.contrib.sqlalchemy.plugins.init.config.sync' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.sync import default_before_send_handler purge_module(["litestar.contrib.sqlalchemy.plugins.init.config.sync"], __file__) with pytest.warns( DeprecationWarning, match="importing autocommit_before_send_handler from 'litestar.contrib.sqlalchemy.plugins.init.config.sync' is deprecated", ): from litestar.contrib.sqlalchemy.plugins.init.config.sync import autocommit_before_send_handler def test_deprecated_sqlalchemy_repository_util_imports() -> None: purge_module(["litestar.contrib.sqlalchemy.repository._util"], __file__) with pytest.warns( DeprecationWarning, match="importing wrap_sqlalchemy_exception from 'litestar.contrib.sqlalchemy.repository._util' is deprecated", ): from litestar.contrib.sqlalchemy.repository._util import wrap_sqlalchemy_exception purge_module(["litestar.contrib.sqlalchemy.repository._util"], __file__) with pytest.warns( DeprecationWarning, match="importing get_instrumented_attr from 'litestar.contrib.sqlalchemy.repository._util' is deprecated", ): from litestar.contrib.sqlalchemy.repository._util import get_instrumented_attr litestar-2.16.0/tests/unit/test_controller.py000066400000000000000000000074161500564371300213600ustar00rootroot00000000000000from typing import Any, Type, Union import msgspec import pytest from litestar import ( Controller, HttpMethod, Litestar, Response, delete, get, patch, post, put, websocket, ) from litestar.connection import WebSocket from litestar.exceptions import ImproperlyConfiguredException from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT from litestar.testing import create_test_client from tests.models import DataclassPerson, DataclassPersonFactory @pytest.mark.parametrize( "decorator, http_method, expected_status_code, return_value, return_annotation", [ ( get, HttpMethod.GET, HTTP_200_OK, Response(content=DataclassPersonFactory.build()), Response[DataclassPerson], ), (get, HttpMethod.GET, HTTP_200_OK, DataclassPersonFactory.build(), DataclassPerson), (post, HttpMethod.POST, HTTP_201_CREATED, DataclassPersonFactory.build(), DataclassPerson), (put, HttpMethod.PUT, HTTP_200_OK, DataclassPersonFactory.build(), DataclassPerson), (patch, HttpMethod.PATCH, HTTP_200_OK, DataclassPersonFactory.build(), DataclassPerson), (delete, HttpMethod.DELETE, HTTP_204_NO_CONTENT, None, None), ], ) async def test_controller_http_method( decorator: Union[Type[get], Type[post], Type[put], Type[patch], Type[delete]], http_method: HttpMethod, expected_status_code: int, return_value: Any, return_annotation: Any, ) -> None: test_path = "/person" class MyController(Controller): path = test_path @decorator() # type: ignore[misc] def test_method(self) -> return_annotation: return return_value with create_test_client(MyController) as client: response = client.request(http_method, test_path) assert response.status_code == expected_status_code if return_value is not None and not isinstance(return_value, Response): assert response.json() == msgspec.to_builtins(return_value) def test_controller_with_websocket_handler() -> None: test_path = "/person" class MyController(Controller): path = test_path @get() def get_person(self) -> DataclassPerson: return DataclassPersonFactory.build() @websocket(path="/socket") async def ws(self, socket: WebSocket) -> None: await socket.accept() await socket.send_json({"data": "123"}) await socket.close() client = create_test_client(route_handlers=MyController) with client.websocket_connect(f"{test_path}/socket") as ws: ws.send_json({"data": "123"}) data = ws.receive_json() assert data def test_controller_validation() -> None: class BuggyController(Controller): path: str = "/ctrl" @get() async def handle_get(self) -> str: return "Hello World" @get() async def handle_get2(self) -> str: return "Hello World" with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[BuggyController]) def test_controller_subclassing() -> None: class BaseController(Controller): @get("/{id:int}") async def test_get(self, id: int) -> str: return f"{self.__class__.__name__} {id}" class FooController(BaseController): path = "/foo" class BarController(BaseController): path = "/bar" with create_test_client([FooController, BarController]) as client: response = client.get("/foo/123") assert response.status_code == 200 assert response.text == "FooController 123" response = client.get("/bar/123") assert response.status_code == 200 assert response.text == "BarController 123" litestar-2.16.0/tests/unit/test_data_extractors.py000066400000000000000000000135661500564371300223670ustar00rootroot00000000000000from typing import Any, List from unittest.mock import AsyncMock import pytest from pytest_mock import MockFixture from litestar import Request from litestar.connection.base import empty_receive from litestar.data_extractors import ConnectionDataExtractor, ResponseDataExtractor from litestar.datastructures import Cookie from litestar.enums import RequestEncodingType from litestar.response.base import ASGIResponse from litestar.status_codes import HTTP_200_OK from litestar.testing import RequestFactory factory = RequestFactory() async def test_connection_data_extractor() -> None: request = factory.post( path="/a/b/c", headers={"Common": "abc", "Special": "123", "Content-Type": "application/json; charset=utf-8"}, cookies=[Cookie(key="regular"), Cookie(key="auth")], query_params={"first": ["1", "2", "3"], "second": ["jeronimo"]}, data={"hello": "world"}, ) request.scope["path_params"] = {"first": "10", "second": "20", "third": "30"} extractor = ConnectionDataExtractor(parse_body=True, parse_query=True) extracted_data = extractor(request) assert await extracted_data.get("body") == await request.json() # type: ignore[misc] assert extracted_data.get("content_type") == request.content_type assert extracted_data.get("headers") == dict(request.headers) assert extracted_data.get("headers") == dict(request.headers) assert extracted_data.get("path") == request.scope["path"] assert extracted_data.get("path") == request.scope["path"] assert extracted_data.get("path_params") == request.scope["path_params"] assert extracted_data.get("query") == request.query_params.dict() assert extracted_data.get("scheme") == request.scope["scheme"] def test_parse_query() -> None: request = factory.post( path="/a/b/c", query_params={"first": ["1", "2", "3"], "second": ["jeronimo"]}, ) parsed_extracted_data = ConnectionDataExtractor(parse_query=True)(request) unparsed_extracted_data = ConnectionDataExtractor()(request) assert parsed_extracted_data.get("query") == request.query_params.dict() assert unparsed_extracted_data.get("query") == request.scope["query_string"] # Close to avoid warnings about un-awaited coroutines. parsed_extracted_data.get("body").close() # type: ignore[union-attr] unparsed_extracted_data.get("body").close() # type: ignore[union-attr] async def test_parse_json_data() -> None: request = factory.post(path="/a/b/c", data={"hello": "world"}) assert await ConnectionDataExtractor(parse_body=True)(request).get("body") == await request.json() # type: ignore[misc] assert await ConnectionDataExtractor()(request).get("body") == await request.body() # type: ignore[misc] async def test_parse_form_data() -> None: request = factory.post(path="/a/b/c", data={"file": b"123"}, request_media_type=RequestEncodingType.MULTI_PART) assert await ConnectionDataExtractor(parse_body=True)(request).get("body") == dict(await request.form()) # type: ignore[misc] async def test_parse_url_encoded() -> None: request = factory.post(path="/a/b/c", data={"key": "123"}, request_media_type=RequestEncodingType.URL_ENCODED) assert await ConnectionDataExtractor(parse_body=True)(request).get("body") == dict(await request.form()) # type: ignore[misc] @pytest.mark.parametrize("req", [factory.get(headers={"Special": "123"}), factory.get(headers={"special": "123"})]) def test_request_extraction_header_obfuscation(req: Request[Any, Any, Any]) -> None: extractor = ConnectionDataExtractor(obfuscate_headers={"special"}) extracted_data = extractor(req) assert extracted_data.get("headers") == {"special": "*****"} # Close to avoid warnings about un-awaited coroutines. extracted_data.get("body").close() # type: ignore[union-attr] @pytest.mark.parametrize( "req, key", [ (factory.get(cookies=[Cookie(key="special")]), "special"), (factory.get(cookies=[Cookie(key="Special")]), "Special"), ], ) def test_request_extraction_cookie_obfuscation(req: Request[Any, Any, Any], key: str) -> None: extractor = ConnectionDataExtractor(obfuscate_cookies={"special"}) extracted_data = extractor(req) assert extracted_data.get("cookies") == {"Path": "/", "SameSite": "lax", key: "*****"} # Close to avoid warnings about un-awaited coroutines. extracted_data.get("body").close() # type: ignore[union-attr] async def test_response_data_extractor() -> None: headers = {"common": "abc", "special": "123", "content-type": "application/json"} cookies = [Cookie(key="regular"), Cookie(key="auth")] response = ASGIResponse(body=b'{"hello":"world"}', cookies=cookies, headers=headers) extractor = ResponseDataExtractor() messages: List[Any] = [] async def send(message: "Any") -> None: messages.append(message) await response({}, empty_receive, send) # type: ignore[arg-type] assert len(messages) == 2 extracted_data = extractor(messages) # type: ignore[arg-type] assert extracted_data.get("status_code") == HTTP_200_OK assert extracted_data.get("body") == b'{"hello":"world"}' assert extracted_data.get("headers") == {**headers, "content-length": "17"} assert extracted_data.get("cookies") == {"Path": "/", "SameSite": "lax", "auth": "", "regular": ""} async def test_request_data_extractor_skip_keys() -> None: req = factory.get() extractor = ConnectionDataExtractor() assert (await extractor.extract(req, {"body"})).keys() == {"body"} async def test_skip_parse_malformed_body_false_raises(mocker: MockFixture) -> None: mocker.patch("litestar.testing.request_factory.Request.json", new=AsyncMock(side_effect=ValueError())) req = factory.post(headers={"Content-Type": "application/json"}) extractor = ConnectionDataExtractor(parse_body=True, skip_parse_malformed_body=False) with pytest.raises(ValueError): await extractor.extract(req, {"body"}) litestar-2.16.0/tests/unit/test_datastructures/000077500000000000000000000000001500564371300216705ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_datastructures/__init_.py000066400000000000000000000000001500564371300236300ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_datastructures/test_cookie.py000066400000000000000000000031361500564371300245550ustar00rootroot00000000000000from datetime import datetime, timedelta from time_machine import travel from litestar.datastructures import Cookie def test_basic_cookie_as_header() -> None: cookie = Cookie(key="key") assert cookie.to_header() == 'Set-Cookie: key=""; Path=/; SameSite=lax' @travel(datetime.utcnow, tick=False) def test_cookie_as_header() -> None: expires_sec = 123 cookie = Cookie( key="key", value="value", path="/path", expires=expires_sec, domain="domain.com", secure=True, httponly=True, samesite="strict", ) now = datetime.utcnow() expected_expired = (now + timedelta(seconds=expires_sec)).strftime("%a, %d %b %Y %H:%M:%S GMT") assert cookie.to_header() == ( f"Set-Cookie: key=value; Domain=domain.com; expires={expected_expired}; HttpOnly; Path=/path; SameSite=strict; Secure" ) def test_cookie_with_max_age_as_header() -> None: cookie = Cookie(key="key", max_age=10) assert cookie.to_header() == 'Set-Cookie: key=""; Max-Age=10; Path=/; SameSite=lax' def test_cookie_as_header_without_header_name() -> None: cookie = Cookie(key="key") assert cookie.to_header(header="") == 'key=""; Path=/; SameSite=lax' def test_equality() -> None: assert Cookie(key="key") == Cookie(key="key") assert Cookie(key="key") != Cookie(key="key", path="/test") assert Cookie(key="key", path="/test") != Cookie(key="key", path="/test", domain="localhost") assert Cookie(key="key", path="/test", domain="localhost") == Cookie(key="key", path="/test", domain="localhost") assert Cookie(key="key") != "key" litestar-2.16.0/tests/unit/test_datastructures/test_headers.py000066400000000000000000000320471500564371300247220ustar00rootroot00000000000000from dataclasses import dataclass from typing import TYPE_CHECKING, Callable, List, Optional, Union import pytest from pytest import FixtureRequest from litestar import MediaType from litestar.datastructures import ( Accept, CacheControlHeader, ETag, Headers, MutableScopeHeaders, ) from litestar.datastructures.headers import Header from litestar.exceptions import ImproperlyConfiguredException, ValidationException from litestar.types.asgi_types import HTTPResponseBodyEvent, HTTPResponseStartEvent from litestar.utils.dataclass import simple_asdict if TYPE_CHECKING: from litestar.types.asgi_types import RawHeaders, RawHeadersList, Scope @pytest.fixture def raw_headers() -> "RawHeadersList": return [(b"foo", b"bar")] @pytest.fixture def raw_headers_tuple() -> "RawHeaders": return [(b"foo", b"bar")] def test_header_container_requires_header_key_being_defined() -> None: class TestHeader(Header): def _get_header_value(self) -> str: return "" def from_header(self, header_value: str) -> "Header": # type: ignore[explicit-override, override] return self with pytest.raises(ImproperlyConfiguredException): TestHeader().to_header() @pytest.fixture def mutable_headers(raw_headers: "RawHeadersList") -> MutableScopeHeaders: return MutableScopeHeaders({"headers": raw_headers}) @pytest.fixture def mutable_headers_from_tuple(raw_headers_tuple: "RawHeaders") -> MutableScopeHeaders: return MutableScopeHeaders({"headers": raw_headers_tuple}) @pytest.fixture(params=[True, False]) def existing_headers_key(request: FixtureRequest) -> str: return "Foo" if request.param else "foo" def test_headers_from_mapping() -> None: headers = Headers({"foo": "bar", "baz": "zab"}) assert headers["foo"] == "bar" assert headers["baz"] == "zab" def test_headers_from_raw_list() -> None: headers = Headers([(b"foo", b"bar"), (b"foo", b"baz")]) assert headers.getall("foo") == ["bar", "baz"] def test_headers_from_raw_tuple() -> None: headers = Headers(((b"foo", b"bar"), (b"foo", b"baz"))) assert headers.getall("foo") == ["bar", "baz"] def test_headers_from_scope(create_scope: "Callable[..., Scope]") -> None: headers = Headers.from_scope(create_scope(headers=[(b"foo", b"bar"), (b"buzz", b"bup")])) assert headers["foo"] == "bar" assert headers["buzz"] == "bup" def test_headers_to_header_list() -> None: raw = [(b"foo", b"bar"), (b"foo", b"baz")] headers = Headers(raw) assert headers.to_header_list() == raw def test_headers_tuple_to_header_list() -> None: raw = ((b"foo", b"bar"), (b"foo", b"baz")) headers = Headers(raw) assert headers.to_header_list() == list(raw) def test_mutable_scope_headers_from_iterable() -> None: raw = [(b"foo", b"bar"), (b"foo", b"baz")] headers = MutableScopeHeaders({"headers": iter(raw)}) assert headers.headers == raw def test_mutable_scope_headers_from_message(raw_headers: "RawHeadersList", raw_headers_tuple: "RawHeaders") -> None: headers = MutableScopeHeaders.from_message( HTTPResponseStartEvent(type="http.response.start", status=200, headers=raw_headers) ) assert headers.headers == raw_headers headers = MutableScopeHeaders.from_message( HTTPResponseStartEvent(type="http.response.start", status=200, headers=raw_headers_tuple) ) assert headers.headers == list(raw_headers_tuple) def test_mutable_scope_headers_from_message_invalid_type() -> None: with pytest.raises(ValueError): MutableScopeHeaders.from_message(HTTPResponseBodyEvent(type="http.response.body", body=b"", more_body=False)) def test_mutable_scope_headers_add( raw_headers: "RawHeadersList", mutable_headers: MutableScopeHeaders, existing_headers_key: str ) -> None: mutable_headers.add(existing_headers_key, "baz") assert raw_headers == [(b"foo", b"bar"), (b"foo", b"baz")] def test_mutable_scope_headers_from_tuple_add( raw_headers_tuple: "RawHeaders", mutable_headers_from_tuple: MutableScopeHeaders, existing_headers_key: str ) -> None: mutable_headers_from_tuple.add(existing_headers_key, "baz") assert list(raw_headers_tuple) == [(b"foo", b"bar"), (b"foo", b"baz")] def test_mutable_scope_headers_getall_singular_value( mutable_headers: MutableScopeHeaders, mutable_headers_from_tuple: MutableScopeHeaders, existing_headers_key: str ) -> None: assert mutable_headers.getall(existing_headers_key) == ["bar"] assert mutable_headers_from_tuple.getall(existing_headers_key) == ["bar"] def test_mutable_scope_headers_getall_multi_value( mutable_headers: MutableScopeHeaders, mutable_headers_from_tuple: MutableScopeHeaders, existing_headers_key: str ) -> None: mutable_headers.add(existing_headers_key, "baz") assert mutable_headers.getall("foo") == ["bar", "baz"] mutable_headers_from_tuple.add(existing_headers_key, "baz") assert mutable_headers.getall("foo") == ["bar", "baz"] def test_mutable_scope_headers_getall_not_found_no_default(mutable_headers: MutableScopeHeaders) -> None: with pytest.raises(KeyError): mutable_headers.getall("bar") def test_mutable_scope_headers_getall_not_found_default(mutable_headers: MutableScopeHeaders) -> None: assert mutable_headers.getall("bar", ["default"]) == ["default"] def test_mutable_scope_headers_extend_header_value( raw_headers: "RawHeadersList", mutable_headers: MutableScopeHeaders ) -> None: mutable_headers.extend_header_value("foo", "baz") assert raw_headers == [(b"foo", b"bar,baz")] def test_mutable_scope_headers_from_tuple_extend_header_value( raw_headers_tuple: "RawHeaders", mutable_headers_from_tuple: MutableScopeHeaders ) -> None: mutable_headers_from_tuple.extend_header_value("foo", "baz") assert list(raw_headers_tuple) == [(b"foo", b"bar,baz")] def test_mutable_scope_headers_extend_header_value_new_header( raw_headers: "RawHeadersList", mutable_headers: MutableScopeHeaders ) -> None: mutable_headers.extend_header_value("bar", "baz") assert raw_headers == [(b"foo", b"bar"), (b"bar", b"baz")] def test_mutable_scope_headers_from_tuple_extend_header_value_new_header( raw_headers_tuple: "RawHeaders", mutable_headers_from_tuple: MutableScopeHeaders ) -> None: mutable_headers_from_tuple.extend_header_value("bar", "baz") assert list(raw_headers_tuple) == [(b"foo", b"bar"), (b"bar", b"baz")] def test_mutable_scope_headers_getitem(mutable_headers: MutableScopeHeaders, existing_headers_key: str) -> None: assert mutable_headers[existing_headers_key] == "bar" def test_mutable_scope_headers_getitem_not_found(mutable_headers: MutableScopeHeaders) -> None: with pytest.raises(KeyError): mutable_headers["bar"] def test_mutable_scope_headers_setitem_existing_key( raw_headers: "RawHeadersList", mutable_headers: MutableScopeHeaders, existing_headers_key: str ) -> None: mutable_headers[existing_headers_key] = "baz" assert raw_headers == [(b"foo", b"baz")] def test_mutable_scope_headers_from_tuple_setitem_existing_key( raw_headers_tuple: "RawHeaders", mutable_headers_from_tuple: MutableScopeHeaders, existing_headers_key: str ) -> None: mutable_headers_from_tuple[existing_headers_key] = "baz" assert list(raw_headers_tuple) == [(b"foo", b"baz")] def test_mutable_scope_headers_setitem_new_key( raw_headers: "RawHeadersList", mutable_headers: MutableScopeHeaders ) -> None: mutable_headers["bar"] = "baz" assert raw_headers == [(b"foo", b"bar"), (b"bar", b"baz")] def test_mutable_scope_headers_setitem_delitem( raw_headers: "RawHeadersList", mutable_headers: MutableScopeHeaders, existing_headers_key: str ) -> None: mutable_headers.add("foo", "baz") mutable_headers["bar"] = "baz" del mutable_headers[existing_headers_key] assert raw_headers == [(b"bar", b"baz")] def test_mutable_scope_headers_setdefault() -> None: headers = MutableScopeHeaders() assert headers.setdefault("foo", "bar") == "bar" assert headers.setdefault("foo", "baz") == "bar" assert headers.getall("foo") == ["bar"] def test_mutable_scope_header_len(mutable_headers: MutableScopeHeaders) -> None: assert len(mutable_headers) == 1 mutable_headers.add("foo", "bar") assert len(mutable_headers) == 2 mutable_headers["bar"] = "baz" assert len(mutable_headers) == 3 def test_mutable_scope_header_iter(mutable_headers: MutableScopeHeaders) -> None: mutable_headers.add("foo", "baz") mutable_headers["bar"] = "zab" assert list(mutable_headers) == ["foo", "foo", "bar"] def test_cache_control_to_header() -> None: header = CacheControlHeader(max_age=10, private=True) expected_header_values = ["max-age=10, private", "private, max-age=10"] assert header.to_header() in expected_header_values assert header.to_header(include_header_name=True) in [f"cache-control: {v}" for v in expected_header_values] def test_cache_control_from_header() -> None: header_value = ( "public, private, no-store, no-cache, max-age=10000, s-maxage=1000, no-transform, " "must-revalidate, proxy-revalidate, must-understand, immutable, stale-while-revalidate=100" ) header = CacheControlHeader.from_header(header_value) assert simple_asdict(header) == { "documentation_only": False, "public": True, "private": True, "no_store": True, "no_cache": True, "max_age": 10000, "s_maxage": 1000, "no_transform": True, "must_revalidate": True, "proxy_revalidate": True, "must_understand": True, "immutable": True, "stale_while_revalidate": 100, } def test_cache_control_from_header_single_value() -> None: header_value = "no-cache" header = CacheControlHeader.from_header(header_value) header_dict = simple_asdict(header, exclude_none=True) assert header_dict == {"no_cache": True, "documentation_only": False} @pytest.mark.parametrize("invalid_value", ["x=y=z", "x, ", "no-cache=10", "invalid-header=10"]) def test_cache_control_from_header_invalid_value(invalid_value: str) -> None: with pytest.raises(ImproperlyConfiguredException): CacheControlHeader.from_header(invalid_value) def test_cache_control_header_prevent_storing() -> None: header = CacheControlHeader.prevent_storing() header_dict = simple_asdict(header, exclude_none=True) assert header_dict == {"no_store": True, "documentation_only": False} def test_cache_control_header_unsupported_type_annotation() -> None: @dataclass class InvalidCacheControlHeader(CacheControlHeader): foo_field: Union[int, str] = "foo" with pytest.raises(ImproperlyConfiguredException): InvalidCacheControlHeader.from_header("unsupported_type") def test_etag_documentation_only() -> None: assert ETag(documentation_only=True).value is None def test_etag_no_value() -> None: with pytest.raises(ValidationException): ETag() with pytest.raises(ValidationException): ETag(weak=True) def test_etag_non_ascii() -> None: with pytest.raises(ValueError): ETag(value="f↓o") def test_etag_from_header() -> None: etag = ETag.from_header('"foo"') assert etag.value == "foo" assert etag.weak is False @pytest.mark.parametrize("value", ['W/"foo"', 'w/"foo"']) def test_etag_from_header_weak(value: str) -> None: etag = ETag.from_header(value) assert etag.value == "foo" assert etag.weak is True @pytest.mark.parametrize("value", ['"føo"', 'W/"føo"']) def test_etag_from_header_non_ascii_value(value: str) -> None: with pytest.raises(ImproperlyConfiguredException): ETag.from_header(value) @pytest.mark.parametrize("value", ["foo", "W/foo"]) def test_etag_from_header_missing_quotes(value: str) -> None: with pytest.raises(ImproperlyConfiguredException): ETag.from_header(value) def test_etag_to_header() -> None: assert ETag(value="foo").to_header() == '"foo"' def test_etag_to_header_weak() -> None: assert ETag(value="foo", weak=True).to_header() == 'W/"foo"' @pytest.mark.parametrize( "accept_value,provided_types,best_match", ( ("text/plain", ["text/plain"], "text/plain"), ("text/plain", [MediaType.TEXT], MediaType.TEXT), ("text/plain", ["text/plain"], "text/plain"), ("text/plain", ["text/html"], None), ("text/*", ["text/html"], "text/html"), ("*/*", ["text/html"], "text/html"), ("text/plain;p=test", ["text/plain"], "text/plain"), ("text/plain", ["text/plain;p=test"], None), ("text/plain;p=test", ["text/plain;p=test"], "text/plain;p=test"), ("text/plain", ["text/*"], "text/plain"), ("text/html", ["*/*"], "text/html"), ("text/plain;q=0.8,text/html", ["text/plain", "text/html"], "text/html"), ("text/*,text/html", ["text/plain", "text/html"], "text/html"), ), ) def test_accept_best_match(accept_value: str, provided_types: List[str], best_match: Optional[str]) -> None: accept = Accept(accept_value) assert accept.best_match(provided_types) == best_match def test_accept_accepts() -> None: accept = Accept("text/plain;q=0.8,text/html") assert accept.accepts(MediaType.TEXT) litestar-2.16.0/tests/unit/test_datastructures/test_multi_dicts.py000066400000000000000000000041261500564371300256240ustar00rootroot00000000000000from __future__ import annotations from unittest.mock import patch import pytest from litestar.datastructures import UploadFile from litestar.datastructures.multi_dicts import FormMultiDict, ImmutableMultiDict, MultiDict @pytest.mark.parametrize("multi_class", [MultiDict, ImmutableMultiDict]) def test_multi_to_dict(multi_class: type[MultiDict | ImmutableMultiDict]) -> None: multi = multi_class([("key", "value"), ("key", "value2"), ("key2", "value3")]) assert multi.dict() == {"key": ["value", "value2"], "key2": ["value3"]} @pytest.mark.parametrize("multi_class", [MultiDict, ImmutableMultiDict]) def test_multi_multi_items(multi_class: type[MultiDict | ImmutableMultiDict]) -> None: data = [("key", "value"), ("key", "value2"), ("key2", "value3")] multi = multi_class(data) assert sorted(multi.multi_items()) == sorted(data) def test_multi_dict_as_immutable() -> None: data = [("key", "value"), ("key", "value2"), ("key2", "value3")] multi = MultiDict[str](data) # pyright: ignore assert multi.immutable().dict() == ImmutableMultiDict(data).dict() def test_immutable_multi_dict_as_mutable() -> None: data = [("key", "value"), ("key", "value2"), ("key2", "value3")] multi = ImmutableMultiDict[str](data) # pyright: ignore assert multi.mutable_copy().dict() == MultiDict(data).dict() async def test_form_multi_dict_close() -> None: multi = FormMultiDict( [ ("foo", UploadFile(filename="foo", content_type="text/plain")), ("bar", UploadFile(filename="foo", content_type="text/plain")), ] ) with patch("litestar.datastructures.multi_dicts.UploadFile.close") as mock_close: await multi.close() assert mock_close.call_count == 2 # calls the real UploadFile.close method to clean up await multi.close() @pytest.mark.parametrize("type_", [MultiDict, ImmutableMultiDict]) def test_copy(type_: type[MultiDict | ImmutableMultiDict]) -> None: d = type_([("foo", "bar"), ("foo", "baz")]) copy = d.copy() assert set(d.multi_items()) == set(copy.multi_items()) assert isinstance(d, type_) litestar-2.16.0/tests/unit/test_datastructures/test_response_header.py000066400000000000000000000005061500564371300264500ustar00rootroot00000000000000import pytest from litestar.datastructures import ResponseHeader from litestar.exceptions import ImproperlyConfiguredException def test_response_headers_validation() -> None: ResponseHeader(name="test", documentation_only=True) with pytest.raises(ImproperlyConfiguredException): ResponseHeader(name="test") litestar-2.16.0/tests/unit/test_datastructures/test_secret_values.py000066400000000000000000000126161500564371300261530ustar00rootroot00000000000000from __future__ import annotations import msgspec import pytest from typing_extensions import Annotated from litestar import Litestar, get, post from litestar.datastructures.secret_values import SecretBytes, SecretString from litestar.openapi.spec.parameter import Parameter as OpenAPIParameter from litestar.openapi.spec.schema import Schema from litestar.params import Parameter from litestar.serialization import default_deserializer, default_serializer from litestar.testing import create_test_client def test_secret_string_get_secret() -> None: secret_string = SecretString("some_secret_value") assert secret_string.get_secret() == "some_secret_value" def test_secret_string_get_obscured_representation() -> None: secret_string = SecretString("some_secret_value") assert secret_string.get_obscured() == "******" def test_secret_string_str() -> None: secret_string = SecretString("some_secret_value") assert str(secret_string) == "******" def test_secret_string_repr() -> None: secret_string = SecretString("some_secret_value") assert repr(secret_string) == "SecretString('******')" def test_secret_bytes_get_secret() -> None: secret_bytes = SecretBytes(b"some_secret_value") assert secret_bytes.get_secret() == b"some_secret_value" def test_secret_bytes_get_obscured_representation() -> None: secret_bytes = SecretBytes(b"some_secret_value") assert secret_bytes.get_obscured() == b"******" def test_secret_bytes_str() -> None: secret_bytes = SecretBytes(b"some_secret_value") assert str(secret_bytes) == str(b"******") def test_secret_bytes_repr() -> None: secret_bytes = SecretBytes(b"some_secret_value") assert repr(secret_bytes) == "SecretBytes(b'******')" def test_secret_string_encode() -> None: secret_string = SecretString("some_secret") assert default_serializer(secret_string) == "******" def test_secret_bytes_encode() -> None: secret_bytes = SecretBytes(b"some_secret") assert default_serializer(secret_bytes) == "******" def test_secret_string_decode() -> None: secret = default_deserializer(SecretString, "super-secret") assert isinstance(secret, SecretString) assert secret.get_secret() == "super-secret" def test_secret_bytes_decode() -> None: secret = default_deserializer(SecretBytes, b"super-secret") assert isinstance(secret, SecretBytes) assert secret.get_secret() == b"super-secret" def test_secret_string_parameter() -> None: @get() def get_secret(secret: SecretString) -> SecretBytes: assert secret.get_secret() == "super-secret" return SecretBytes(b"super-secret") with create_test_client([get_secret]) as client: response = client.get("/?secret=super-secret") assert response.status_code == 200 assert response.json() == "******" def test_decode_secret_string_on_model() -> None: class Model(msgspec.Struct): secret: SecretString @post(signature_types=[Model]) async def post_secret(data: Model) -> None: assert data.secret.get_secret() == "super" with create_test_client([post_secret]) as client: response = client.post("/", json={"secret": "super"}) assert response.status_code == 201 def test_decode_secret_bytes_on_model() -> None: class Model(msgspec.Struct): secret: SecretBytes @post(signature_types=[Model]) async def post_secret(data: Model) -> None: assert data.secret.get_secret() == b"super" with create_test_client([post_secret]) as client: response = client.post("/", json={"secret": "super"}) assert response.status_code == 201 @pytest.mark.parametrize(("secret_type",), [(SecretString,), (SecretBytes,)]) def test_decode_secret_string_on_model_client_error(secret_type: type[SecretString | SecretBytes]) -> None: model = msgspec.defstruct(name="Model", fields=[("secret", secret_type)]) @post(signature_namespace={"model": model}) async def post_secret(data: model) -> None: # type: ignore[valid-type] return None with create_test_client([post_secret]) as client: response = client.post("/", json={"secret": 123}) assert response.status_code == 400 assert response.json() == { "status_code": 400, "detail": "Validation failed for POST /", "extra": [{"message": "Unsupported type: ", "key": "secret", "source": "body"}], } def test_secret_openapi() -> None: @get(sync_to_thread=False) def get_secret(secret: Annotated[SecretString, Parameter(header="x-secret")]) -> str: return secret.get_obscured() app = Litestar(route_handlers=[get_secret]) paths = app.openapi_schema.paths assert paths is not None op = paths["/"].get assert op is not None assert op.parameters is not None param = op.parameters[0] assert isinstance(param, OpenAPIParameter) assert param.name == "x-secret" assert param.param_in == "header" assert isinstance(param.schema, Schema) assert param.schema.type == "string" def test_secret_value_in_model_repr() -> None: class Model(msgspec.Struct): string: SecretString bytes: SecretBytes model = Model(string=SecretString("super-secret"), bytes=SecretBytes(b"super-secret")) assert repr(model) == "Model(string=SecretString('******'), bytes=SecretBytes(b'******'))" assert str(model) == "Model(string=SecretString('******'), bytes=SecretBytes(b'******'))" litestar-2.16.0/tests/unit/test_datastructures/test_state.py000066400000000000000000000046771500564371300244370ustar00rootroot00000000000000from __future__ import annotations from copy import copy from typing import Any import pytest from litestar.datastructures import State from litestar.datastructures.state import ImmutableState @pytest.mark.parametrize("state_class", (ImmutableState, State)) def test_state_immutable_mapping(state_class: type[ImmutableState]) -> None: state_dict = {"first": 1, "second": 2, "third": 3} state = state_class(state_dict, deep_copy=True) assert len(state) == 3 assert "first" in state assert state["first"] == 1 assert list(state.items()) == [("first", 1), ("second", 2), ("third", 3)] assert state assert isinstance(state.mutable_copy(), State) del state_dict["first"] assert "first" in state @pytest.mark.parametrize( "zero_object", (ImmutableState({"first": 1}), State({"first": 1}), {"first": 1}, [("first", 1)]) ) def test_state_init(zero_object: Any) -> None: state = ImmutableState(zero_object) assert state.first @pytest.mark.parametrize("zero_object", (ImmutableState({}), State(), {}, [], None)) def test_state_mapping(zero_object: Any) -> None: state = State(zero_object) assert not state state["first"] = "first" state["second"] = "second" assert state.first == "first" assert state["second"] == "second" del state["first"] del state.second assert "first" not in state assert "second" not in state assert isinstance(state.immutable_copy(), ImmutableState) def test_state_attributes() -> None: state_dict = {"first": 1, "second": 2, "third": 3} state = State(state_dict) assert state.first == 1 del state.first with pytest.raises(AttributeError): assert state.first state.fourth = 4 assert state.fourth == 4 with pytest.raises(AttributeError): del state.first def test_state_dict() -> None: state_dict = {"first": 1, "second": 2, "third": 3} state = State(state_dict) assert state.dict() == state_dict def test_state_copy() -> None: state_dict = {"key": {"inner": 1}} state = State(state_dict) copy = state.copy() del state.key assert copy.key def test_state_copy_deep_copy_false() -> None: state = State({}, deep_copy=False) assert state.copy()._deep_copy is False def test_unpicklable_deep_copy_false() -> None: # a module cannot be deep copied import typing state = ImmutableState({"module": typing}, deep_copy=False) copy(state) ImmutableState.validate(state) litestar-2.16.0/tests/unit/test_datastructures/test_upload_file.py000066400000000000000000000035571500564371300255760ustar00rootroot00000000000000from os import urandom from pathlib import Path from typing import Optional from litestar import post from litestar.datastructures import UploadFile from litestar.enums import RequestEncodingType from litestar.params import Body from litestar.status_codes import HTTP_201_CREATED from litestar.testing import create_test_client async def test_upload_file_methods() -> None: data = urandom(5) upload_file = UploadFile(content_type="application/text", filename="tmp.txt", max_spool_size=10, file_data=data) assert repr(upload_file) == "tmp.txt - application/text" assert not upload_file.rolled_to_disk assert await upload_file.read() == data assert await upload_file.read() == b"" await upload_file.seek(0) assert await upload_file.read() == data await upload_file.write(b"extra_data") assert upload_file.rolled_to_disk await upload_file.seek(5) # type: ignore[unreachable] assert await upload_file.read() == b"extra_data" await upload_file.write(b"writing_async_extra_data") await upload_file.seek(15) assert await upload_file.read() == b"writing_async_extra_data" await upload_file.close() assert upload_file.file.closed def test_cleanup_is_being_performed(tmpdir: Path) -> None: path1 = tmpdir / "test.txt" Path(path1).write_bytes(b"") upload_file: Optional[UploadFile] = None @post("/form", sync_to_thread=False) def handler(data: UploadFile = Body(media_type=RequestEncodingType.MULTI_PART)) -> None: nonlocal upload_file upload_file = data assert not upload_file.file.closed with create_test_client(handler) as client, open(path1, "rb") as f: response = client.post("/form", files={"test": ("test.txt", f, "text/plain")}) assert response.status_code == HTTP_201_CREATED assert upload_file assert upload_file.file.closed litestar-2.16.0/tests/unit/test_datastructures/test_url.py000066400000000000000000000071301500564371300241040ustar00rootroot00000000000000from typing import TYPE_CHECKING, Callable import pytest from litestar.datastructures import MultiDict from litestar.datastructures.url import URL, make_absolute_url if TYPE_CHECKING: from litestar.types import Scope @pytest.mark.parametrize( "base,path", [ ("http://example.org", "foo/bar?param=value¶m2=value2"), ("http://example.org/", "foo/bar?param=value¶m2=value2"), ("http://example.org", "/foo/bar?param=value¶m2=value2"), ("http://example.org/", "/foo/bar?param=value¶m2=value2"), ], ) def test_make_absolute_url(path: str, base: str) -> None: result = "http://example.org/foo/bar?param=value¶m2=value2" assert make_absolute_url(path, base) == result def test_url() -> None: url = URL("https://foo:hunter2@example.org:81/bar/baz?query=param&bool=true#fragment") assert url.scheme == "https" assert url.netloc == "foo:hunter2@example.org:81" assert url.path == "/bar/baz" assert url.query == "query=param&bool=true" assert url.fragment == "fragment" assert url.username == "foo" assert url.password == "hunter2" assert url.port == 81 assert url.hostname == "example.org" assert url.query_params.dict() == {"query": ["param"], "bool": ["true"]} @pytest.mark.parametrize( "component,value", [ ("scheme", "https"), ("netloc", "example.org"), ("path", "/foo/bar"), ("query", "foo=bar"), ("fragment", "anchor"), ], ) def test_url_from_components(component: str, value: str) -> None: expected = {"scheme": "", "netloc": "", "path": "", "query": "", "fragment": "", component: value} url = URL.from_components(**{component: value}) for key, value in expected.items(): assert getattr(url, key) == value @pytest.mark.parametrize( "component,replacement,expected", [ ("scheme", "http", "http"), ("netloc", "example.com", "example.com"), ("path", "/foo", "/foo"), ("query", None, ""), ("query", "", ""), ("query", MultiDict({}), ""), ("query", "foo=baz", "foo=baz"), ("query", MultiDict({"foo": "baz"}), "foo=baz"), ("fragment", "anchor2", "anchor2"), ], ) def test_url_with_replacements(component: str, replacement: str, expected: str) -> None: defaults = { "scheme": "https", "netloc": "example.org", "path": "/foo/bar", "query": "foo=bar", "fragment": "anchor", } url = URL.from_components(**defaults) defaults[component] = expected url = url.with_replacements(**{component: replacement}) for key, value in defaults.items(): assert getattr(url, key) == value def test_url_from_scope(create_scope: Callable[..., "Scope"]) -> None: scope = create_scope( scheme="https", server=("testserver.local", 70), root_path="/foo", path="/bar", query_string="bar=baz", headers=[], ) url = URL.from_scope(scope) assert url.scheme == "https" assert url.netloc == "testserver.local:70" assert url.path == "/foo/bar" assert url.query == "bar=baz" def test_url_from_scope_with_host(create_scope: Callable[..., "Scope"]) -> None: scope = create_scope(headers=[(b"host", b"testserver.local:42")]) url = URL.from_scope(scope) assert url.netloc == "testserver.local:42" def test_url_eq() -> None: assert URL("") == URL("") assert URL("/foo") == "/foo" assert URL("") != 1 def test_url_repr() -> None: url = URL("https://foo:bar@testserver.local:42") assert repr(url) == "URL('https://foo:bar@testserver.local:42')" litestar-2.16.0/tests/unit/test_deprecations.py000066400000000000000000000163331500564371300216530ustar00rootroot00000000000000import importlib import pytest from litestar.types.asgi_types import ASGIApp @pytest.mark.parametrize( "import_path, import_name", ( ( "litestar.contrib.repository", "AbstractAsyncRepository", ), ( "litestar.contrib.repository", "AbstractSyncRepository", ), ( "litestar.contrib.repository", "ConflictError", ), ( "litestar.contrib.repository", "FilterTypes", ), ( "litestar.contrib.repository", "NotFoundError", ), ( "litestar.contrib.repository", "RepositoryError", ), ( "litestar.contrib.repository.exceptions", "ConflictError", ), ( "litestar.contrib.repository.exceptions", "NotFoundError", ), ( "litestar.contrib.repository.exceptions", "RepositoryError", ), ( "litestar.contrib.repository.filters", "BeforeAfter", ), ( "litestar.contrib.repository.filters", "CollectionFilter", ), ( "litestar.contrib.repository.filters", "FilterTypes", ), ( "litestar.contrib.repository.filters", "LimitOffset", ), ( "litestar.contrib.repository.filters", "OrderBy", ), ( "litestar.contrib.repository.filters", "SearchFilter", ), ( "litestar.contrib.repository.filters", "NotInCollectionFilter", ), ( "litestar.contrib.repository.filters", "OnBeforeAfter", ), ( "litestar.contrib.repository.filters", "NotInSearchFilter", ), ( "litestar.contrib.repository.handlers", "signature_namespace_values", ), ( "litestar.contrib.repository.handlers", "on_app_init", ), ( "litestar.contrib.repository.testing", "GenericAsyncMockRepository", ), ), ) def test_repository_deprecations(import_path: str, import_name: str) -> None: module = importlib.import_module(import_path) assert getattr(module, import_name) def test_litestar_type_deprecation() -> None: with pytest.warns(DeprecationWarning): from litestar.types.internal_types import LitestarType # noqa: F401 def test_contrib_minijnja_deprecation() -> None: with pytest.warns(DeprecationWarning): from litestar.contrib.minijnja import MiniJinjaTemplateEngine # noqa: F401 def test_litestar_templates_template_context_deprecation() -> None: with pytest.warns(DeprecationWarning): from litestar.template.base import TemplateContext # noqa: F401 def test_minijinja_from_state_deprecation() -> None: with pytest.warns(DeprecationWarning): from litestar.contrib.minijinja import minijinja_from_state # noqa: F401 def test_constants_deprecations() -> None: with pytest.warns(DeprecationWarning): from litestar.constants import SCOPE_STATE_NAMESPACE # noqa: F401 def test_utils_deprecations() -> None: with pytest.warns(DeprecationWarning): from litestar.utils import ( # noqa: F401 delete_litestar_scope_state, get_litestar_scope_state, set_litestar_scope_state, ) def test_utils_scope_deprecations() -> None: with pytest.warns(DeprecationWarning): from litestar.utils.scope import ( # noqa: F401 delete_litestar_scope_state, get_litestar_scope_state, set_litestar_scope_state, ) def test_is_sync_or_async_generator_deprecation() -> None: with pytest.warns(DeprecationWarning): from litestar.utils.predicates import is_sync_or_async_generator # noqa: F401 with pytest.warns(DeprecationWarning): from litestar.utils import is_sync_or_async_generator as _ # noqa: F401 def test_openapi_config_openapi_controller_deprecation() -> None: from litestar.openapi.config import OpenAPIConfig from litestar.openapi.controller import OpenAPIController with pytest.warns(DeprecationWarning): OpenAPIConfig(title="API", version="1.0", openapi_controller=OpenAPIController) def test_openapi_config_root_schema_site_deprecation() -> None: from litestar.openapi.config import OpenAPIConfig with pytest.warns(DeprecationWarning): OpenAPIConfig(title="API", version="1.0", root_schema_site="redoc") def test_openapi_config_enabled_endpoints_deprecation() -> None: from litestar.openapi.config import OpenAPIConfig with pytest.warns(DeprecationWarning): OpenAPIConfig(title="API", version="1.0", enabled_endpoints={"redoc"}) def test_cors_middleware_public_interface_deprecation() -> None: with pytest.warns(DeprecationWarning): from litestar.middleware.cors import CORSMiddleware # noqa: F401 def test_exception_handler_middleware_debug_deprecation(mock_asgi_app: ASGIApp) -> None: from litestar.middleware._internal.exceptions import ExceptionHandlerMiddleware with pytest.warns(DeprecationWarning): ExceptionHandlerMiddleware(mock_asgi_app, debug=True) def test_exception_handler_middleware_exception_handlers_deprecation(mock_asgi_app: ASGIApp) -> None: from litestar.middleware._internal.exceptions import ExceptionHandlerMiddleware with pytest.warns(DeprecationWarning): ExceptionHandlerMiddleware(mock_asgi_app, debug=None, exception_handlers={}) def test_deprecate_exception_handler_middleware() -> None: with pytest.warns(DeprecationWarning): from litestar.middleware.exceptions import ExceptionHandlerMiddleware # noqa: F401 with pytest.raises(ImportError): from litestar.middleware.exceptions.middleware import OtherName # noqa: F401 def test_deprecate_exception_handler_middleware_2() -> None: with pytest.warns(DeprecationWarning): from litestar.middleware.exceptions.middleware import ExceptionHandlerMiddleware # noqa: F401 with pytest.raises(ImportError): from litestar.middleware.exceptions import OtherName # noqa: F401 def test_deprecate_create_debug_response() -> None: with pytest.warns(DeprecationWarning): from litestar.middleware.exceptions._debug_response import create_debug_response # noqa: F401 with pytest.raises(ImportError): from litestar.middleware.exceptions._debug_response import OtherName # noqa: F401 def test_deprecate_create_exception_response() -> None: with pytest.warns(DeprecationWarning): from litestar.middleware.exceptions.middleware import create_exception_response # noqa: F401 with pytest.raises(ImportError): from litestar.middleware.exceptions.middleware import OtherName # noqa: F401 def test_deprecate_exception_response_content() -> None: with pytest.warns(DeprecationWarning): from litestar.middleware.exceptions.middleware import ExceptionResponseContent # noqa: F401 with pytest.raises(ImportError): from litestar.middleware.exceptions.middleware import OtherName # noqa: F401 litestar-2.16.0/tests/unit/test_di.py000066400000000000000000000107761500564371300175740ustar00rootroot00000000000000from functools import partial from typing import Any, AsyncGenerator, Generator import pytest from litestar.di import Provide from litestar.exceptions import ImproperlyConfiguredException, LitestarWarning from litestar.types import Empty def generator_func() -> Generator[float, None, None]: yield 0.1 async def async_generator_func() -> AsyncGenerator[float, None]: yield 0.1 async def async_callable(val: str = "three-one") -> str: return val def sync_callable(val: str = "three-one") -> str: return val async_partial = partial(async_callable, "why-three-and-one") sync_partial = partial(sync_callable, "why-three-and-one") class C: val = 31 def __init__(self) -> None: self.val = 13 @classmethod async def async_class(cls) -> int: return cls.val @classmethod def sync_class(cls) -> int: return cls.val @staticmethod async def async_static() -> str: return "one-three" @staticmethod def sync_static() -> str: return "one-three" async def async_instance(self) -> int: return self.val def sync_instance(self) -> int: return self.val async def test_provide_default(anyio_backend: str) -> None: provider = Provide(dependency=async_callable) value = await provider() assert value == "three-one" async def test_provide_cached(anyio_backend: str) -> None: provider = Provide(dependency=async_callable, use_cache=True) assert provider.value is Empty value = await provider() assert value == "three-one" assert provider.value == value second_value = await provider() assert value == second_value third_value = await provider() assert value == third_value async def test_run_in_thread(anyio_backend: str) -> None: provider = Provide(dependency=sync_callable, sync_to_thread=True) value = await provider() assert value == "three-one" def test_provider_equality_check() -> None: first_provider = Provide(dependency=sync_callable, sync_to_thread=False) second_provider = Provide(dependency=sync_callable, sync_to_thread=False) assert first_provider == second_provider third_provider = Provide(dependency=sync_callable, use_cache=True, sync_to_thread=False) assert first_provider != third_provider second_provider.value = True assert first_provider != second_provider @pytest.mark.parametrize( "fn, exp", [ (C.async_class, 31), (C.sync_class, 31), (C.async_static, "one-three"), (C.sync_static, "one-three"), (C().async_instance, 13), (C().sync_instance, 13), (async_callable, "three-one"), (sync_callable, "three-one"), (async_partial, "why-three-and-one"), (sync_partial, "why-three-and-one"), ], ) @pytest.mark.usefixtures("disable_warn_sync_to_thread_with_async") async def test_provide_for_callable(fn: Any, exp: Any, anyio_backend: str) -> None: assert await Provide(fn, sync_to_thread=False)() == exp @pytest.mark.usefixtures("enable_warn_implicit_sync_to_thread") def test_sync_callable_without_sync_to_thread_warns() -> None: def func() -> None: pass with pytest.warns(LitestarWarning, match="discouraged since synchronous callables"): Provide(func) @pytest.mark.parametrize("sync_to_thread", [True, False]) def test_async_callable_with_sync_to_thread_warns(sync_to_thread: bool) -> None: async def func() -> None: pass with pytest.warns(LitestarWarning, match="asynchronous callable"): Provide(func, sync_to_thread=sync_to_thread) def test_generator_with_sync_to_thread_warns() -> None: def func() -> Generator[int, None, None]: yield 1 with pytest.warns(LitestarWarning, match="Use of generator"): Provide(func, sync_to_thread=True) @pytest.mark.parametrize( ("dep", "exp"), [ (sync_callable, True), (async_callable, False), (generator_func, True), (async_generator_func, True), ], ) def test_dependency_has_async_callable(dep: Any, exp: bool) -> None: assert Provide(dep).has_sync_callable is exp def test_raises_when_dependency_is_not_callable() -> None: with pytest.raises(ImproperlyConfiguredException): Provide(123) # type: ignore[arg-type] @pytest.mark.parametrize( ("dep",), [ (generator_func,), (async_generator_func,), ], ) def test_raises_when_generator_dependency_is_cached(dep: Any) -> None: with pytest.raises(ImproperlyConfiguredException): Provide(dep, use_cache=True) litestar-2.16.0/tests/unit/test_dto/000077500000000000000000000000001500564371300174015ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_dto/__init__.py000066400000000000000000000001661500564371300215150ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass @dataclass class Model: a: int b: str litestar-2.16.0/tests/unit/test_dto/conftest.py000066400000000000000000000070341500564371300216040ustar00rootroot00000000000000# ruff: noqa: UP006 from __future__ import annotations from typing import Any, Collection, Generator, TypeVar, get_type_hints import pytest from litestar import Request, get from litestar._openapi.schema_generation import SchemaCreator from litestar.dto import AbstractDTO, DTOConfig, DTOField, DTOFieldDefinition from litestar.enums import MediaType from litestar.openapi.spec import Reference, Schema from litestar.testing import RequestFactory from litestar.types.serialization import LitestarEncodableType from litestar.typing import FieldDefinition from . import Model T = TypeVar("T", bound=Model) @pytest.fixture def ModelDataDTO(use_experimental_dto_backend: bool) -> type[AbstractDTO]: class DTOCls(AbstractDTO[Model]): config = DTOConfig(experimental_codegen_backend=use_experimental_dto_backend) def decode_builtins(self, value: Any) -> Model: return Model(a=1, b="2") def decode_bytes(self, value: bytes) -> Model: return Model(a=1, b="2") def data_to_encodable_type(self, data: Model | Collection[Model]) -> bytes | LitestarEncodableType: return Model(a=1, b="2") @classmethod def create_openapi_schema( cls, field_definition: FieldDefinition, handler_id: str, schema_creator: SchemaCreator ) -> Reference | Schema: return Schema() @classmethod def generate_field_definitions(cls, model_type: type[Any]) -> Generator[DTOFieldDefinition, None, None]: for k, v in get_type_hints(model_type).items(): yield DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg(annotation=v, name=k), model_name="Model", default_factory=None, dto_field=DTOField(), ) @classmethod def detect_nested_field(cls, field_definition: FieldDefinition) -> bool: return False return DTOCls @pytest.fixture() def ModelReturnDTO() -> type[AbstractDTO]: class ReturnDO(AbstractDTO[Model]): def decode_builtins(self, value: Any) -> Any: raise RuntimeError("Return DTO should not have this method called") def decode_bytes(self, value: Any) -> Any: raise RuntimeError("Return DTO should not have this method called") def data_to_encodable_type(self, data: Model | Collection[Model]) -> bytes | LitestarEncodableType: return b'{"a": 1, "b": "2"}' @classmethod def create_openapi_schema( cls, field_definition: FieldDefinition, handler_id: str, schema_creator: SchemaCreator ) -> Reference | Schema: return Schema() @classmethod def generate_field_definitions(cls, model_type: type[Any]) -> Generator[DTOFieldDefinition, None, None]: for k, v in get_type_hints(model_type).items(): yield DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg(annotation=v, name=k), model_name="Model", default_factory=None, dto_field=DTOField(), ) @classmethod def detect_nested_field(cls, field_definition: FieldDefinition) -> bool: return False return ReturnDO @pytest.fixture() def asgi_connection() -> Request[Any, Any, Any]: @get("/", name="handler_id", media_type=MediaType.JSON) def _handler() -> None: ... return RequestFactory().get(path="/", route_handler=_handler) litestar-2.16.0/tests/unit/test_dto/test_config.py000066400000000000000000000004101500564371300222520ustar00rootroot00000000000000import pytest from litestar.dto import DTOConfig from litestar.exceptions import ImproperlyConfiguredException def test_include_and_exclude_raises() -> None: with pytest.raises(ImproperlyConfiguredException): DTOConfig(include={"a"}, exclude={"b"}) litestar-2.16.0/tests/unit/test_dto/test_factory/000077500000000000000000000000001500564371300221075ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_dto/test_factory/__init__.py000066400000000000000000000002101500564371300242110ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass @dataclass(unsafe_hash=True) class Model: a: int b: str litestar-2.16.0/tests/unit/test_dto/test_factory/test_backends/000077500000000000000000000000001500564371300247205ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_dto/test_factory/test_backends/__init__.py000066400000000000000000000000001500564371300270170ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_dto/test_factory/test_backends/conftest.py000066400000000000000000000005321500564371300271170ustar00rootroot00000000000000from __future__ import annotations import pytest from litestar.dto._backend import DTOBackend from litestar.dto._codegen_backend import DTOCodegenBackend @pytest.fixture() def backend_cls(use_experimental_dto_backend: bool) -> type[DTOBackend | DTOCodegenBackend]: return DTOCodegenBackend if use_experimental_dto_backend else DTOBackend litestar-2.16.0/tests/unit/test_dto/test_factory/test_backends/test_backends.py000066400000000000000000000440741500564371300301140ustar00rootroot00000000000000# ruff: noqa: UP006,UP007 from __future__ import annotations import inspect from dataclasses import dataclass, field from types import ModuleType from typing import TYPE_CHECKING, Callable, Dict, List, Optional from unittest.mock import MagicMock import pytest from msgspec import Meta, Struct, to_builtins from litestar import Litestar, Request, get, post from litestar._openapi.schema_generation import SchemaCreator from litestar.dto import DataclassDTO, DTOConfig, DTOField from litestar.dto._backend import DTOBackend, _create_struct_field_meta_for_field_definition from litestar.dto._codegen_backend import DTOCodegenBackend from litestar.dto._types import CollectionType, SimpleType, TransferDTOFieldDefinition from litestar.dto.data_structures import DTOFieldDefinition from litestar.enums import MediaType from litestar.exceptions import SerializationException from litestar.openapi.spec.example import Example from litestar.openapi.spec.reference import Reference from litestar.openapi.spec.schema import Schema from litestar.params import KwargDefinition from litestar.serialization import encode_json from litestar.testing import RequestFactory from litestar.typing import FieldDefinition if TYPE_CHECKING: from typing import Any @dataclass class NestedDC: a: int b: str @dataclass class DC: a: int nested: NestedDC nested_list: List[NestedDC] nested_mapping: Dict[str, NestedDC] integer: int b: str = field(default="b") c: List[int] = field(default_factory=list) optional: Optional[str] = None DESTRUCTURED = { "a": 1, "b": "b", "c": [], "nested": {"a": 1, "b": "two"}, "nested_list": [{"a": 1, "b": "two"}], "nested_mapping": {"a": {"a": 1, "b": "two"}}, "integer": 1, "optional": None, } RAW = b'{"a":1,"nested":{"a":1,"b":"two"},"nested_list":[{"a":1,"b":"two"}],"nested_mapping":{"a":{"a":1,"b":"two"}},"integer":1,"b":"b","c":[],"optional":null}' MSGPACK_RAW = b"\x88\xa1a\x01\xa6nested\x82\xa1a\x01\xa1b\xa3two\xabnested_list\x91\x82\xa1a\x01\xa1b\xa3two\xaenested_mapping\x81\xa1a\x82\xa1a\x01\xa1b\xa3two\xa7integer\x01\xa1b\xa1b\xa1c\x90\xa8optional\xc0" COLLECTION_RAW = b'[{"a":1,"nested":{"a":1,"b":"two"},"nested_list":[{"a":1,"b":"two"}],"nested_mapping":{"a":{"a":1,"b":"two"}},"integer":1,"b":"b","c":[],"optional":null}]' STRUCTURED = DC( a=1, b="b", c=[], nested=NestedDC(a=1, b="two"), nested_list=[NestedDC(a=1, b="two")], nested_mapping={"a": NestedDC(a=1, b="two")}, optional=None, integer=1, ) @pytest.fixture(name="dto_factory") def fx_backend_factory(use_experimental_dto_backend: bool) -> type[DataclassDTO]: class Factory(DataclassDTO): config = DTOConfig(experimental_codegen_backend=use_experimental_dto_backend) model_type = DC return Factory @pytest.fixture(name="asgi_connection") def fx_asgi_connection() -> Request[Any, Any, Any]: @get("/", name="handler_id", media_type=MediaType.JSON) def _handler() -> None: ... return RequestFactory().get(path="/", route_handler=_handler) def test_backend_parse_raw_json( dto_factory: type[DataclassDTO], asgi_connection: Request[Any, Any, Any], backend_cls: type[DTOBackend] ) -> None: assert ( to_builtins( backend_cls( dto_factory=dto_factory, field_definition=FieldDefinition.from_annotation(DC), model_type=DC, wrapper_attribute_name=None, is_data_field=True, handler_id="test", ).parse_raw(RAW, asgi_connection) ) == DESTRUCTURED ) def test_backend_parse_raw_msgpack(dto_factory: type[DataclassDTO], backend_cls: type[DTOBackend]) -> None: @get("/", name="handler_id", media_type=MediaType.MESSAGEPACK) def _handler() -> None: ... asgi_connection = RequestFactory().get( path="/", route_handler=_handler, headers={"Content-Type": MediaType.MESSAGEPACK} ) assert ( to_builtins( backend_cls( dto_factory=dto_factory, field_definition=FieldDefinition.from_annotation(DC), model_type=DC, wrapper_attribute_name=None, is_data_field=True, handler_id="test", ).parse_raw(MSGPACK_RAW, asgi_connection) ) == DESTRUCTURED ) def test_backend_parse_unsupported_media_type( dto_factory: type[DataclassDTO], asgi_connection: Request[Any, Any, Any], backend_cls: type[DTOBackend] ) -> None: @get("/", name="handler_id", media_type="text/css") def _handler() -> None: ... asgi_connection = RequestFactory().get(path="/", route_handler=_handler, headers={"Content-Type": "text/css"}) with pytest.raises(SerializationException): backend_cls( dto_factory=dto_factory, field_definition=FieldDefinition.from_annotation(DC), model_type=DC, wrapper_attribute_name=None, is_data_field=True, handler_id="test", ).parse_raw(b"", asgi_connection) def test_backend_iterable_annotation(dto_factory: type[DataclassDTO], backend_cls: type[DTOBackend]) -> None: backend = DTOBackend( handler_id="test", dto_factory=dto_factory, field_definition=FieldDefinition.from_annotation(List[DC]), model_type=DC, wrapper_attribute_name=None, is_data_field=True, ) field_definition = FieldDefinition.from_annotation(backend.annotation) assert field_definition.origin is list assert field_definition.has_inner_subclass_of(Struct) def test_backend_scalar_annotation(dto_factory: type[DataclassDTO], backend_cls: type[DTOBackend]) -> None: backend = backend_cls( handler_id="test", dto_factory=dto_factory, field_definition=FieldDefinition.from_annotation(DC), model_type=DC, wrapper_attribute_name=None, is_data_field=True, ) assert FieldDefinition.from_annotation(backend.annotation).is_subclass_of(Struct) def test_backend_populate_data_from_builtins( dto_factory: type[DataclassDTO], asgi_connection: Request[Any, Any, Any], backend_cls: type[DTOBackend] ) -> None: backend = backend_cls( handler_id="test", dto_factory=dto_factory, field_definition=FieldDefinition.from_annotation(DC), model_type=DC, wrapper_attribute_name=None, is_data_field=True, ) data = backend.populate_data_from_builtins(builtins=DESTRUCTURED, asgi_connection=asgi_connection) assert data == STRUCTURED def test_backend_create_openapi_schema(dto_factory: type[DataclassDTO]) -> None: @post("/", dto=dto_factory, name="test") def handler(data: DC) -> DC: return data app = Litestar(route_handlers=[handler]) creator = SchemaCreator(plugins=app.plugins.openapi) ref = dto_factory.create_openapi_schema( handler_id=app.get_handler_index_by_name("test")["handler"].handler_id, # type: ignore[index] field_definition=FieldDefinition.from_annotation(DC), schema_creator=creator, ) schemas = creator.schema_registry.generate_components_schemas() assert isinstance(ref, Reference) schema = schemas[ref.value] assert schema.title == "HandlerDCResponseBody" assert schema.properties is not None a, b, c = schema.properties["a"], schema.properties["b"], schema.properties["c"] assert isinstance(a, Schema) assert a.type == "integer" assert isinstance(b, Schema) assert b.type == "string" assert isinstance(c, Schema) assert c.type == "array" assert isinstance(c.items, Schema) assert c.items.type == "integer" assert isinstance(nested := schema.properties["nested"], Reference) # noqa: RUF018 nested_schema = schemas[nested.value] assert nested_schema.title == "HandlerDCNestedDCResponseBody" assert nested_schema.properties is not None nested_a, nested_b = nested_schema.properties["a"], nested_schema.properties["b"] assert isinstance(nested_a, Schema) assert nested_a.type == "integer" assert isinstance(nested_b, Schema) assert nested_b.type == "string" def test_backend_model_name_uniqueness(dto_factory: type[DataclassDTO], backend_cls: type[DTOBackend]) -> None: backend = backend_cls( handler_id="test", dto_factory=dto_factory, field_definition=FieldDefinition.from_annotation(DC), model_type=DC, wrapper_attribute_name=None, is_data_field=True, ) backend._seen_model_names.clear() unique_names: set = set() field_definition = TransferDTOFieldDefinition.from_dto_field_definition( field_definition=DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg(annotation=int, name="a"), default_factory=None, dto_field=DTOField(), model_name="some_module.SomeModel", ), serialization_name="a", transfer_type=SimpleType(field_definition=FieldDefinition.from_annotation(int), nested_field_info=None), is_partial=False, is_excluded=False, ) for _ in range(100): model_class = backend.create_transfer_model_type("some_module.SomeModel", field_definitions=(field_definition,)) unique_names.add(model_class.__name__) assert len(unique_names) == 100 assert backend._seen_model_names == unique_names def test_backend_populate_data_from_raw( dto_factory: type[DataclassDTO], asgi_connection: Request[Any, Any, Any], backend_cls: type[DTOBackend] ) -> None: backend = backend_cls( handler_id="test", dto_factory=dto_factory, field_definition=FieldDefinition.from_annotation(DC), model_type=DC, wrapper_attribute_name=None, is_data_field=True, ) data = backend.populate_data_from_raw(RAW, asgi_connection) assert data == STRUCTURED def test_backend_populate_collection_data_from_raw( dto_factory: type[DataclassDTO], asgi_connection: Request[Any, Any, Any], backend_cls: type[DTOBackend] ) -> None: backend = backend_cls( handler_id="test", dto_factory=dto_factory, field_definition=FieldDefinition.from_annotation(List[DC]), model_type=DC, wrapper_attribute_name=None, is_data_field=True, ) data = backend.populate_data_from_raw(COLLECTION_RAW, asgi_connection) assert data == [STRUCTURED] def test_backend_encode_data( dto_factory: type[DataclassDTO], asgi_connection: Request[Any, Any, Any], backend_cls: type[DTOBackend] ) -> None: backend = backend_cls( handler_id="test", dto_factory=dto_factory, field_definition=FieldDefinition.from_annotation(DC), model_type=DC, wrapper_attribute_name=None, is_data_field=True, ) data = backend.encode_data(STRUCTURED) assert encode_json(data) == RAW def test_backend_encode_collection_data( dto_factory: type[DataclassDTO], asgi_connection: Request[Any, Any, Any], backend_cls: type[DTOBackend] ) -> None: backend = backend_cls( handler_id="test", dto_factory=dto_factory, field_definition=FieldDefinition.from_annotation(List[DC]), model_type=DC, wrapper_attribute_name=None, is_data_field=True, ) data = backend.encode_data([STRUCTURED]) assert encode_json(data) == COLLECTION_RAW def test_transfer_only_touches_included_attributes(backend_cls: type[DTOBackend]) -> None: """Ensure attribute that are not included are never touched in any way during transfer. https://github.com/litestar-org/litestar/issues/2125 """ mock = MagicMock() @dataclass() class Foo: id: str bar: str = "" class Factory(DataclassDTO): config = DTOConfig(include={"id"}) backend = backend_cls( handler_id="test", dto_factory=Factory, field_definition=TransferDTOFieldDefinition.from_annotation(Foo), model_type=Foo, wrapper_attribute_name=None, is_data_field=False, ) Foo.bar = property(fget=lambda s: mock(return_value=""), fset=lambda s, v: None) # type: ignore[assignment] backend.encode_data(Foo(id="1")) assert mock.call_count == 0 def test_custom_attribute_accessor(backend_cls: type[DTOBackend]) -> None: mock = MagicMock() @dataclass() class Foo: id: str bar: str = "" def my_getattr(obj: object, attr: str) -> Any: mock(attr) return getattr(obj, attr) class MyDataclassDTO(DataclassDTO): attribute_accessor = my_getattr class Factory(MyDataclassDTO): config = DTOConfig(include={"id"}) backend = backend_cls( handler_id="test", dto_factory=Factory, field_definition=TransferDTOFieldDefinition.from_annotation(Foo), model_type=Foo, wrapper_attribute_name=None, is_data_field=False, ) backend.encode_data(Foo(id="1")) mock.assert_called_once_with("id") def test_codegen_attribute_accessor_not_used_when_default() -> None: # when no custom 'attribute_accessor' is used, we expect the logic to be skipped # entirely, and plain 'obj.value' attribute access to be performed, as this is more # performant than using the default 'getattr' @dataclass() class Foo: some_field: str class Factory(DataclassDTO): config = DTOConfig() backend = DTOCodegenBackend( handler_id="test", dto_factory=Factory, field_definition=TransferDTOFieldDefinition.from_annotation(Foo), model_type=Foo, wrapper_attribute_name=None, is_data_field=False, ) to_model_source = inspect.getsource(backend._transfer_to_model_type) encode_source = inspect.getsource(backend._encode_data) assert ".some_field" in to_model_source assert ".some_field" in encode_source assert "__getattr_impl" not in to_model_source assert "__getattr_impl" not in encode_source def test_parse_model_nested_exclude(create_module: Callable[[str], ModuleType], backend_cls: type[DTOBackend]) -> None: module = create_module( """ from dataclasses import dataclass from typing import List from litestar.dto import DataclassDTO @dataclass class NestedNestedModel: e: int f: int @dataclass class NestedModel: c: int d: List[NestedNestedModel] @dataclass class Model: a: int b: NestedModel dto_type = DataclassDTO[Model] """ ) class Factory(DataclassDTO): config = DTOConfig(max_nested_depth=2, exclude={"a", "b.c", "b.d.0.e"}) backend = backend_cls( handler_id="test", dto_factory=Factory, field_definition=FieldDefinition.from_annotation(module.Model), model_type=module.Model, wrapper_attribute_name=None, is_data_field=True, ) parsed = backend.parsed_field_definitions assert next(f for f in parsed if f.name == "a").is_excluded assert parsed[1].name == "b" b_transfer_type = parsed[1].transfer_type assert isinstance(b_transfer_type, SimpleType) b_nested_info = b_transfer_type.nested_field_info assert b_nested_info is not None assert next(f for f in b_nested_info.field_definitions if f.name == "c").is_excluded assert b_nested_info.field_definitions[1].name == "d" b_d_transfer_type = b_nested_info.field_definitions[1].transfer_type assert isinstance(b_d_transfer_type, CollectionType) assert isinstance(b_d_transfer_type.inner_type, SimpleType) b_d_nested_info = b_d_transfer_type.inner_type.nested_field_info assert b_d_nested_info is not None assert next(f for f in b_d_nested_info.field_definitions if f.name == "e").is_excluded assert b_d_nested_info.field_definitions[1].name == "f" def test_parse_model_nested_include(create_module: Callable[[str], ModuleType], backend_cls: type[DTOBackend]) -> None: module = create_module( """ from dataclasses import dataclass from typing import List from litestar.dto import DataclassDTO @dataclass class NestedNestedModel: e: int f: int @dataclass class NestedModel: c: int d: List[NestedNestedModel] @dataclass class Model: a: int b: NestedModel dto_type = DataclassDTO[Model] """ ) class Factory(DataclassDTO): config = DTOConfig(max_nested_depth=2, include={"a", "b.c", "b.d.0.e"}) backend = backend_cls( handler_id="test", dto_factory=Factory, field_definition=FieldDefinition.from_annotation(module.Model), model_type=module.Model, wrapper_attribute_name=None, is_data_field=True, ) parsed = backend.parsed_field_definitions assert not next(f for f in parsed if f.name == "a").is_excluded assert parsed[1].name == "b" b_transfer_type = parsed[1].transfer_type assert isinstance(b_transfer_type, SimpleType) b_nested_info = b_transfer_type.nested_field_info assert b_nested_info is not None assert not next(f for f in b_nested_info.field_definitions if f.name == "c").is_excluded assert b_nested_info.field_definitions[1].name == "d" b_d_transfer_type = b_nested_info.field_definitions[1].transfer_type assert isinstance(b_d_transfer_type, CollectionType) assert isinstance(b_d_transfer_type.inner_type, SimpleType) b_d_nested_info = b_d_transfer_type.inner_type.nested_field_info assert b_d_nested_info is not None assert not next(f for f in b_d_nested_info.field_definitions if f.name == "e").is_excluded assert b_d_nested_info.field_definitions[1].name == "f" @pytest.mark.parametrize( ("constraint_kwargs",), ( ({},), ({"gt": 0, "lt": 2},), ({"ge": 0, "le": 2},), ({"min_length": 1, "max_length": 2},), ({"pattern": "test"},), ), ) def test_create_struct_field_meta_for_field_definition(constraint_kwargs: Any) -> None: mock_field = MagicMock(spec=TransferDTOFieldDefinition, is_partial=False) mock_field.kwarg_definition = KwargDefinition( description="test", examples=[Example(value=1)], title="test", **constraint_kwargs, ) assert _create_struct_field_meta_for_field_definition(mock_field) == Meta( description="test", examples=[1], title="test", **constraint_kwargs, ) litestar-2.16.0/tests/unit/test_dto/test_factory/test_backends/test_base_dto.py000066400000000000000000000240041500564371300301110ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Dict, Generator, List, Tuple, Union import pytest from litestar.connection import ASGIConnection from litestar.dto import DataclassDTO, DTOConfig, DTOField from litestar.dto._backend import DTOBackend from litestar.dto._types import ( CollectionType, CompositeType, MappingType, SimpleType, TransferDTOFieldDefinition, TupleType, UnionType, ) from litestar.dto.data_structures import DTOFieldDefinition from litestar.types import DataclassProtocol from litestar.typing import FieldDefinition if TYPE_CHECKING: from typing import AbstractSet from litestar.dto._types import TransferType @dataclass class Model: a: int b: str @dataclass class Model2: c: int d: str @pytest.fixture(name="data_model_type") def fx_data_model_type() -> Any: return type("Model", (Model,), {}) @pytest.fixture(name="data_model") def fx_data_model(data_model_type: type[Model]) -> Model: return data_model_type(a=1, b="2") @pytest.fixture(name="field_definitions") def fx_field_definitions(data_model_type: type[Model]) -> list[DTOFieldDefinition]: return [ DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( annotation=int, name="a", ), default_factory=None, dto_field=DTOField(), model_name="some_module.SomeModel", ), DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( annotation=str, name="b", ), default_factory=None, dto_field=DTOField(), model_name="some_module.SomeModel", ), ] @pytest.fixture(name="backend") def fx_backend( data_model_type: type[Model], field_definitions: list[DTOFieldDefinition], backend_cls: type[DTOBackend], use_experimental_dto_backend: bool, ) -> DTOBackend: class _Factory(DataclassDTO): config = DTOConfig(experimental_codegen_backend=use_experimental_dto_backend) @classmethod def generate_field_definitions( cls, model_type: type[DataclassProtocol] ) -> Generator[DTOFieldDefinition, None, None]: yield from field_definitions class _Backend(backend_cls): # type: ignore[valid-type,misc] def create_transfer_model_type( self, model_name: str, field_definitions: tuple[TransferDTOFieldDefinition, ...] ) -> type[Any]: """Create a model for data transfer. Args: unique_name: name for the type that should be unique across all transfer types. field_definitions: field definitions for the container type. Returns: A ``BackendT`` class. """ return Model def parse_raw(self, raw: bytes, asgi_connection: ASGIConnection) -> Any: """Parse raw bytes into transfer model type. Args: raw: bytes asgi_connection: Information about the active connection. Returns: The raw bytes parsed into transfer model type. """ return None def parse_builtins(self, builtins: Any, asgi_connection: ASGIConnection) -> Any: """Parse builtin types into transfer model type. Args: builtins: Builtin type. asgi_connection: Information about the active connection. Returns: The builtin type parsed into transfer model type. """ return None return _Backend( dto_factory=_Factory, is_data_field=True, field_definition=FieldDefinition.from_annotation(data_model_type), model_type=data_model_type, wrapper_attribute_name=None, handler_id="test", ) def create_transfer_type( backend: DTOBackend, field_definition: FieldDefinition, exclude: AbstractSet[str] | None = None, include: AbstractSet[str] | None = None, field_name: str = "name", unique_name: str = "some_module.SomeModel.name", nested_depth: int = 0, rename_fields: dict[str, str] | None = None, ) -> TransferType: return backend._create_transfer_type( field_definition=field_definition, exclude=exclude or set(), include=include or set(), field_name=field_name, unique_name=unique_name, nested_depth=nested_depth, rename_fields=rename_fields or {}, ) @pytest.mark.parametrize( ("field_definition", "should_have_nested", "has_nested_field_info"), [ (FieldDefinition.from_annotation(Union[Model, None]), True, (True, False)), (FieldDefinition.from_annotation(Union[Model, str]), True, (True, False)), (FieldDefinition.from_annotation(Union[Model, int]), True, (True, False)), (FieldDefinition.from_annotation(Union[Model, Model2]), True, (True, True)), (FieldDefinition.from_annotation(Union[int, str]), False, (False, False)), ], ) def test_create_transfer_type_union( field_definition: FieldDefinition, should_have_nested: bool, has_nested_field_info: tuple[bool, ...], backend: DTOBackend, ) -> None: transfer_type = create_transfer_type(backend, field_definition) assert isinstance(transfer_type, UnionType) assert transfer_type.has_nested is should_have_nested inner_types = transfer_type.inner_types assert len(inner_types) == len(transfer_type.field_definition.inner_types) for inner_type, has_nested in zip(inner_types, has_nested_field_info): assert isinstance(inner_type, SimpleType) assert bool(inner_type.nested_field_info) is has_nested @pytest.mark.parametrize( ("field_definition", "should_have_nested", "has_nested_field_info"), [ (FieldDefinition.from_annotation(Tuple[Model, None]), True, (True, False)), (FieldDefinition.from_annotation(Tuple[Model, str]), True, (True, False)), (FieldDefinition.from_annotation(Tuple[Model, int]), True, (True, False)), (FieldDefinition.from_annotation(Tuple[Model, Model2]), True, (True, True)), (FieldDefinition.from_annotation(Tuple[int, str]), False, (False, False)), (FieldDefinition.from_annotation(Tuple[Model, ...]), True, (True,)), (FieldDefinition.from_annotation(Tuple[int, ...]), False, (False,)), ], ) def test_create_transfer_type_tuple( field_definition: FieldDefinition, should_have_nested: bool, has_nested_field_info: tuple[bool, ...], backend: DTOBackend, ) -> None: transfer_type = create_transfer_type(backend, field_definition) assert isinstance(transfer_type, CompositeType) assert transfer_type.has_nested is should_have_nested if field_definition.inner_types[-1].annotation is Ellipsis: assert isinstance(transfer_type, CollectionType) inner_type = transfer_type.inner_type assert isinstance(inner_type, SimpleType) assert bool(inner_type.nested_field_info) is has_nested_field_info[0] else: assert isinstance(transfer_type, TupleType) inner_types = transfer_type.inner_types assert len(inner_types) == len(transfer_type.field_definition.inner_types) for inner_type, has_nested in zip(inner_types, has_nested_field_info): assert isinstance(inner_type, SimpleType) assert bool(inner_type.nested_field_info) is has_nested @pytest.mark.parametrize( ("field_definition", "should_have_nested", "has_nested_field_info"), [ (FieldDefinition.from_annotation(Dict[Model, None]), True, (True, False)), (FieldDefinition.from_annotation(Dict[Model, str]), True, (True, False)), (FieldDefinition.from_annotation(Dict[Model, int]), True, (True, False)), (FieldDefinition.from_annotation(Dict[Model, Model2]), True, (True, True)), (FieldDefinition.from_annotation(Dict[int, str]), False, (False, False)), ], ) def test_create_transfer_type_mapping( field_definition: FieldDefinition, should_have_nested: bool, has_nested_field_info: tuple[bool, ...], backend: DTOBackend, ) -> None: transfer_type = create_transfer_type(backend, field_definition) assert isinstance(transfer_type, MappingType) assert transfer_type.has_nested is should_have_nested key_type = transfer_type.key_type value_type = transfer_type.value_type for inner_type, has_nested in zip((key_type, value_type), has_nested_field_info): assert isinstance(inner_type, SimpleType) assert bool(inner_type.nested_field_info) is has_nested @pytest.mark.parametrize( ("field_definition", "should_have_nested", "has_nested_field_info"), [ (FieldDefinition.from_annotation(List[Model]), True, True), (FieldDefinition.from_annotation(List[int]), False, False), ], ) def test_create_transfer_type_collection( field_definition: FieldDefinition, should_have_nested: bool, has_nested_field_info: bool, backend: DTOBackend, ) -> None: transfer_type = create_transfer_type(backend, field_definition) assert isinstance(transfer_type, CollectionType) assert transfer_type.has_nested is should_have_nested inner_type = transfer_type.inner_type assert isinstance(inner_type, SimpleType) assert bool(inner_type.nested_field_info) is has_nested_field_info def test_create_collection_type_nested_union(backend: DTOBackend) -> None: field_definition = FieldDefinition.from_annotation(List[Union[Model, Model2]]) transfer_type = create_transfer_type(backend, field_definition) assert isinstance(transfer_type, CollectionType) assert transfer_type.has_nested is True inner_type = transfer_type.inner_type assert isinstance(inner_type, UnionType) assert inner_type.has_nested is True inner_types = inner_type.inner_types assert len(inner_types) == len(inner_type.field_definition.inner_types) for inner_type in inner_types: assert isinstance(inner_type, SimpleType) assert bool(inner_type.nested_field_info) litestar-2.16.0/tests/unit/test_dto/test_factory/test_backends/test_utils.py000066400000000000000000000167161500564371300275040ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import Dict, List, Tuple, Union import pytest from msgspec import Struct from litestar.dto import DTOField from litestar.dto._backend import _create_transfer_model_type_annotation, _should_mark_private from litestar.dto._types import ( CollectionType, CompositeType, MappingType, NestedFieldInfo, SimpleType, TupleType, ) from litestar.dto.data_structures import DTOFieldDefinition from litestar.typing import FieldDefinition @dataclass class DataModel: a: int b: str class TransferModel(Struct): a: int b: str def test_create_transfer_model_type_annotation_simple_type_without_nested_field_info() -> None: transfer_type = SimpleType(field_definition=FieldDefinition.from_annotation(int), nested_field_info=None) annotation = _create_transfer_model_type_annotation(transfer_type=transfer_type) assert annotation == int def test_create_transfer_model_type_annotation_simple_type_with_nested_field_info() -> None: transfer_type = SimpleType( field_definition=FieldDefinition.from_annotation(DataModel), nested_field_info=NestedFieldInfo(model=TransferModel, field_definitions=()), ) annotation = _create_transfer_model_type_annotation(transfer_type=transfer_type) assert annotation == TransferModel def test_create_transfer_model_type_annotation_collection_type_not_nested() -> None: transfer_type = CollectionType( field_definition=FieldDefinition.from_annotation(List[int]), inner_type=SimpleType(field_definition=FieldDefinition.from_annotation(int), nested_field_info=None), has_nested=False, ) annotation = _create_transfer_model_type_annotation(transfer_type=transfer_type) assert annotation == List[int] def test_create_transfer_model_type_annotation_collection_type_nested() -> None: transfer_type = CollectionType( field_definition=FieldDefinition.from_annotation(List[DataModel]), inner_type=SimpleType( field_definition=FieldDefinition.from_annotation(DataModel), nested_field_info=NestedFieldInfo(model=TransferModel, field_definitions=()), ), has_nested=True, ) annotation = _create_transfer_model_type_annotation(transfer_type=transfer_type) assert annotation == List[TransferModel] def test_create_transfer_model_type_annotation_mapping_type_not_nested() -> None: transfer_type = MappingType( field_definition=FieldDefinition.from_annotation(Dict[str, int]), key_type=SimpleType(field_definition=FieldDefinition.from_annotation(str), nested_field_info=None), value_type=SimpleType(field_definition=FieldDefinition.from_annotation(int), nested_field_info=None), has_nested=False, ) annotation = _create_transfer_model_type_annotation(transfer_type=transfer_type) assert annotation == Dict[str, int] def test_create_transfer_model_type_annotation_mapping_type_nested() -> None: transfer_type = MappingType( field_definition=FieldDefinition.from_annotation(Dict[str, DataModel]), key_type=SimpleType(field_definition=FieldDefinition.from_annotation(str), nested_field_info=None), value_type=SimpleType( field_definition=FieldDefinition.from_annotation(DataModel), nested_field_info=NestedFieldInfo(model=TransferModel, field_definitions=()), ), has_nested=True, ) annotation = _create_transfer_model_type_annotation(transfer_type=transfer_type) assert annotation == Dict[str, TransferModel] def test_create_transfer_model_type_annotation_tuple_type_not_nested() -> None: transfer_type = TupleType( field_definition=FieldDefinition.from_annotation(Tuple[str, int]), inner_types=( SimpleType(field_definition=FieldDefinition.from_annotation(str), nested_field_info=None), SimpleType(field_definition=FieldDefinition.from_annotation(int), nested_field_info=None), ), has_nested=False, ) annotation = _create_transfer_model_type_annotation(transfer_type=transfer_type) assert annotation == Tuple[str, int] def test_create_transfer_model_type_annotation_tuple_type_nested() -> None: transfer_type = TupleType( field_definition=FieldDefinition.from_annotation(Tuple[str, DataModel]), inner_types=( SimpleType(field_definition=FieldDefinition.from_annotation(str), nested_field_info=None), SimpleType( field_definition=FieldDefinition.from_annotation(DataModel), nested_field_info=NestedFieldInfo(model=TransferModel, field_definitions=()), ), ), has_nested=True, ) annotation = _create_transfer_model_type_annotation(transfer_type=transfer_type) assert annotation == Tuple[str, TransferModel] def test_create_transfer_model_type_annotation_unexpected_transfer_type() -> None: transfer_type = CompositeType(field_definition=FieldDefinition.from_annotation(Union[str, int]), has_nested=False) with pytest.raises(RuntimeError): _create_transfer_model_type_annotation(transfer_type=transfer_type) def test_should_mark_private_underscore_fields_private_true() -> None: assert ( _should_mark_private( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg(annotation=int, name="a", default=1), model_name="A", default_factory=None, dto_field=DTOField(), ), True, ) is False ) assert ( _should_mark_private( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg(annotation=int, name="_a", default=1), model_name="A", default_factory=None, dto_field=DTOField(), ), True, ) is True ) assert ( _should_mark_private( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg(annotation=int, name="_a", default=1), model_name="A", default_factory=None, dto_field=DTOField(mark="read-only"), ), True, ) is False ) def test_should_mark_private_underscore_fields_private_false() -> None: assert ( _should_mark_private( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg(annotation=int, name="a", default=1), model_name="A", default_factory=None, dto_field=DTOField(), ), False, ) is False ) assert ( _should_mark_private( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg(annotation=int, name="_a", default=1), model_name="A", default_factory=None, dto_field=DTOField(), ), False, ) is False ) assert ( _should_mark_private( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg(annotation=int, name="_a", default=1), model_name="A", default_factory=None, dto_field=DTOField(mark="read-only"), ), False, ) is False ) litestar-2.16.0/tests/unit/test_dto/test_factory/test_base_dto.py000066400000000000000000000142161500564371300253040ustar00rootroot00000000000000# ruff: noqa: UP006 from __future__ import annotations import dataclasses from dataclasses import dataclass from typing import TYPE_CHECKING, Generic, Tuple, TypeVar, Union import pytest from typing_extensions import Annotated from litestar import Request from litestar.dto import DataclassDTO, DTOConfig from litestar.exceptions.dto_exceptions import InvalidAnnotationException from litestar.typing import FieldDefinition from . import Model if TYPE_CHECKING: from typing import Any from litestar.dto._backend import DTOBackend T = TypeVar("T", bound=Model) def get_backend(dto_type: type[DataclassDTO[Any]]) -> DTOBackend: value = next(iter(dto_type._dto_backends.values())) return value["data_backend"] # pyright: ignore def test_forward_referenced_type_argument_raises_exception() -> None: with pytest.raises(InvalidAnnotationException): DataclassDTO["Model"] def test_union_type_argument_raises_exception() -> None: class ModelB(Model): ... with pytest.raises(InvalidAnnotationException): DataclassDTO[Union[Model, ModelB]] def test_type_narrowing_with_scalar_type_arg() -> None: dto = DataclassDTO[Model] assert dto.config == DTOConfig() assert dto.model_type is Model # type: ignore[misc] def test_type_narrowing_with_annotated_scalar_type_arg() -> None: config = DTOConfig() dto = DataclassDTO[Annotated[Model, config]] assert dto.config is config assert dto.model_type is Model # type: ignore[misc] def test_type_narrowing_with_only_type_var() -> None: t = TypeVar("t", bound=Model) generic_dto = DataclassDTO[t] # pyright: ignore assert generic_dto is DataclassDTO def test_type_narrowing_with_annotated_type_var() -> None: config = DTOConfig() t = TypeVar("t", bound=Model) generic_dto = DataclassDTO[Annotated[t, config]] # pyright: ignore assert generic_dto is not DataclassDTO assert issubclass(generic_dto, DataclassDTO) assert generic_dto.config is config assert not hasattr(generic_dto, "model_type") def test_extra_annotated_metadata_ignored() -> None: config = DTOConfig() dto = DataclassDTO[Annotated[Model, config, "a"]] assert dto.config is config def test_overwrite_config() -> None: first = DTOConfig(exclude={"a"}) generic_dto = DataclassDTO[Annotated[T, first]] # pyright: ignore second = DTOConfig(exclude={"b"}) dto = generic_dto[Annotated[Model, second]] # pyright: ignore assert dto.config is second def test_existing_config_not_overwritten() -> None: assert getattr(DataclassDTO, "_config", None) is None first = DTOConfig(exclude={"a"}) generic_dto = DataclassDTO[Annotated[T, first]] # pyright: ignore dto = generic_dto[Model] # pyright: ignore assert dto.config is first def test_config_assigned_via_subclassing() -> None: class CustomGenericDTO(DataclassDTO[T]): config = DTOConfig(exclude={"a"}) concrete_dto = CustomGenericDTO[Model] assert concrete_dto.config.exclude == {"a"} async def test_from_bytes(asgi_connection: Request[Any, Any, Any]) -> None: dto_type = DataclassDTO[Model] dto_type.create_for_field_definition( FieldDefinition.from_kwarg(Model, name="data"), handler_id=asgi_connection.route_handler.handler_id ) assert dto_type(asgi_connection).decode_bytes(b'{"a":1,"b":"two"}') == Model(a=1, b="two") def test_config_field_rename(asgi_connection: Request[Any, Any, Any]) -> None: config = DTOConfig(rename_fields={"a": "z"}) DataclassDTO._dto_backends = {} dto_type = DataclassDTO[Annotated[Model, config]] dto_type.create_for_field_definition(FieldDefinition.from_kwarg(Model, name="data"), handler_id="handler_id") field_definitions = dto_type._dto_backends["handler_id"]["data_backend"].parsed_field_definitions # pyright: ignore assert field_definitions[0].serialization_name == "z" def test_type_narrowing_with_multiple_configs() -> None: config_1 = DTOConfig() config_2 = DTOConfig() dto = DataclassDTO[Annotated[Model, config_1, config_2]] assert dto.config is config_1 def test_raises_invalid_annotation_for_non_homogenous_collection_types() -> None: dto_type = DataclassDTO[Model] with pytest.raises(InvalidAnnotationException): dto_type.create_for_field_definition( handler_id="handler", field_definition=FieldDefinition.from_annotation(Tuple[Model, str]), ) def test_raises_invalid_annotation_for_mismatched_types() -> None: dto_type = DataclassDTO[Model] @dataclass class OtherModel: a: int with pytest.raises(InvalidAnnotationException): dto_type.create_for_field_definition( handler_id="handler", field_definition=FieldDefinition.from_annotation(OtherModel) ) def test_sub_types_supported() -> None: DataclassDTO._dto_backends = {} dto_type = DataclassDTO[Model] @dataclass class SubType(Model): c: int dto_type.create_for_field_definition( handler_id="handler_id", field_definition=FieldDefinition.from_kwarg(SubType, name="data") ) assert ( dto_type._dto_backends["handler_id"]["data_backend"].parsed_field_definitions[-1].name == "c" # pyright: ignore ) def test_get_config_for_model_type() -> None: base_config = DTOConfig(rename_strategy="camel") class CustomDTO(DataclassDTO[T], Generic[T]): @classmethod def get_config_for_model_type(cls, config: DTOConfig, model_type: type[Any]) -> DTOConfig: return dataclasses.replace(config, exclude={"foo"}) annotated_dto = CustomDTO[Model] annotated_dto_with_config = CustomDTO[Annotated[Model, base_config]] class SubclassDTO(CustomDTO[Model]): pass class SubclassDTOWithConfig(CustomDTO[Model]): config = base_config assert annotated_dto.config.exclude == {"foo"} assert SubclassDTO.config.exclude == {"foo"} # we expect existing configs to have been merged assert annotated_dto_with_config.config.exclude == {"foo"} assert annotated_dto_with_config.config.rename_strategy == "camel" assert SubclassDTOWithConfig.config.exclude == {"foo"} assert SubclassDTOWithConfig.config.rename_strategy == "camel" litestar-2.16.0/tests/unit/test_dto/test_factory/test_dataclass_dto.py000066400000000000000000000134511500564371300263310ustar00rootroot00000000000000from __future__ import annotations import itertools import sys from dataclasses import dataclass, field, replace from typing import ClassVar, List from unittest.mock import ANY import pytest from typing_extensions import Annotated from litestar.dto import DataclassDTO, DTOField from litestar.dto.data_structures import DTOFieldDefinition from litestar.typing import FieldDefinition @dataclass class Model: a: int b: str = field(default="b") c: List[int] = field(default_factory=list) # noqa: UP006 d: ClassVar[float] = 1.0 @property def computed(self) -> str: return "i am a property" @pytest.fixture(name="dto_type") def fx_dto_type() -> type[DataclassDTO[Model]]: return DataclassDTO[Model] @pytest.mark.skipif(sys.version_info >= (3, 9), reason="generic builtin collection") def test_dataclass_field_definitions_38(dto_type: type[DataclassDTO[Model]]) -> None: expected = [ replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( name="a", annotation=int, ), default_factory=None, model_name=Model.__name__, dto_field=DTOField(), ), metadata=ANY, type_wrappers=ANY, raw=ANY, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg(name="b", annotation=str, default="b"), default_factory=None, model_name=Model.__name__, dto_field=DTOField(), ), metadata=ANY, type_wrappers=ANY, raw=ANY, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( name="c", annotation=List[int], ), default_factory=list, model_name=Model.__name__, dto_field=DTOField(), ), metadata=ANY, type_wrappers=ANY, raw=ANY, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( name="computed", annotation=str, ), default_factory=None, model_name=Model.__name__, dto_field=DTOField(mark="read-only"), ), metadata=ANY, type_wrappers=ANY, raw=ANY, ), ] for field_def, exp in itertools.zip_longest(expected, dto_type.generate_field_definitions(Model), fillvalue=None): assert field_def == exp def test_dataclass_field_definitions(dto_type: type[DataclassDTO[Model]]) -> None: expected = [ replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( name="a", annotation=int, ), default_factory=None, model_name=Model.__name__, dto_field=DTOField(), ), metadata=ANY, type_wrappers=ANY, raw=ANY, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg(name="b", annotation=str, default="b"), default_factory=None, model_name=Model.__name__, dto_field=DTOField(), ), metadata=ANY, type_wrappers=ANY, raw=ANY, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( name="c", annotation=List[int], ), default_factory=list, model_name=Model.__name__, dto_field=DTOField(), ), metadata=ANY, type_wrappers=ANY, raw=ANY, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( name="computed", annotation=str, ), default_factory=None, model_name=Model.__name__, dto_field=DTOField(mark="read-only"), ), metadata=ANY, type_wrappers=ANY, raw=ANY, ), ] for field_def, exp in itertools.zip_longest(expected, dto_type.generate_field_definitions(Model), fillvalue=None): assert field_def == exp def test_dataclass_detect_nested(dto_type: type[DataclassDTO[Model]]) -> None: assert dto_type.detect_nested_field(FieldDefinition.from_annotation(Model)) is True assert dto_type.detect_nested_field(FieldDefinition.from_annotation(int)) is False ReadOnlyInt = Annotated[int, DTOField("read-only")] def test_dataclass_dto_annotated_dto_field() -> None: @dataclass class Model: a: Annotated[int, DTOField("read-only")] b: ReadOnlyInt dto_type = DataclassDTO[Model] fields = list(dto_type.generate_field_definitions(Model)) assert fields[0].dto_field == DTOField("read-only") assert fields[1].dto_field == DTOField("read-only") def test_property_underscore_exclude() -> None: @dataclass class Model: one: str @property def _computed(self) -> int: return 1 @property def __also_computed(self) -> int: return 1 dto_type = DataclassDTO[Model] fields = list(dto_type.generate_field_definitions(Model)) assert fields[0].name == "one" assert len(fields) == 1 litestar-2.16.0/tests/unit/test_dto/test_factory/test_field.py000066400000000000000000000006611500564371300246060ustar00rootroot00000000000000from __future__ import annotations import pytest from litestar.dto.field import DTO_FIELD_META_KEY, extract_dto_field from litestar.exceptions import ImproperlyConfiguredException from litestar.typing import FieldDefinition def test_extract_dto_field_unexpected_type() -> None: with pytest.raises(ImproperlyConfiguredException): extract_dto_field(FieldDefinition.from_annotation(int), {DTO_FIELD_META_KEY: object()}) litestar-2.16.0/tests/unit/test_dto/test_factory/test_integration.py000066400000000000000000001060671500564371300260550ustar00rootroot00000000000000# ruff: noqa: UP007, UP006 from __future__ import annotations from dataclasses import dataclass, field from types import ModuleType from typing import TYPE_CHECKING, Callable, Dict, Generic, List, Optional, Sequence, TypeVar, cast from unittest.mock import MagicMock from uuid import UUID import msgspec import pytest from msgspec import Struct from typing_extensions import Annotated from litestar import Controller, Response, get, patch, post from litestar.connection.request import Request from litestar.datastructures import UploadFile from litestar.dto import DataclassDTO, DTOConfig, DTOData, MsgspecDTO, dto_field from litestar.dto.types import RenameStrategy from litestar.enums import MediaType, RequestEncodingType from litestar.openapi.spec.response import OpenAPIResponse from litestar.openapi.spec.schema import Schema from litestar.pagination import ClassicPagination, CursorPagination, OffsetPagination from litestar.params import Body from litestar.serialization import encode_json from litestar.testing import create_test_client from tests.helpers import not_none if TYPE_CHECKING: from typing import Any from litestar import Litestar def test_url_encoded_form_data(use_experimental_dto_backend: bool) -> None: @dataclass() class User: name: str age: int read_only: str = field(default="read-only", metadata=dto_field("read-only")) class UserDTO(DataclassDTO[User]): config = DTOConfig(experimental_codegen_backend=use_experimental_dto_backend) @post(dto=UserDTO, signature_types=[User]) def handler(data: User = Body(media_type=RequestEncodingType.URL_ENCODED)) -> User: return data with create_test_client(route_handlers=[handler]) as client: response = client.post( "/", content=b"id=1&name=John&age=42&read_only=whoops", headers={"Content-Type": "application/x-www-form-urlencoded"}, ) assert response.json() == {"name": "John", "age": 42, "read_only": "read-only"} async def test_multipart_encoded_form_data(use_experimental_dto_backend: bool) -> None: default_file = UploadFile(content_type="text/plain", filename="forbidden", file_data=b"forbidden") @dataclass class Payload: file: UploadFile forbidden: UploadFile = field( default=default_file, metadata=dto_field("read-only"), ) class PayloadDTO(DataclassDTO[Payload]): config = DTOConfig(experimental_codegen_backend=use_experimental_dto_backend) @post(dto=PayloadDTO, return_dto=None, signature_types=[Payload], media_type=MediaType.TEXT) async def handler(data: Payload = Body(media_type=RequestEncodingType.MULTI_PART)) -> bytes: return await data.forbidden.read() with create_test_client(route_handlers=[handler]) as client: response = client.post( "/", files={"file": b"abc123", "forbidden": b"123abc"}, ) assert response.content == b"forbidden" await default_file.close() def test_renamed_field(use_experimental_dto_backend: bool) -> None: @dataclass class Foo: bar: str config = DTOConfig(rename_fields={"bar": "baz"}, experimental_codegen_backend=use_experimental_dto_backend) dto = DataclassDTO[Annotated[Foo, config]] @post(dto=dto, signature_types=[Foo]) def handler(data: Foo) -> Foo: assert data.bar == "hello" return data with create_test_client(route_handlers=[handler]) as client: response = client.post("/", json={"baz": "hello"}) assert response.json() == {"baz": "hello"} def test_renamed_field_nested(use_experimental_dto_backend: bool, create_module: Callable[[str], ModuleType]) -> None: # https://github.com/litestar-org/litestar/issues/2721 module = create_module( """ from dataclasses import dataclass from typing import List @dataclass class Bar: id: str @dataclass class Foo: id: str bar: Bar bars: List[Bar] """ ) Foo = module.Foo config = DTOConfig( rename_fields={"id": "foo_id", "bar.id": "bar_id", "bars.0.id": "bars_id"}, experimental_codegen_backend=use_experimental_dto_backend, ) dto = DataclassDTO[Annotated[Foo, config]] # type: ignore[valid-type] @post(dto=dto, signature_types=[Foo]) def handler(data: Foo) -> Foo: # type: ignore[valid-type] return data with create_test_client(route_handlers=[handler]) as client: response = client.post("/", json={"foo_id": "1", "bar": {"bar_id": "2"}, "bars": [{"bars_id": "3"}]}) assert response.json() == {"foo_id": "1", "bar": {"bar_id": "2"}, "bars": [{"bars_id": "3"}]} @dataclass class Spam: main_id: str = "spam-id" @dataclass class Fzop: bar: str = "hello" SPAM: str = "bye" spam_bar: str = "welcome" spam_model: Optional[Spam] = None @pytest.mark.parametrize( "rename_strategy, instance, tested_fields, data", [ ("upper", Fzop(bar="hi"), ["BAR"], {"BAR": "hi"}), ("lower", Fzop(SPAM="goodbye"), ["spam"], {"spam": "goodbye"}), (lambda x: x[::-1], Fzop(bar="h", SPAM="bye!"), ["rab", "MAPS"], {"rab": "h", "MAPS": "bye!"}), ("camel", Fzop(spam_bar="star"), ["spamBar"], {"spamBar": "star"}), ("pascal", Fzop(spam_bar="star"), ["SpamBar"], {"SpamBar": "star"}), ("camel", Fzop(spam_model=Spam()), ["spamModel"], {"spamModel": {"mainId": "spam-id"}}), ( "kebab", Fzop(spam_bar="star", spam_model=Spam()), ["spam-bar", "spam-model"], {"spam-bar": "star", "spam-model": {"main-id": "spam-id"}}, ), ], ) def test_fields_alias_generator( rename_strategy: RenameStrategy, instance: Fzop, tested_fields: list[str], data: dict[str, str], use_experimental_dto_backend: bool, ) -> None: DataclassDTO._dto_backends = {} config = DTOConfig(rename_strategy=rename_strategy, experimental_codegen_backend=use_experimental_dto_backend) dto = DataclassDTO[Annotated[Fzop, config]] @post(dto=dto) def handler(data: Fzop) -> Fzop: assert data.bar == instance.bar assert data.SPAM == instance.SPAM return data with create_test_client(route_handlers=[handler]) as client: response = client.post("/", json=data) for f in tested_fields: assert response.json()[f] == data[f] def test_dto_data_injection(use_experimental_dto_backend: bool) -> None: @dataclass class Foo: bar: str config = DTOConfig(experimental_codegen_backend=use_experimental_dto_backend) @post(dto=DataclassDTO[Annotated[Foo, config]], return_dto=None, signature_types=[Foo]) def handler(data: DTOData[Foo]) -> Foo: assert isinstance(data, DTOData) assert data.as_builtins() == {"bar": "hello"} assert isinstance(data.create_instance(), Foo) return data.create_instance() with create_test_client(route_handlers=[handler]) as client: response = client.post("/", json={"bar": "hello"}) assert response.json() == {"bar": "hello"} @dataclass class NestedFoo: bar: str baz: str @dataclass class NestingBar: foo: NestedFoo def test_dto_data_injection_with_nested_model(use_experimental_dto_backend: bool) -> None: @post( dto=DataclassDTO[ Annotated[ NestingBar, DTOConfig(exclude={"foo.baz"}, experimental_codegen_backend=use_experimental_dto_backend) ] ], return_dto=None, ) def handler(data: DTOData[NestingBar]) -> Dict[str, Any]: assert isinstance(data, DTOData) return cast("dict[str, Any]", data.as_builtins()) with create_test_client(route_handlers=[handler]) as client: resp = client.post("/", json={"foo": {"bar": "hello"}}) assert resp.status_code == 201 assert resp.json() == {"foo": {"bar": "hello"}} def test_dto_data_create_instance_nested_kwargs(use_experimental_dto_backend: bool) -> None: @post( dto=DataclassDTO[ Annotated[ NestingBar, DTOConfig(exclude={"foo.baz"}, experimental_codegen_backend=use_experimental_dto_backend) ] ], return_dto=None, ) def handler(data: DTOData[NestingBar]) -> NestingBar: assert isinstance(data, DTOData) result = data.create_instance(foo__baz="world") assert result.foo.baz == "world" return result with create_test_client(route_handlers=[handler]) as client: response = client.post("/", json={"foo": {"bar": "hello"}}) assert response.status_code == 201 assert response.json() == {"foo": {"bar": "hello", "baz": "world"}} @dataclass class User: name: str age: int read_only: str = field(default="read-only", metadata=dto_field("read-only")) def test_dto_data_with_url_encoded_form_data(use_experimental_dto_backend: bool) -> None: config = DTOConfig(experimental_codegen_backend=use_experimental_dto_backend) @post(dto=DataclassDTO[Annotated[User, config]]) def handler(data: DTOData[User] = Body(media_type=RequestEncodingType.URL_ENCODED)) -> User: return data.create_instance() with create_test_client(route_handlers=[handler]) as client: response = client.post( "/", content=b"id=1&name=John&age=42&read_only=whoops", headers={"Content-Type": "application/x-www-form-urlencoded"}, ) assert response.json() == {"name": "John", "age": 42, "read_only": "read-only"} RenamedBarT = TypeVar("RenamedBarT") @dataclass class GenericRenamedBar(Generic[RenamedBarT]): bar: str spam_bar: RenamedBarT foo_foo: str @dataclass class InnerBar: best_greeting: str @dataclass class RenamedBar(GenericRenamedBar[InnerBar]): pass def test_dto_data_create_instance_renamed_fields(use_experimental_dto_backend: bool) -> None: @post( dto=DataclassDTO[ Annotated[ RenamedBar, DTOConfig( exclude={"foo_foo"}, rename_strategy="camel", experimental_codegen_backend=use_experimental_dto_backend, ), ] ], return_dto=DataclassDTO[ Annotated[ RenamedBar, DTOConfig(rename_strategy="camel", experimental_codegen_backend=use_experimental_dto_backend), ] ], ) def handler(data: DTOData[RenamedBar]) -> RenamedBar: assert isinstance(data, DTOData) result = data.create_instance(foo_foo="world") assert result.foo_foo == "world" assert result.spam_bar.best_greeting == "hello world" return result with create_test_client(route_handlers=[handler]) as client: response = client.post("/", json={"bar": "hello", "spamBar": {"bestGreeting": "hello world"}}) assert response.status_code == 201 assert response.json() == {"bar": "hello", "fooFoo": "world", "spamBar": {"bestGreeting": "hello world"}} def test_dto_data_with_patch_request(use_experimental_dto_backend: bool) -> None: class PatchDTO(DataclassDTO[Annotated[User, DTOConfig(experimental_codegen_backend=use_experimental_dto_backend)]]): config = DTOConfig(partial=True) @patch(dto=PatchDTO, return_dto=None) def handler(data: DTOData[User]) -> User: return data.update_instance(User(name="John", age=42)) with create_test_client(route_handlers=[handler]) as client: response = client.patch("/", json={"age": 41, "read_only": "whoops"}) assert response.json() == {"name": "John", "age": 41, "read_only": "read-only"} @dataclass class UniqueModelName: id: int foo: str def test_dto_openapi_with_unique_handler_names(use_experimental_dto_backend: bool) -> None: @post( dto=DataclassDTO[ Annotated[ UniqueModelName, DTOConfig(exclude={"id"}, experimental_codegen_backend=use_experimental_dto_backend) ] ], return_dto=DataclassDTO[UniqueModelName], signature_namespace={"UniqueModelName": UniqueModelName}, ) def handler(data: UniqueModelName) -> UniqueModelName: return data with create_test_client(route_handlers=[handler]) as client: response = client.get("/schema/openapi.json") schemas = list(response.json()["components"]["schemas"].values()) assert len(schemas) == 2 assert schemas[0]["title"] == "HandlerUniqueModelNameRequestBody" assert schemas[1]["title"] == "HandlerUniqueModelNameResponseBody" @dataclass class SharedModelName: id: int foo: str def test_dto_openapi_without_unique_handler_names(use_experimental_dto_backend: bool) -> None: write_dto = DataclassDTO[ Annotated[SharedModelName, DTOConfig(exclude={"id"}, experimental_codegen_backend=use_experimental_dto_backend)] ] read_dto = DataclassDTO[SharedModelName] @post(dto=write_dto, return_dto=read_dto) def handler(data: SharedModelName) -> SharedModelName: return data class MyController(Controller): path = "/sub-path" @post(dto=write_dto, return_dto=read_dto) def handler(self, data: SharedModelName) -> SharedModelName: return data with create_test_client(route_handlers=[handler, MyController]) as client: response = client.get("/schema/openapi.json") schemas = list(response.json()["components"]["schemas"].values()) assert len(schemas) == 4 assert schemas[0]["title"] == "HandlerSharedModelNameRequestBody" assert schemas[1]["title"] == "HandlerSharedModelNameResponseBody" assert ( schemas[2]["title"] == "tests.unit.test_dto.test_factory.test_integration.test_dto_openapi_without_unique_handler_names..MyController.handlerSharedModelNameRequestBody" ) assert ( schemas[3]["title"] == "tests.unit.test_dto.test_factory.test_integration.test_dto_openapi_without_unique_handler_names..MyController.handlerSharedModelNameResponseBody" ) def test_url_encoded_form_data_patch_request(use_experimental_dto_backend: bool) -> None: @dataclass() class User: name: str age: int read_only: str = field(default="read-only", metadata=dto_field("read-only")) dto = DataclassDTO[ Annotated[User, DTOConfig(partial=True, experimental_codegen_backend=use_experimental_dto_backend)] ] @post(dto=dto, return_dto=None, signature_types=[User]) def handler(data: DTOData[User] = Body(media_type=RequestEncodingType.URL_ENCODED)) -> Dict[str, Any]: return data.as_builtins() # type:ignore[no-any-return] with create_test_client(route_handlers=[handler]) as client: response = client.post( "/", content=b"name=John&read_only=whoops", headers={"Content-Type": "application/x-www-form-urlencoded"}, ) assert response.json() == {"name": "John"} def test_dto_with_generic_sequence_annotations(use_experimental_dto_backend: bool) -> None: @dataclass class User: name: str age: int @post( dto=DataclassDTO[Annotated[User, DTOConfig(experimental_codegen_backend=use_experimental_dto_backend)]], signature_types=[User], ) def handler(data: Sequence[User]) -> Sequence[User]: return data with create_test_client(route_handlers=[handler]) as client: response = client.post("/", json=[{"name": "John", "age": 42}]) assert response.json() == [{"name": "John", "age": 42}] def test_dto_private_fields(use_experimental_dto_backend: bool) -> None: @dataclass class Foo: bar: str _baz: int mock = MagicMock() @post( dto=DataclassDTO[Annotated[Foo, DTOConfig(experimental_codegen_backend=use_experimental_dto_backend)]], signature_types=[Foo], ) def handler(data: DTOData[Foo]) -> Foo: mock.received_data = data.as_builtins() return data.create_instance(_baz=42) with create_test_client(route_handlers=[handler]) as client: response = client.post("/", json={"bar": "hello", "_baz": "world"}) assert response.status_code == 201 assert response.json() == {"bar": "hello"} assert mock.received_data == {"bar": "hello"} def test_dto_private_fields_disabled(use_experimental_dto_backend: bool) -> None: @dataclass class Foo: bar: str _baz: int @post( dto=DataclassDTO[ Annotated[ Foo, DTOConfig(underscore_fields_private=False, experimental_codegen_backend=use_experimental_dto_backend), ] ], signature_types=[Foo], ) def handler(data: Foo) -> Foo: return data with create_test_client(route_handlers=[handler]) as client: response = client.post("/", json={"bar": "hello", "_baz": 42}) assert response.status_code == 201 assert response.json() == {"bar": "hello", "_baz": 42} def test_dto_concrete_builtin_collection_types(use_experimental_dto_backend: bool) -> None: @dataclass class Foo: bar: dict baz: list @post( dto=DataclassDTO[ Annotated[ Foo, DTOConfig(underscore_fields_private=False, experimental_codegen_backend=use_experimental_dto_backend), ] ], signature_types=[Foo], ) def handler(data: Foo) -> Foo: return data with create_test_client(route_handlers=[handler]) as client: response = client.post("/", json={"bar": {"a": 1, "b": [1, 2, 3]}, "baz": [4, 5, 6]}) assert response.status_code == 201 assert response.json() == {"bar": {"a": 1, "b": [1, 2, 3]}, "baz": [4, 5, 6]} @dataclass class PaginatedUser: name: str age: int def test_dto_classic_pagination(use_experimental_dto_backend: bool) -> None: @get( dto=DataclassDTO[ Annotated[ PaginatedUser, DTOConfig(exclude={"age"}, experimental_codegen_backend=use_experimental_dto_backend) ] ] ) def handler() -> ClassicPagination[PaginatedUser]: return ClassicPagination( items=[PaginatedUser(name="John", age=42), PaginatedUser(name="Jane", age=43)], page_size=2, current_page=1, total_pages=20, ) with create_test_client(handler) as client: response = client.get("/") assert response.json() == { "items": [{"name": "John"}, {"name": "Jane"}], "page_size": 2, "current_page": 1, "total_pages": 20, } def test_dto_cursor_pagination(use_experimental_dto_backend: bool) -> None: uuid = UUID("00000000-0000-0000-0000-000000000000") @get( dto=DataclassDTO[ Annotated[ PaginatedUser, DTOConfig(exclude={"age"}, experimental_codegen_backend=use_experimental_dto_backend) ] ] ) def handler() -> CursorPagination[UUID, PaginatedUser]: return CursorPagination( items=[PaginatedUser(name="John", age=42), PaginatedUser(name="Jane", age=43)], results_per_page=2, cursor=uuid, ) with create_test_client(handler) as client: response = client.get("/") assert response.json() == { "items": [{"name": "John"}, {"name": "Jane"}], "results_per_page": 2, "cursor": "00000000-0000-0000-0000-000000000000", } def test_dto_offset_pagination(use_experimental_dto_backend: bool) -> None: @get( dto=DataclassDTO[ Annotated[ PaginatedUser, DTOConfig(exclude={"age"}, experimental_codegen_backend=use_experimental_dto_backend) ] ] ) def handler() -> OffsetPagination[PaginatedUser]: return OffsetPagination( items=[PaginatedUser(name="John", age=42), PaginatedUser(name="Jane", age=43)], limit=2, offset=0, total=20, ) with create_test_client(handler) as client: response = client.get("/") assert response.json() == { "items": [{"name": "John"}, {"name": "Jane"}], "limit": 2, "offset": 0, "total": 20, } T = TypeVar("T") V = TypeVar("V") K = TypeVar("K") @dataclass class Wrapped(Generic[T, V]): data: T other: V def test_dto_generic_dataclass_wrapped_list_response(use_experimental_dto_backend: bool) -> None: @get(dto=DataclassDTO[Annotated[PaginatedUser, DTOConfig(exclude={"age"})]]) def handler() -> Wrapped[List[PaginatedUser], int]: return Wrapped( data=[PaginatedUser(name="John", age=42), PaginatedUser(name="Jane", age=43)], other=2, ) with create_test_client(handler) as client: response = client.get("/") assert response.json() == {"data": [{"name": "John"}, {"name": "Jane"}], "other": 2} def test_dto_generic_dataclass_wrapped_scalar_response(use_experimental_dto_backend: bool) -> None: @get( dto=DataclassDTO[ Annotated[ PaginatedUser, DTOConfig(exclude={"age"}, experimental_codegen_backend=use_experimental_dto_backend) ] ] ) def handler() -> Wrapped[PaginatedUser, int]: return Wrapped( data=PaginatedUser(name="John", age=42), other=2, ) with create_test_client(handler) as client: response = client.get("/") assert response.json() == {"data": {"name": "John"}, "other": 2} @dataclass class WrappedWithDict(Generic[K, V, T]): data: T other: Dict[K, V] def test_dto_generic_dataclass_wrapped_scalar_response_with_additional_mapping_data( use_experimental_dto_backend: bool, ) -> None: @get( dto=DataclassDTO[ Annotated[ PaginatedUser, DTOConfig(exclude={"age"}, experimental_codegen_backend=use_experimental_dto_backend) ] ] ) def handler() -> WrappedWithDict[str, int, PaginatedUser]: return WrappedWithDict( data=PaginatedUser(name="John", age=42), other={"a": 1, "b": 2}, ) with create_test_client(handler) as client: response = client.get("/") assert response.json() == {"data": {"name": "John"}, "other": {"a": 1, "b": 2}} def test_dto_response_wrapped_scalar_return_type(use_experimental_dto_backend: bool) -> None: @get( dto=DataclassDTO[ Annotated[ PaginatedUser, DTOConfig(exclude={"age"}, experimental_codegen_backend=use_experimental_dto_backend) ] ] ) def handler() -> Response[PaginatedUser]: return Response(content=PaginatedUser(name="John", age=42)) with create_test_client(handler) as client: response = client.get("/") assert response.json() == {"name": "John"} def test_dto_response_wrapped_collection_return_type(use_experimental_dto_backend: bool) -> None: @get( dto=DataclassDTO[ Annotated[ PaginatedUser, DTOConfig(exclude={"age"}, experimental_codegen_backend=use_experimental_dto_backend) ] ] ) def handler() -> Response[List[PaginatedUser]]: return Response(content=[PaginatedUser(name="John", age=42), PaginatedUser(name="Jane", age=43)]) with create_test_client(handler) as client: response = client.get("/") assert response.json() == [{"name": "John"}, {"name": "Jane"}] def test_schema_required_fields_with_msgspec_dto(use_experimental_dto_backend: bool) -> None: class MsgspecUser(Struct): age: int name: str class UserDTO(MsgspecDTO[MsgspecUser]): config = DTOConfig(experimental_codegen_backend=use_experimental_dto_backend) @post(dto=UserDTO, return_dto=None, signature_types=[MsgspecUser]) def handler(data: MsgspecUser, request: Request) -> dict: schema = request.app.openapi_schema return schema.to_schema() with create_test_client(handler) as client: data = MsgspecUser(name="A", age=10) headers = {"Content-Type": "application/json; charset=utf-8"} received = client.post( "/", content=msgspec.json.encode(data), headers=headers, ) required = next(iter(received.json()["components"]["schemas"].values()))["required"] assert len(required) == 2 def test_schema_required_fields_with_dataclass_dto(use_experimental_dto_backend: bool) -> None: @dataclass class DataclassUser: age: int name: str class UserDTO(DataclassDTO[DataclassUser]): config = DTOConfig(experimental_codegen_backend=use_experimental_dto_backend) @post(dto=UserDTO, return_dto=None, signature_types=[DataclassUser]) def handler(data: DataclassUser, request: Request) -> dict: schema = request.app.openapi_schema return schema.to_schema() with create_test_client(handler) as client: data = DataclassUser(name="A", age=10) headers = {"Content-Type": "application/json; charset=utf-8"} received = client.post( "/", content=msgspec.json.encode(data), headers=headers, ) required = next(iter(received.json()["components"]["schemas"].values()))["required"] assert len(required) == 2 def test_schema_required_fields_with_msgspec_dto_and_default_fields(use_experimental_dto_backend: bool) -> None: class MsgspecUser(Struct): age: int name: str = "A" class UserDTO(MsgspecDTO[MsgspecUser]): config = DTOConfig(experimental_codegen_backend=use_experimental_dto_backend) @post(dto=UserDTO, return_dto=None, signature_types=[MsgspecUser]) def handler(data: MsgspecUser, request: Request) -> dict: schema = request.app.openapi_schema return schema.to_schema() with create_test_client(handler) as client: data = MsgspecUser(name="A", age=10) headers = {"Content-Type": "application/json; charset=utf-8"} received = client.post( "/", content=msgspec.json.encode(data), headers=headers, ) required = next(iter(received.json()["components"]["schemas"].values()))["required"] assert required == ["age"] X = TypeVar("X", bound=Struct) class ClassicNameStyle(Struct): first_name: str surname: str class BoundUser(Struct, Generic[X]): age: int data: X class Superuser(BoundUser[ClassicNameStyle]): pass def test_dto_with_msgspec_with_bound_generic_and_inherited_models(use_experimental_dto_backend: bool) -> None: @post(dto=MsgspecDTO[Annotated[Superuser, DTOConfig(experimental_codegen_backend=use_experimental_dto_backend)]]) def handler(data: Superuser) -> Superuser: return data with create_test_client(handler) as client: data = Superuser(data=ClassicNameStyle(first_name="A", surname="B"), age=10) received = client.post( "/", content=encode_json(data), headers={"Content-Type": "application/json; charset=utf-8"}, ) assert msgspec.json.decode(received.content, type=Superuser) == data def test_dto_returning_mapping(use_experimental_dto_backend: bool) -> None: @dataclass class Lexeme: id: int name: str class LexemeDTO(DataclassDTO[Lexeme]): config = DTOConfig(exclude={"id"}, experimental_codegen_backend=use_experimental_dto_backend) @get(return_dto=LexemeDTO, signature_types=[Lexeme]) async def get_definition() -> Dict[str, Lexeme]: return {"hello": Lexeme(id=1, name="hello"), "world": Lexeme(id=2, name="world")} with create_test_client(route_handlers=[get_definition]) as client: response = client.get("/") assert response.json() == {"hello": {"name": "hello"}, "world": {"name": "world"}} def test_data_dto_with_default() -> None: """A POST request without Body should inject the default value. https://github.com/litestar-org/litestar/issues/2902 """ @dataclass class Foo: foo: str @post(path="/", dto=DataclassDTO[Foo], signature_types=[Foo]) def test(data: Optional[Foo] = None) -> dict: return {"foo": data} with create_test_client([test]) as client: response = client.post("/") assert response.json() == {"foo": None} @pytest.mark.parametrize( "field_type, constraint_name, constraint_value, request_data", [ (int, "gt", 2, 2), (int, "ge", 2, 1), (int, "lt", 2, 2), (int, "le", 2, 3), (int, "multiple_of", 2, 3), (str, "min_length", 2, "1"), (str, "max_length", 1, "12"), (str, "pattern", r"\d", "a"), ], ) def test_msgspec_dto_copies_constraints( field_type: Any, constraint_name: str, constraint_value: Any, request_data: Any, use_experimental_dto_backend: bool ) -> None: # https://github.com/litestar-org/litestar/issues/3026 struct = msgspec.defstruct( "Foo", fields=[("bar", Annotated[field_type, msgspec.Meta(**{constraint_name: constraint_value})])], # type: ignore[list-item] ) @post( "/", dto=Annotated[MsgspecDTO[struct], DTOConfig(experimental_codegen_backend=use_experimental_dto_backend)], # type: ignore[arg-type, valid-type] signature_namespace={"struct": struct}, ) def handler(data: struct) -> None: # type: ignore[valid-type] pass with create_test_client([handler]) as client: assert client.post("/", json={"bar": request_data}).status_code == 400 def test_msgspec_dto_dont_copy_length_constraint_for_partial_dto() -> None: class Foo(msgspec.Struct): bar: Annotated[str, msgspec.Meta(min_length=2)] baz: Annotated[str, msgspec.Meta(max_length=2)] class FooDTO(MsgspecDTO[Foo]): config = DTOConfig(partial=True) @post("/", dto=FooDTO, signature_types={Foo}) def handler(data: Foo) -> None: pass with create_test_client([handler]) as client: assert client.post("/", json={"bar": "1", "baz": "123"}).status_code == 201 def test_openapi_schema_for_type_with_generic_pagination_type( create_module: Callable[[str], ModuleType], use_experimental_dto_backend: bool ) -> None: module = create_module( """ from dataclasses import dataclass from litestar import Litestar, get from litestar.dto import DataclassDTO from litestar.pagination import ClassicPagination @dataclass class Test: name: str age: int @get("/without-dto", sync_to_thread=False) def without_dto() -> ClassicPagination[Test]: return ClassicPagination( items=[Test("John", 25), Test("Jane", 30)], page_size=1, current_page=2, total_pages=2, ) @get("/with-dto", return_dto=DataclassDTO[Test], sync_to_thread=False) def with_dto() -> ClassicPagination[Test]: return ClassicPagination( items=[Test("John", 25), Test("Jane", 30)], page_size=1, current_page=2, total_pages=2, ) app = Litestar([without_dto, with_dto]) """ ) openapi = cast("Litestar", module.app).openapi_schema paths = not_none(openapi.paths) without_dto_response = not_none(not_none(paths["/without-dto"].get).responses)["200"] with_dto_response = not_none(not_none(paths["/with-dto"].get).responses)["200"] assert isinstance(without_dto_response, OpenAPIResponse) assert isinstance(with_dto_response, OpenAPIResponse) without_dto_schema = not_none(without_dto_response.content)["application/json"].schema with_dto_schema = not_none(with_dto_response.content)["application/json"].schema assert isinstance(without_dto_schema, Schema) assert isinstance(with_dto_schema, Schema) assert not_none(without_dto_schema.properties).keys() == not_none(with_dto_schema.properties).keys() def test_openapi_schema_for_type_with_custom_generic_type( create_module: Callable[[str], ModuleType], use_experimental_dto_backend: bool ) -> None: module = create_module( """ from dataclasses import dataclass from datetime import datetime from typing import Generic, List, TypeVar from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from litestar import Litestar, get from litestar.plugins.sqlalchemy import SQLAlchemyDTO from litestar.dto import DTOConfig T = TypeVar("T") @dataclass class WithCount(Generic[T]): count: int data: List[T] class Base(DeclarativeBase): ... class User(Base): __tablename__ = "user" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] password: Mapped[str] created_at: Mapped[datetime] class UserDTO(SQLAlchemyDTO[User]): config = DTOConfig(exclude={"password", "created_at"}) @get("/users", dto=UserDTO, sync_to_thread=False) def get_users() -> WithCount[User]: return WithCount( count=1, data=[User(id=1, name="Litestar User", password="xyz", created_at=datetime.now())] ) app = Litestar(route_handlers=[get_users]) """ ) openapi = cast("Litestar", module.app).openapi_schema schema = openapi.components.schemas["WithCount_litestar.dto._backend.GetUsersUserResponseBody_"] assert not_none(schema.properties).keys() == {"count", "data"} model_schema = openapi.components.schemas["GetUsersUserResponseBody"] assert not_none(model_schema.properties).keys() == {"id", "name"} def test_openapi_schema_for_dto_includes_body_examples(create_module: Callable[[str], ModuleType]) -> None: module = create_module( """ from dataclasses import dataclass from uuid import UUID from typing_extensions import Annotated from litestar import Litestar, post from litestar.dto import DataclassDTO from litestar.openapi.spec import Example from litestar.params import Body @dataclass class Item: id: UUID name: str body = Body( title="Create item", description="Create a new item.", examples=[ Example( summary="Post is Ok", value={ "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "Swatch", }, ) ], ) @post() async def create_item(data: Annotated[Item, body]) -> Item: return data @post("dto", dto=DataclassDTO[Item]) async def create_item_with_dto(data: Annotated[Item, body]) -> Item: return data app = Litestar(route_handlers=[create_item, create_item_with_dto]) """ ) openapi_schema = module.app.openapi_schema item_schema = openapi_schema.components.schemas["Item"] item_with_dto_schema = openapi_schema.components.schemas["CreateItemWithDtoItemRequestBody"] assert item_schema.examples == item_with_dto_schema.examples @pytest.mark.parametrize("forbid_unknown_fields, expected_status_code", [(False, 201), (True, 400)]) def test_forbid_unknown_fields( use_experimental_dto_backend: bool, forbid_unknown_fields: bool, expected_status_code: int ) -> None: @dataclass class Foo: bar: str config = DTOConfig( forbid_unknown_fields=forbid_unknown_fields, experimental_codegen_backend=use_experimental_dto_backend, ) dto = DataclassDTO[Annotated[Foo, config]] @post(dto=dto, signature_types=[Foo]) def handler(data: Foo) -> Foo: return data with create_test_client(route_handlers=[handler]) as client: response = client.post("/", json={"bar": "hello", "baz": "given"}) assert response.status_code == expected_status_code litestar-2.16.0/tests/unit/test_dto/test_factory/test_utils.py000066400000000000000000000030011500564371300246520ustar00rootroot00000000000000from typing import Generic, List, Optional, TypeVar from litestar.dto import DataclassDTO from litestar.typing import FieldDefinition from tests.models import DataclassPerson T = TypeVar("T") def test_resolve_model_type_optional() -> None: field_definition = FieldDefinition.from_annotation(Optional[int]) assert DataclassDTO[DataclassPerson].resolve_model_type(field_definition) == FieldDefinition.from_annotation(int) def test_resolve_generic_wrapper_type_no_origin() -> None: field_definition = FieldDefinition.from_annotation(int) assert DataclassDTO[DataclassPerson].resolve_generic_wrapper_type(field_definition) is None def test_resolve_generic_wrapper_type_origin_no_parameters() -> None: field_definition = FieldDefinition.from_annotation(List[int]) assert DataclassDTO[DataclassPerson].resolve_generic_wrapper_type(field_definition) is None def test_resolve_generic_wrapper_type_model_type_not_subtype_of_specialized_type() -> None: class Wrapper(Generic[T]): t: T field_definition = FieldDefinition.from_annotation(Wrapper[int]) assert DataclassDTO[DataclassPerson].resolve_generic_wrapper_type(field_definition) is None def test_resolve_generic_wrapper_type_type_var_not_attribute() -> None: class Wrapper(Generic[T]): def returns_t(self) -> T: # type:ignore[empty-body] ... field_definition = FieldDefinition.from_annotation(Wrapper[int]) assert DataclassDTO[DataclassPerson].resolve_generic_wrapper_type(field_definition) is None litestar-2.16.0/tests/unit/test_dto/test_integration.py000066400000000000000000000101211500564371300233300ustar00rootroot00000000000000from __future__ import annotations from typing import Dict from unittest.mock import MagicMock from litestar import Controller, Litestar, Router, post from litestar.dto import AbstractDTO, DTOConfig from litestar.dto._backend import DTOBackend from litestar.testing import create_test_client from . import Model def test_dto_defined_on_handler(ModelDataDTO: type[AbstractDTO]) -> None: @post(dto=ModelDataDTO, signature_types=[Model]) def handler(data: Model) -> Model: assert data == Model(a=1, b="2") return data with create_test_client(route_handlers=handler) as client: response = client.post("/", json={"what": "ever"}) assert response.status_code == 201 assert response.json() == {"a": 1, "b": "2"} def test_dto_defined_on_controller(ModelDataDTO: type[AbstractDTO]) -> None: class MyController(Controller): dto = ModelDataDTO @post() def handler(self, data: Model) -> Model: assert data == Model(a=1, b="2") return data with create_test_client(route_handlers=MyController) as client: response = client.post("/", json={"what": "ever"}) assert response.status_code == 201 assert response.json() == {"a": 1, "b": "2"} def test_dto_defined_on_router(ModelDataDTO: type[AbstractDTO]) -> None: @post() def handler(data: Model) -> Model: assert data == Model(a=1, b="2") return data router = Router(path="/", route_handlers=[handler], dto=ModelDataDTO) with create_test_client(route_handlers=router) as client: response = client.post("/", json={"what": "ever"}) assert response.status_code == 201 assert response.json() == {"a": 1, "b": "2"} def test_dto_defined_on_app(ModelDataDTO: type[AbstractDTO]) -> None: @post() def handler(data: Model) -> Model: assert data == Model(a=1, b="2") return data with create_test_client(route_handlers=handler, dto=ModelDataDTO) as client: response = client.post("/", json={"what": "ever"}) assert response.status_code == 201 assert response.json() == {"a": 1, "b": "2"} def test_set_dto_none_disables_inherited_dto(ModelDataDTO: type[AbstractDTO]) -> None: @post(dto=None, signature_namespace={"dict": Dict}) def handler(data: dict[str, str]) -> dict[str, str]: assert data == {"hello": "world"} return data mock_dto = MagicMock(spec=ModelDataDTO) with create_test_client(route_handlers=handler, dto=mock_dto) as client: # pyright:ignore response = client.post("/", json={"hello": "world"}) assert response.status_code == 201 assert response.json() == {"hello": "world"} mock_dto.assert_not_called() def test_dto_and_return_dto(ModelDataDTO: type[AbstractDTO], ModelReturnDTO: type[AbstractDTO]) -> None: @post() def handler(data: Model) -> Model: assert data == Model(a=1, b="2") return data with create_test_client(route_handlers=handler, dto=ModelDataDTO, return_dto=ModelReturnDTO) as client: response = client.post("/", json={"what": "ever"}) assert response.status_code == 201 assert response.json() == {"a": 1, "b": "2"} def test_enable_experimental_backend_override_in_dto_config(ModelDataDTO: type[AbstractDTO]) -> None: ModelDataDTO.config = DTOConfig(experimental_codegen_backend=False) @post(dto=ModelDataDTO, signature_types=[Model]) def handler(data: Model) -> Model: return data Litestar(route_handlers=[handler]) backend = handler.resolve_data_dto()._dto_backends[handler.handler_id]["data_backend"] # type: ignore[union-attr] assert isinstance(backend, DTOBackend) def test_use_codegen_backend_by_default(ModelDataDTO: type[AbstractDTO]) -> None: ModelDataDTO.config = DTOConfig() @post(dto=ModelDataDTO, signature_types=[Model]) def handler(data: Model) -> Model: return data Litestar(route_handlers=[handler]) backend = handler.resolve_data_dto()._dto_backends[handler.handler_id]["data_backend"] # type: ignore[union-attr] assert isinstance(backend, DTOBackend) litestar-2.16.0/tests/unit/test_dto/test_interface.py000066400000000000000000000007331500564371300227550ustar00rootroot00000000000000from __future__ import annotations from typing import Any from unittest.mock import MagicMock from litestar.dto import AbstractDTO from litestar.openapi.spec.schema import Schema from litestar.typing import FieldDefinition def test_dto_interface_create_openapi_schema_default_implementation(ModelDataDTO: type[AbstractDTO]) -> None: assert ( ModelDataDTO.create_openapi_schema(FieldDefinition.from_annotation(Any), MagicMock(), MagicMock()) == Schema() ) litestar-2.16.0/tests/unit/test_events.py000066400000000000000000000125111500564371300204710ustar00rootroot00000000000000from typing import Any from unittest.mock import MagicMock import pytest from pytest_lazy_fixtures import lf from litestar import Litestar, Request, get from litestar.events.emitter import SimpleEventEmitter from litestar.events.listener import EventListener, listener from litestar.exceptions import ImproperlyConfiguredException from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from litestar.types import AnyIOBackend @pytest.fixture() def mock() -> MagicMock: return MagicMock() @pytest.fixture() def sync_listener(mock: MagicMock) -> EventListener: @listener("test_event") def listener_fn(*args: Any, **kwargs: Any) -> None: mock(*args, **kwargs) return listener_fn @pytest.fixture() def async_listener(mock: MagicMock) -> EventListener: @listener("test_event") async def listener_fn(*args: Any, **kwargs: Any) -> None: mock(*args, **kwargs) return listener_fn @pytest.mark.parametrize("event_listener", [lf("sync_listener"), lf("async_listener")]) def test_event_listener(mock: MagicMock, event_listener: EventListener, anyio_backend: AnyIOBackend) -> None: @get("/") def route_handler(request: Request[Any, Any, Any]) -> None: request.app.emit("test_event", "positional", keyword="keyword-value") with create_test_client( route_handlers=[route_handler], listeners=[event_listener], backend=anyio_backend ) as client: response = client.get("/") assert response.status_code == HTTP_200_OK mock.assert_called_with("positional", keyword="keyword-value") async def test_shutdown_awaits_pending(async_listener: EventListener, mock: MagicMock) -> None: async with SimpleEventEmitter([async_listener]) as emitter: for _ in range(100): emitter.emit("test_event") assert mock.call_count == 100 def test_multiple_event_listeners( async_listener: EventListener, sync_listener: EventListener, mock: MagicMock, anyio_backend: AnyIOBackend ) -> None: @get("/") def route_handler(request: Request[Any, Any, Any]) -> None: request.app.emit("test_event") with create_test_client( route_handlers=[route_handler], listeners=[async_listener, sync_listener], backend=anyio_backend ) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert mock.call_count == 2 def test_multiple_event_ids(mock: MagicMock, anyio_backend: AnyIOBackend) -> None: @listener("test_event_1", "test_event_2") def event_handler() -> None: mock() @get("/{event_id:int}") def route_handler(request: Request[Any, Any, Any], event_id: int) -> None: request.app.emit(f"test_event_{event_id}") with create_test_client(route_handlers=[route_handler], listeners=[event_handler], backend=anyio_backend) as client: response_1 = client.get("/1") response_2 = client.get("/2") assert response_1.status_code == HTTP_200_OK assert response_2.status_code == HTTP_200_OK assert mock.call_count == 2 async def test_raises_when_decorator_called_without_callable() -> None: with pytest.raises(ImproperlyConfiguredException): listener("test_even")(True) # type: ignore[arg-type] async def test_raises_when_not_initialized() -> None: app = Litestar([]) with pytest.raises(RuntimeError): app.emit("x") async def test_raises_when_not_listener_are_registered_for_an_event_id(async_listener: EventListener) -> None: with create_test_client(route_handlers=[], listeners=[async_listener]) as client: with pytest.raises(ImproperlyConfiguredException): client.app.emit("x") async def test_event_listener_raises_exception( async_listener: EventListener, sync_listener: EventListener, mock: MagicMock ) -> None: """Test that an event listener that raises an exception does not prevent other listeners from being called. https://github.com/litestar-org/litestar/issues/2809 https://github.com/litestar-org/litestar/issues/4044 """ error_mock = MagicMock() @listener("async_error_event") async def async_raising_listener(*args: Any, **kwargs: Any) -> None: error_mock() raise ValueError("test") @listener("sync_error_event") def sync_raising_listener(*args: Any, **kwargs: Any) -> None: error_mock() raise ValueError("test") @get("/async-error") def route_handler_1(request: Request[Any, Any, Any]) -> None: request.app.emit("async_error_event") @get("/sync-error") def route_handler_2(request: Request[Any, Any, Any]) -> None: request.app.emit("sync_error_event") @get("/no-error") def route_handler_3(request: Request[Any, Any, Any]) -> None: request.app.emit("test_event") with create_test_client( route_handlers=[route_handler_1, route_handler_2, route_handler_3], listeners=[async_listener, sync_listener, async_raising_listener, sync_raising_listener], ) as client: first_response = client.get("/async-error") second_response = client.get("/sync-error") third_response = client.get("/no-error") assert first_response.status_code == HTTP_200_OK assert second_response.status_code == HTTP_200_OK assert third_response.status_code == HTTP_200_OK error_mock.assert_called() mock.assert_called() litestar-2.16.0/tests/unit/test_exceptions.py000066400000000000000000000202111500564371300213420ustar00rootroot00000000000000from typing import Type import pytest from hypothesis import given from hypothesis import strategies as st from starlette.exceptions import HTTPException as StarletteHTTPException from litestar import get from litestar.enums import MediaType from litestar.exceptions import ( HTTPException, ImproperlyConfiguredException, LitestarException, MissingDependencyException, ValidationException, ) from litestar.exceptions.responses import create_exception_response from litestar.status_codes import HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR from litestar.testing import RequestFactory, create_test_client class CustomLitestarException(LitestarException): detail = "Custom Exception" class CustomHTTPException(HTTPException): detail = "Custom HTTP Exception" @given(detail=st.text()) def test_litestar_exception_detail(detail: str) -> None: for result in LitestarException(detail=detail), LitestarException(detail): assert result.detail == detail @given(detail=st.text()) def test_custom_litestar_exception_detail(detail: str) -> None: for result in CustomLitestarException(detail=detail), CustomLitestarException(detail): assert result.detail == (detail or "Custom Exception") @given(detail=st.text()) @pytest.mark.parametrize("ex_type", [LitestarException, CustomLitestarException]) def test_litestar_exception_repr(ex_type: Type[LitestarException], detail: str) -> None: for result in ex_type(detail), ex_type(detail=detail): if result.detail: assert repr(result) == f"{result.__class__.__name__} - {result.detail}" else: assert repr(result) == result.__class__.__name__ @given(detail=st.text()) @pytest.mark.parametrize("ex_type", [LitestarException, CustomLitestarException]) def test_litestar_exception_str(ex_type: Type[LitestarException], detail: str) -> None: for result in ex_type(detail), ex_type(detail=detail): assert str(result) == result.detail.strip() result = ex_type(200, detail=detail) assert str(result) == f"200 {detail}".strip() @given(detail=st.text()) def test_http_exception_detail(detail: str) -> None: for result in HTTPException(detail=detail), HTTPException(detail): assert result.detail == (detail or "Internal Server Error") @given(detail=st.text()) def test_custom_http_exception_detail(detail: str) -> None: for result in CustomHTTPException(detail=detail), CustomHTTPException(detail): assert result.detail == (detail or "Custom HTTP Exception") @given(status_code=st.integers(min_value=400, max_value=404), detail=st.text()) @pytest.mark.parametrize("ex_type", [HTTPException, CustomHTTPException]) def test_http_exception(ex_type: Type[HTTPException], status_code: int, detail: str) -> None: assert ex_type().status_code == HTTP_500_INTERNAL_SERVER_ERROR for result in ex_type(detail, status_code=status_code), ex_type(detail=detail, status_code=status_code): assert isinstance(result, LitestarException) assert repr(result) == f"{result.status_code} - {result.__class__.__name__} - {result.detail}" assert str(result) == f"{result.status_code}: {result.detail}".strip() @given(detail=st.text()) def test_improperly_configured_exception(detail: str) -> None: result = ImproperlyConfiguredException(detail=detail) assert repr(result) == f"{HTTP_500_INTERNAL_SERVER_ERROR} - {result.__class__.__name__} - {result.detail}" assert isinstance(result, HTTPException) assert isinstance(result, ValueError) def test_validation_exception() -> None: result = ValidationException() assert repr(result) == f"{HTTP_400_BAD_REQUEST} - {result.__class__.__name__} - {result.detail}" assert isinstance(result, HTTPException) assert isinstance(result, ValueError) @pytest.mark.parametrize("media_type", [MediaType.JSON, MediaType.TEXT]) def test_create_exception_response_utility_litestar_http_exception(media_type: MediaType) -> None: exc = HTTPException(detail="litestar http exception", status_code=HTTP_400_BAD_REQUEST, extra=["any"]) request = RequestFactory(handler_kwargs={"media_type": media_type}).get() response = create_exception_response(request=request, exc=exc) assert response.status_code == HTTP_400_BAD_REQUEST assert response.media_type == media_type if media_type == MediaType.JSON: assert response.content == {"status_code": 400, "detail": "litestar http exception", "extra": ["any"]} else: assert response.content == b'{"status_code":400,"detail":"litestar http exception","extra":["any"]}' @pytest.mark.parametrize("media_type", [MediaType.JSON, MediaType.TEXT]) def test_create_exception_response_utility_starlette_http_exception(media_type: MediaType) -> None: @get("/", media_type=media_type) def handler() -> str: raise StarletteHTTPException(status_code=400) with create_test_client(handler) as client: response = client.get("/", headers={"Accept": media_type}) assert response.json() == {"status_code": 400, "detail": "Bad Request"} @pytest.mark.parametrize("media_type", [MediaType.JSON, MediaType.TEXT]) def test_create_exception_response_utility_non_http_exception(media_type: MediaType) -> None: exc = RuntimeError("yikes") request = RequestFactory(handler_kwargs={"media_type": media_type}).get() response = create_exception_response(request=request, exc=exc) assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert response.media_type == media_type if media_type == MediaType.JSON: assert response.content == {"status_code": 500, "detail": "Internal Server Error"} else: assert response.content == b'{"status_code":500,"detail":"Internal Server Error"}' def test_missing_dependency_exception() -> None: exc = MissingDependencyException("some_package") expected = ( "Package 'some_package' is not installed but required. You can install it by running 'pip install " "litestar[some_package]' to install litestar with the required extra or 'pip install some_package' to install " "the package separately" ) assert str(exc) == expected def test_missing_dependency_exception_differing_package_name() -> None: exc = MissingDependencyException("some_package", "install_via_this", "other-extra") expected = ( "Package 'some_package' is not installed but required. You can install it by running 'pip install " "litestar[other-extra]' to install litestar with the required extra or 'pip install install_via_this' to " "install the package separately" ) assert str(exc) == expected @pytest.mark.parametrize("media_type", (MediaType.HTML, MediaType.JSON, MediaType.TEXT)) def test_default_exception_handling_of_internal_server_errors(media_type: MediaType) -> None: @get("/") def handler() -> None: raise ValueError("internal problem") with create_test_client(handler) as client: response = client.get("/", headers={"Accept": media_type}) assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR if media_type == MediaType.HTML: assert response.text.startswith("") elif media_type == MediaType.JSON: assert response.json().get("details").startswith("Traceback (most recent call last") else: assert response.text.startswith("Traceback (most recent call last") def test_non_litestar_exception_with_status_code_is_500() -> None: # https://github.com/litestar-org/litestar/issues/3082 class MyException(Exception): status_code: int = 400 @get("/") def handler() -> None: raise MyException("hello") with create_test_client([handler]) as client: assert client.get("/").status_code == 500 def test_non_litestar_exception_with_detail_is_not_included() -> None: # https://github.com/litestar-org/litestar/issues/3082 class MyException(Exception): status_code: int = 400 detail: str = "hello" @get("/") def handler() -> None: raise MyException() with create_test_client([handler], debug=False) as client: assert client.get("/", headers={"Accept": MediaType.JSON}).json().get("detail") == "Internal Server Error" litestar-2.16.0/tests/unit/test_file_system.py000066400000000000000000000070421500564371300215130ustar00rootroot00000000000000import sys from pathlib import Path from stat import S_IRWXO from typing import TYPE_CHECKING import pytest from fsspec.implementations.local import LocalFileSystem from litestar.exceptions import InternalServerException, NotAuthorizedException from litestar.file_system import BaseLocalFileSystem, FileSystemAdapter if TYPE_CHECKING: from litestar.types import FileSystemProtocol @pytest.mark.parametrize("file_system", (BaseLocalFileSystem(), LocalFileSystem())) async def test_file_adapter_open(tmpdir: Path, file_system: "FileSystemProtocol") -> None: file = Path(tmpdir / "test.txt") file.write_bytes(b"test") adapter = FileSystemAdapter(file_system=file_system) async with await adapter.open(file=file) as opened_file: assert await opened_file.read() == b"test" @pytest.mark.parametrize("file_system", (BaseLocalFileSystem(), LocalFileSystem())) @pytest.mark.xfail(sys.platform == "win32", reason="permissions equivalent missing on windows") async def test_file_adapter_open_handles_permission_exception(tmpdir: Path, file_system: "FileSystemProtocol") -> None: file = Path(tmpdir / "test.txt") file.write_bytes(b"test") owner_permissions = file.stat().st_mode file.chmod(S_IRWXO) adapter = FileSystemAdapter(file_system=file_system) with pytest.raises(NotAuthorizedException): async with await adapter.open(file=file): pass Path(tmpdir).chmod(owner_permissions) @pytest.mark.parametrize("file_system", (BaseLocalFileSystem(), LocalFileSystem())) async def test_file_adapter_open_handles_file_not_found_exception(file_system: "FileSystemProtocol") -> None: adapter = FileSystemAdapter(file_system=file_system) with pytest.raises(InternalServerException): async with await adapter.open(file="non_existing_file.txt"): pass @pytest.mark.parametrize("file_system", (BaseLocalFileSystem(), LocalFileSystem())) @pytest.mark.xfail(sys.platform == "win32", reason="Suspected fsspec issue", strict=False) async def test_file_adapter_info(tmpdir: Path, file_system: "FileSystemProtocol") -> None: file = Path(tmpdir / "test.txt") file.write_bytes(b"test") adapter = FileSystemAdapter(file_system=file_system) result = file.stat() assert await adapter.info(file) == { "gid": result.st_gid, "ino": result.st_ino, "islink": False, "mode": result.st_mode, "mtime": result.st_mtime, "name": str(file), "nlink": 1, "created": result.st_ctime, "size": result.st_size, "type": "file", "uid": result.st_uid, } @pytest.mark.parametrize("file_system", (BaseLocalFileSystem(), LocalFileSystem())) async def test_file_adapter_info_handles_file_not_found_exception(file_system: "FileSystemProtocol") -> None: adapter = FileSystemAdapter(file_system=file_system) with pytest.raises(FileNotFoundError): await adapter.info(path="non_existing_file.txt") @pytest.mark.parametrize("file_system", (BaseLocalFileSystem(), LocalFileSystem())) @pytest.mark.xfail(sys.platform == "win32", reason="permissions equivalent missing on windows") async def test_file_adapter_info_handles_permission_exception(tmpdir: Path, file_system: "FileSystemProtocol") -> None: file = Path(tmpdir / "test.txt") file.write_bytes(b"test") owner_permissions = file.stat().st_mode Path(tmpdir).chmod(S_IRWXO) adapter = FileSystemAdapter(file_system=file_system) with pytest.raises(NotAuthorizedException): await adapter.info(path=file) Path(tmpdir).chmod(owner_permissions) litestar-2.16.0/tests/unit/test_guards.py000066400000000000000000000105461500564371300204600ustar00rootroot00000000000000from typing import TYPE_CHECKING import pytest from litestar import Litestar, Router, asgi, get, websocket from litestar.connection import WebSocket from litestar.exceptions import PermissionDeniedException, WebSocketDisconnect from litestar.response.base import ASGIResponse from litestar.status_codes import HTTP_200_OK, HTTP_403_FORBIDDEN from litestar.testing import create_test_client from litestar.types import Receive, Scope, Send if TYPE_CHECKING: from litestar.connection import ASGIConnection from litestar.handlers.base import BaseRouteHandler async def local_guard(_: "ASGIConnection", route_handler: "BaseRouteHandler") -> None: if not route_handler.opt or not route_handler.opt.get("allow_all"): raise PermissionDeniedException("local") def router_guard(connection: "ASGIConnection", _: "BaseRouteHandler") -> None: if not connection.headers.get("Authorization-Router"): raise PermissionDeniedException("router") def app_guard(connection: "ASGIConnection", _: "BaseRouteHandler") -> None: if not connection.headers.get("Authorization"): raise PermissionDeniedException("app") def test_guards_with_http_handler() -> None: @get(path="/secret", guards=[local_guard]) def my_http_route_handler() -> None: ... with create_test_client(guards=[app_guard], route_handlers=[my_http_route_handler]) as client: response = client.get("/secret") assert response.status_code == HTTP_403_FORBIDDEN assert response.json().get("detail") == "app" response = client.get("/secret", headers={"Authorization": "yes"}) assert response.status_code == HTTP_403_FORBIDDEN assert response.json().get("detail") == "local" client.app.asgi_router.root_route_map_node.children["/secret"].asgi_handlers["GET"][1].opt["allow_all"] = True response = client.get("/secret", headers={"Authorization": "yes"}) assert response.status_code == HTTP_200_OK def test_guards_with_asgi_handler() -> None: @asgi(path="/secret", guards=[local_guard]) async def my_asgi_handler(scope: Scope, receive: Receive, send: Send) -> None: response = ASGIResponse(body=b'{"hello": "world"}') await response(scope=scope, receive=receive, send=send) with create_test_client(guards=[app_guard], route_handlers=[my_asgi_handler]) as client: response = client.get("/secret") assert response.status_code == HTTP_403_FORBIDDEN assert response.json().get("detail") == "app" response = client.get("/secret", headers={"Authorization": "yes"}) assert response.status_code == HTTP_403_FORBIDDEN assert response.json().get("detail") == "local" client.app.asgi_router.root_route_map_node.children["/secret"].asgi_handlers["asgi"][1].opt["allow_all"] = True response = client.get("/secret", headers={"Authorization": "yes"}) assert response.status_code == HTTP_200_OK def test_guards_with_websocket_handler() -> None: @websocket(path="/", guards=[local_guard]) async def my_websocket_route_handler(socket: WebSocket) -> None: await socket.accept() data = await socket.receive_json() assert data await socket.send_json({"data": "123"}) await socket.close() client = create_test_client(route_handlers=my_websocket_route_handler) with pytest.raises(WebSocketDisconnect), client.websocket_connect("/") as ws: ws.send_json({"data": "123"}) client.app.asgi_router.root_route_map_node.children["/"].asgi_handlers["websocket"][1].opt["allow_all"] = True with client.websocket_connect("/") as ws: ws.send_json({"data": "123"}) def test_guards_layering_for_same_route_handler() -> None: @get(path="/http", guards=[local_guard]) def http_route_handler() -> None: ... router = Router(path="/router", route_handlers=[http_route_handler], guards=[router_guard]) app = Litestar(route_handlers=[http_route_handler, router], guards=[app_guard]) assert ( len( app.asgi_router.root_route_map_node.children["/http"] .asgi_handlers["GET"][1] # type: ignore[arg-type] ._resolved_guards ) == 2 ) assert ( len( app.asgi_router.root_route_map_node.children["/router/http"] .asgi_handlers["GET"][1] # type: ignore[arg-type] ._resolved_guards ) == 3 ) litestar-2.16.0/tests/unit/test_handlers/000077500000000000000000000000001500564371300204135ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_handlers/__init__.py000066400000000000000000000000001500564371300225120ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_handlers/test_asgi_handlers/000077500000000000000000000000001500564371300242555ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_handlers/test_asgi_handlers/__init__.py000066400000000000000000000000001500564371300263540ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_handlers/test_asgi_handlers/test_handle_asgi.py000066400000000000000000000071221500564371300301260ustar00rootroot00000000000000from unittest.mock import MagicMock import pytest from litestar import Controller, Litestar, MediaType, asgi from litestar.enums import ScopeType from litestar.exceptions import LitestarWarning from litestar.response.base import ASGIResponse from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from litestar.types import ASGIApp, Receive, Scope, Send def test_handle_asgi() -> None: @asgi(path="/") async def root_asgi_handler(scope: Scope, receive: Receive, send: Send) -> None: assert scope["type"] == ScopeType.HTTP assert scope["method"] == "GET" response = ASGIResponse(body=b"Hello World", media_type=MediaType.TEXT) await response(scope, receive, send) class MyController(Controller): path = "/asgi" @asgi() async def root_asgi_handler(self, scope: Scope, receive: Receive, send: Send) -> None: assert scope["type"] == ScopeType.HTTP assert scope["method"] == "GET" response = ASGIResponse(body=b"Hello World", media_type=MediaType.TEXT) await response(scope, receive, send) with create_test_client([root_asgi_handler, MyController]) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "Hello World" response = client.get("/asgi") assert response.status_code == HTTP_200_OK assert response.text == "Hello World" def test_asgi_signature_namespace() -> None: class MyController(Controller): path = "/asgi" signature_namespace = {"b": Receive} @asgi(signature_namespace={"c": Send}) async def root_asgi_handler( self, scope: "a", # type:ignore[name-defined] # noqa: F821 receive: "b", # type:ignore[name-defined] # noqa: F821 send: "c", # type:ignore[name-defined] # noqa: F821 ) -> None: await ASGIResponse(body=scope["path"].encode(), media_type=MediaType.TEXT)(scope, receive, send) with create_test_client([MyController], signature_namespace={"a": Scope}) as client: response = client.get("/asgi") assert response.status_code == HTTP_200_OK assert response.text == "/asgi" def test_copy_scope_not_set_warns_on_modification() -> None: @asgi(is_mount=True) async def handler(scope: "Scope", receive: "Receive", send: "Send") -> None: scope["foo"] = "" # type: ignore[typeddict-unknown-key] await ASGIResponse()(scope, receive, send) with create_test_client([handler]) as client: with pytest.warns(LitestarWarning, match="modified 'scope' with 'copy_scope' set to 'None'"): response = client.get("/") assert response.status_code == HTTP_200_OK @pytest.mark.parametrize("copy_scope, expected_value", [(True, None), (False, "foo")]) def test_copy_scope(copy_scope: bool, expected_value: "str | None") -> None: mock = MagicMock() def middleware_factory(app: Litestar) -> ASGIApp: async def middleware(scope: "Scope", receive: "Receive", send: "Send") -> None: await app(scope, receive, send) mock(scope.get("foo")) return middleware @asgi(is_mount=True, copy_scope=copy_scope) async def handler(scope: "Scope", receive: "Receive", send: "Send") -> None: scope["foo"] = "foo" # type: ignore[typeddict-unknown-key] await ASGIResponse()(scope, receive, send) with create_test_client([handler], middleware=[middleware_factory]) as client: client.get("/") mock.assert_called_once_with(expected_value) test_handle_asgi_with_future_annotations.py000066400000000000000000000025761500564371300351210ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_handlers/test_asgi_handlersfrom __future__ import annotations from litestar import Controller, MediaType, asgi from litestar.enums import ScopeType from litestar.response.base import ASGIResponse from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from litestar.types import Receive, Scope, Send def test_handle_asgi() -> None: @asgi(path="/") async def root_asgi_handler(scope: Scope, receive: Receive, send: Send) -> None: assert scope["type"] == ScopeType.HTTP assert scope["method"] == "GET" response = ASGIResponse(body=b"Hello World", media_type=MediaType.TEXT) await response(scope, receive, send) class MyController(Controller): path = "/asgi" @asgi() async def root_asgi_handler(self, scope: Scope, receive: Receive, send: Send) -> None: assert scope["type"] == ScopeType.HTTP assert scope["method"] == "GET" response = ASGIResponse(body=b"Hello World", media_type=MediaType.TEXT) await response(scope, receive, send) with create_test_client([root_asgi_handler, MyController]) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "Hello World" response = client.get("/asgi") assert response.status_code == HTTP_200_OK assert response.text == "Hello World" litestar-2.16.0/tests/unit/test_handlers/test_asgi_handlers/test_validations.py000066400000000000000000000031411500564371300302020ustar00rootroot00000000000000from typing import TYPE_CHECKING import pytest from litestar import Litestar, asgi from litestar.exceptions import ImproperlyConfiguredException from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.types import Receive, Scope, Send def test_asgi_handler_validation() -> None: async def fn_without_scope_arg(receive: "Receive", send: "Send") -> None: pass with pytest.raises(ImproperlyConfiguredException): asgi(path="/")(fn_without_scope_arg).on_registration(Litestar()) async def fn_without_receive_arg(scope: "Scope", send: "Send") -> None: pass with pytest.raises(ImproperlyConfiguredException): asgi(path="/")(fn_without_receive_arg).on_registration(Litestar()) async def fn_without_send_arg(scope: "Scope", receive: "Receive") -> None: pass with pytest.raises(ImproperlyConfiguredException): asgi(path="/")(fn_without_send_arg).on_registration(Litestar()) async def fn_with_return_annotation(scope: "Scope", receive: "Receive", send: "Send") -> dict: return {} with pytest.raises(ImproperlyConfiguredException): asgi(path="/")(fn_with_return_annotation).on_registration(Litestar()) asgi_handler_with_no_fn = asgi(path="/") with pytest.raises(ImproperlyConfiguredException): create_test_client(route_handlers=asgi_handler_with_no_fn) def sync_fn(scope: "Scope", receive: "Receive", send: "Send") -> None: return None with pytest.raises(ImproperlyConfiguredException): asgi(path="/")(sync_fn).on_registration(Litestar()) # type: ignore[arg-type] litestar-2.16.0/tests/unit/test_handlers/test_base_handlers/000077500000000000000000000000001500564371300242445ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_handlers/test_base_handlers/__init__.py000066400000000000000000000000001500564371300263430ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_handlers/test_base_handlers/test_opt.py000066400000000000000000000067301500564371300264650ustar00rootroot00000000000000from typing import TYPE_CHECKING, Any, Callable, Dict import pytest from litestar import ( Controller, Litestar, Router, asgi, delete, get, patch, post, put, websocket, ) if TYPE_CHECKING: from litestar import WebSocket from litestar.types import Receive, RouteHandlerType, Scope, Send def regular_handler() -> None: ... async def asgi_handler(scope: "Scope", receive: "Receive", send: "Send") -> None: ... async def socket_handler(socket: "WebSocket") -> None: ... @pytest.mark.parametrize( "decorator, handler", [ (get, regular_handler), (post, regular_handler), (delete, regular_handler), (put, regular_handler), (patch, regular_handler), (asgi, asgi_handler), (websocket, socket_handler), ], ) def test_opt_settings(decorator: "RouteHandlerType", handler: Callable) -> None: base_opt = {"base": 1, "kwarg_value": 0} result = decorator("/", opt=base_opt, kwarg_value=2)(handler) # type: ignore[arg-type, call-arg] assert result.opt == {"base": 1, "kwarg_value": 2} @pytest.mark.parametrize( "app_opt, router_opt, controller_opt, route_opt, expected_opt", [ [ {"app": "app"}, {"router": "router"}, {"controller": "controller"}, {"route": "route"}, {"app": "app", "router": "router", "controller": "controller", "route": "route"}, ], [ {"override": "app"}, {"router": "router"}, None, {"override": "route"}, {"router": "router", "override": "route"}, ], [None, None, None, None, {}], ], ) def test_opt_resolution( app_opt: Dict[str, Any], router_opt: Dict[str, Any], controller_opt: Dict[str, Any], route_opt: Dict[str, Any], expected_opt: Dict[str, Any], ) -> None: class MyController(Controller): path = "/controller" opt = controller_opt @get(opt=route_opt) def handler(self) -> None: ... router = Router("/router", route_handlers=[MyController], opt=router_opt) app = Litestar(route_handlers=[router], opt=app_opt) assert ( app.asgi_router.root_route_map_node.children["/router/controller"].asgi_handlers["GET"][1].opt == expected_opt ) def test_opt_not_affected_by_route_handler_copying() -> None: class MyController(Controller): path = "/controller" @get(opt={"route": "route"}) def handler(self) -> None: ... @get("/fn_handler", opt={"fn_route": "fn_route"}) def fn_handler() -> None: ... router = Router("/router", route_handlers=[MyController, fn_handler], opt={"router": "router"}) another_router = Router("/another_router", route_handlers=[MyController, fn_handler]) app = Litestar(route_handlers=[router, another_router]) assert app.asgi_router.root_route_map_node.children["/router/controller"].asgi_handlers["GET"][1].opt == { "router": "router", "route": "route", } assert app.asgi_router.root_route_map_node.children["/router/fn_handler"].asgi_handlers["GET"][1].opt == { "router": "router", "fn_route": "fn_route", } assert app.asgi_router.root_route_map_node.children["/another_router/controller"].asgi_handlers["GET"][1].opt == { "route": "route", } assert app.asgi_router.root_route_map_node.children["/another_router/fn_handler"].asgi_handlers["GET"][1].opt == { "fn_route": "fn_route", } litestar-2.16.0/tests/unit/test_handlers/test_base_handlers/test_resolution.py000066400000000000000000000040161500564371300300610ustar00rootroot00000000000000from typing import Awaitable, Callable from litestar import Controller, Litestar, Router, get from litestar.di import Provide def test_resolve_dependencies_without_provide() -> None: async def foo() -> None: pass async def bar() -> None: pass @get(dependencies={"foo": foo, "bar": Provide(bar)}) async def handler() -> None: pass assert handler.resolve_dependencies() == {"foo": Provide(foo), "bar": Provide(bar)} def function_factory() -> Callable[[], Awaitable[None]]: async def func() -> None: return None return func def test_resolve_from_layers() -> None: app_dependency = function_factory() router_dependency = function_factory() controller_dependency = function_factory() handler_dependency = function_factory() class MyController(Controller): path = "/controller" dependencies = {"controller": controller_dependency} @get("/handler", dependencies={"handler": handler_dependency}, name="foo") async def handler(self) -> None: pass router = Router("/router", route_handlers=[MyController], dependencies={"router": router_dependency}) app = Litestar([router], dependencies={"app": app_dependency}, openapi_config=None) handler_map = app.get_handler_index_by_name("foo") assert handler_map handler = handler_map["handler"] assert handler.resolve_dependencies() == { "app": Provide(app_dependency), "router": Provide(router_dependency), "controller": Provide(controller_dependency), "handler": Provide(handler_dependency), } def test_resolve_dependencies_cached() -> None: dependency = Provide(function_factory()) @get(dependencies={"foo": dependency}) async def handler() -> None: pass @get(dependencies={"foo": dependency}) async def handler_2() -> None: pass assert handler.resolve_dependencies() is handler.resolve_dependencies() assert handler_2.resolve_dependencies() is handler_2.resolve_dependencies() litestar-2.16.0/tests/unit/test_handlers/test_base_handlers/test_validations.py000066400000000000000000000014751500564371300302010ustar00rootroot00000000000000from dataclasses import dataclass import pytest from litestar import Litestar, post from litestar.dto import DTOData from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers.base import BaseRouteHandler def test_raise_no_fn_validation() -> None: handler = BaseRouteHandler(path="/") with pytest.raises(ImproperlyConfiguredException): handler.fn def test_dto_data_annotation_with_no_resolved_dto() -> None: @dataclass class Model: """Example dataclass model.""" hello: str @post("/") async def async_hello_world(data: DTOData[Model]) -> Model: """Route Handler that outputs hello world.""" return data.create_instance() with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[async_hello_world]) litestar-2.16.0/tests/unit/test_handlers/test_http_handlers/000077500000000000000000000000001500564371300243115ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_handlers/test_http_handlers/__init__.py000066400000000000000000000000001500564371300264100ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_handlers/test_http_handlers/test_defaults.py000066400000000000000000000026551500564371300275410ustar00rootroot00000000000000from typing import Any import pytest from litestar import HttpMethod from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT @pytest.mark.parametrize( "http_method, expected_status_code", [ (HttpMethod.POST, HTTP_201_CREATED), (HttpMethod.DELETE, HTTP_204_NO_CONTENT), (HttpMethod.GET, HTTP_200_OK), (HttpMethod.HEAD, HTTP_200_OK), (HttpMethod.PUT, HTTP_200_OK), (HttpMethod.PATCH, HTTP_200_OK), ([HttpMethod.POST], HTTP_201_CREATED), ([HttpMethod.DELETE], HTTP_204_NO_CONTENT), ([HttpMethod.GET], HTTP_200_OK), ([HttpMethod.HEAD], HTTP_200_OK), ([HttpMethod.PUT], HTTP_200_OK), ([HttpMethod.PATCH], HTTP_200_OK), ("POST", HTTP_201_CREATED), ("DELETE", HTTP_204_NO_CONTENT), ("GET", HTTP_200_OK), ("HEAD", HTTP_200_OK), ("PUT", HTTP_200_OK), ("PATCH", HTTP_200_OK), (["POST"], HTTP_201_CREATED), (["DELETE"], HTTP_204_NO_CONTENT), (["GET"], HTTP_200_OK), (["HEAD"], HTTP_200_OK), (["PUT"], HTTP_200_OK), (["PATCH"], HTTP_200_OK), ], ) def test_route_handler_default_status_code(http_method: Any, expected_status_code: int) -> None: route_handler = HTTPRouteHandler(http_method=http_method) assert route_handler.status_code == expected_status_code litestar-2.16.0/tests/unit/test_handlers/test_http_handlers/test_delete.py000066400000000000000000000005471500564371300271720ustar00rootroot00000000000000from litestar import delete from litestar.testing import create_test_client def test_handler_return_none_and_204_status_response_empty() -> None: @delete(path="/") async def route() -> None: return None with create_test_client(route_handlers=[route]) as client: response = client.delete("/") assert not response.content litestar-2.16.0/tests/unit/test_handlers/test_http_handlers/test_deprecation.py000066400000000000000000000015231500564371300302200ustar00rootroot00000000000000from __future__ import annotations from importlib import reload from warnings import catch_warnings, simplefilter import pytest from litestar.handlers import delete, get, head, patch, post, put @pytest.mark.parametrize("handler_cls", [get, post, put, patch, delete, head]) def test_subclass_warns_deprecation(handler_cls: get | post | put | patch | delete | head) -> None: with pytest.warns(DeprecationWarning): class SubClass(handler_cls): # type: ignore[valid-type, misc] pass def test_default_no_warns() -> None: with catch_warnings(record=True) as warnings: simplefilter("always") import litestar.handlers.http_handlers.decorators reload(litestar.handlers.http_handlers.decorators) assert len(warnings) == 0 # revert to previous filter simplefilter("default") litestar-2.16.0/tests/unit/test_handlers/test_http_handlers/test_head.py000066400000000000000000000040341500564371300266240ustar00rootroot00000000000000from pathlib import Path from typing import Generic, TypeVar import pytest from litestar import HttpMethod, Litestar, Response, head from litestar.exceptions import ImproperlyConfiguredException from litestar.response.file import ASGIFileResponse, File from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client def test_head_decorator() -> None: @head("/") def handler() -> None: return with create_test_client(handler) as client: response = client.head("/") assert response.status_code == HTTP_200_OK def test_head_decorator_raises_validation_error_if_body_is_declared() -> None: with pytest.raises(ImproperlyConfiguredException): @head("/") def handler() -> dict: return {} Litestar(route_handlers=[handler]) def test_head_decorator_none_response_return_value_allowed() -> None: # https://github.com/litestar-org/litestar/issues/3640 T = TypeVar("T") class MyResponse(Generic[T], Response[T]): pass @head("/1") def handler() -> Response[None]: return Response(None) @head("/2") def handler_subclass() -> MyResponse[None]: return MyResponse(None) Litestar(route_handlers=[handler, handler_subclass]) def test_head_decorator_raises_validation_error_if_method_is_passed() -> None: with pytest.raises(ImproperlyConfiguredException): @head("/", http_method=HttpMethod.HEAD) def handler() -> None: return handler.on_registration(Litestar()) def test_head_decorator_does_not_raise_for_file_response() -> None: @head("/") def handler() -> "File": return File("test_to_response.py") Litestar(route_handlers=[handler]) handler.on_registration(Litestar()) def test_head_decorator_does_not_raise_for_asgi_file_response() -> None: @head("/") def handler() -> ASGIFileResponse: return ASGIFileResponse(file_path=Path("test_head.py")) Litestar(route_handlers=[handler]) handler.on_registration(Litestar()) litestar-2.16.0/tests/unit/test_handlers/test_http_handlers/test_kwarg_handling.py000066400000000000000000000061351500564371300307060ustar00rootroot00000000000000from typing import Any, Optional, Type import pytest from hypothesis import given from hypothesis import strategies as st from litestar import HttpMethod, MediaType, Response, delete, get, patch, post, put from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.handlers.http_handlers._utils import get_default_status_code from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT from litestar.utils import normalize_path def dummy_method() -> None: pass @given( http_method=st.one_of(st.sampled_from(HttpMethod), st.lists(st.sampled_from(HttpMethod))), media_type=st.sampled_from(MediaType), include_in_schema=st.booleans(), response_class=st.one_of(st.none(), st.just(Response)), response_headers=st.one_of(st.none(), st.builds(list)), status_code=st.one_of(st.none(), st.integers(min_value=200, max_value=204)), path=st.one_of(st.none(), st.text()), ) def test_route_handler_kwarg_handling( http_method: Any, media_type: MediaType, include_in_schema: bool, response_class: Optional[Type[Response]], response_headers: Any, status_code: Any, path: Any, ) -> None: if not http_method: with pytest.raises(ImproperlyConfiguredException): HTTPRouteHandler(http_method=http_method) else: decorator = HTTPRouteHandler( http_method=http_method, media_type=media_type, include_in_schema=include_in_schema, response_class=response_class, response_headers=response_headers, status_code=status_code, path=path, ) result = decorator(dummy_method) if isinstance(http_method, list): assert all(method in result.http_methods for method in http_method) else: assert http_method in result.http_methods assert result.media_type == media_type assert result.include_in_schema == include_in_schema assert result.response_class == response_class assert result.response_headers == response_headers if not path: assert result.paths == {"/"} else: assert next(iter(result.paths)) == normalize_path(path) assert result.status_code == status_code or get_default_status_code(http_methods=result.http_methods) @pytest.mark.parametrize( "sub, http_method, expected_status_code", [ (post, HttpMethod.POST, HTTP_201_CREATED), (delete, HttpMethod.DELETE, HTTP_204_NO_CONTENT), (get, HttpMethod.GET, HTTP_200_OK), (put, HttpMethod.PUT, HTTP_200_OK), (patch, HttpMethod.PATCH, HTTP_200_OK), ], ) def test_semantic_route_handlers_disallow_http_method_assignment( sub: Any, http_method: Any, expected_status_code: int ) -> None: result = sub()(dummy_method) assert http_method in result.http_methods assert result.status_code == expected_status_code with pytest.raises(ImproperlyConfiguredException): sub(http_method=HttpMethod.GET if http_method != HttpMethod.GET else HttpMethod.POST) litestar-2.16.0/tests/unit/test_handlers/test_http_handlers/test_media_type.py000066400000000000000000000016341500564371300300460ustar00rootroot00000000000000from enum import Enum from typing import Any, AnyStr import pytest from litestar import Litestar, MediaType, get from tests.models import DataclassPerson class MyEnum(Enum): first = 1 class MyBytes(bytes): ... class CustomStrEnum(str, Enum): foo = "FOO" @pytest.mark.parametrize( "annotation, expected_media_type", ( (str, MediaType.TEXT), (bytes, MediaType.TEXT), (AnyStr, MediaType.TEXT), (MyBytes, MediaType.TEXT), (CustomStrEnum, MediaType.TEXT), (MyEnum, MediaType.JSON), (dict, MediaType.JSON), (DataclassPerson, MediaType.JSON), ), ) def test_media_type_inference(annotation: Any, expected_media_type: MediaType) -> None: @get("/") def handler() -> annotation: return None Litestar(route_handlers=[handler]) handler.on_registration(Litestar()) assert handler.media_type == expected_media_type litestar-2.16.0/tests/unit/test_handlers/test_http_handlers/test_resolution.py000066400000000000000000000037071500564371300301340ustar00rootroot00000000000000import pytest from litestar import Controller, Litestar, Router, post from litestar.exceptions import ImproperlyConfiguredException from litestar.types import Empty def test_resolve_request_max_body_size() -> None: @post("/1") def router_handler() -> None: pass @post("/2") def app_handler() -> None: pass class MyController(Controller): request_max_body_size = 2 @post("/3") def controller_handler(self) -> None: pass router = Router("/", route_handlers=[router_handler], request_max_body_size=1) app = Litestar(route_handlers=[app_handler, router, MyController], request_max_body_size=3) assert router_handler.resolve_request_max_body_size() == 1 assert app_handler.resolve_request_max_body_size() == 3 assert ( next(r for r in app.routes if r.path == "/3").route_handler_map["POST"][0].resolve_request_max_body_size() == 2 # type: ignore[union-attr] ) def test_resolve_request_max_body_size_none() -> None: @post("/1", request_max_body_size=None) def router_handler() -> None: pass Litestar([router_handler]) assert router_handler.resolve_request_max_body_size() is None def test_resolve_request_max_body_size_app_default() -> None: @post("/") def router_handler() -> None: pass app = Litestar(route_handlers=[router_handler]) assert router_handler.resolve_request_max_body_size() == app.request_max_body_size == 10_000_000 def test_resolve_request_max_body_size_empty_on_all_layers_raises() -> None: @post("/") def handler_one() -> None: pass Litestar([handler_one], request_max_body_size=Empty) # type: ignore[arg-type] with pytest.raises(ImproperlyConfiguredException): handler_one.resolve_request_max_body_size() @post("/") def handler_two() -> None: pass with pytest.raises(ImproperlyConfiguredException): handler_two.resolve_request_max_body_size() litestar-2.16.0/tests/unit/test_handlers/test_http_handlers/test_signature_namespace.py000066400000000000000000000025651500564371300317470ustar00rootroot00000000000000from __future__ import annotations from typing import Any, Dict, List import pytest from litestar import Controller, Router, delete, get, patch, post, put from litestar.testing import create_test_client @pytest.mark.parametrize( ("method", "decorator"), [("GET", get), ("PUT", put), ("POST", post), ("PATCH", patch), ("DELETE", delete)] ) def test_websocket_signature_namespace(method: str, decorator: type[get | put | post | patch | delete]) -> None: class MyController(Controller): path = "/" signature_namespace = {"c": float} @decorator(path="/", signature_namespace={"d": List[str], "dict": Dict}, status_code=200) # type:ignore[misc] async def simple_handler( self, a: a, # type:ignore[name-defined] # noqa: F821 b: b, # type:ignore[name-defined] # noqa: F821 c: c, # type:ignore[name-defined] # noqa: F821 d: d, # type:ignore[name-defined] # noqa: F821 ) -> dict[str, Any]: return {"a": a, "b": b, "c": c, "d": d} router = Router("/", route_handlers=[MyController], signature_namespace={"b": str}) with create_test_client(route_handlers=[router], signature_namespace={"a": int}) as client: response = client.request(method=method, url="/?a=1&b=two&c=3.0&d=d") assert response.json() == {"a": 1, "b": "two", "c": 3.0, "d": ["d"]} litestar-2.16.0/tests/unit/test_handlers/test_http_handlers/test_sync.py000066400000000000000000000020771500564371300267040ustar00rootroot00000000000000import pytest from litestar import MediaType, get from litestar.exceptions import LitestarWarning from litestar.testing import create_test_client def sync_handler() -> str: return "Hello World" @pytest.mark.parametrize("sync_to_thread", [True, False]) def test_sync_to_thread(sync_to_thread: bool) -> None: handler = get("/", media_type=MediaType.TEXT, sync_to_thread=sync_to_thread)(sync_handler) with create_test_client(handler) as client: response = client.get("/") assert response.text == "Hello World" @pytest.mark.usefixtures("enable_warn_implicit_sync_to_thread") def test_sync_to_thread_not_set_warns() -> None: with pytest.warns(LitestarWarning, match="discouraged since synchronous callables"): get("/")(sync_handler) @pytest.mark.parametrize("sync_to_thread", [True, False]) def test_async_callable_with_sync_to_thread_warns(sync_to_thread: bool) -> None: with pytest.warns(LitestarWarning, match="asynchronous callable"): @get(sync_to_thread=sync_to_thread) async def handler() -> None: pass litestar-2.16.0/tests/unit/test_handlers/test_http_handlers/test_validations.py000066400000000000000000000122521500564371300302410ustar00rootroot00000000000000from pathlib import Path from types import ModuleType from typing import Any, Callable, Dict, List import pytest from typing_extensions import Annotated from litestar import HttpMethod, Litestar, WebSocket, delete, get, patch, post, put, route from litestar.exceptions import ImproperlyConfiguredException, ValidationException from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.params import Body from litestar.response import File, Redirect from litestar.status_codes import ( HTTP_100_CONTINUE, HTTP_200_OK, HTTP_304_NOT_MODIFIED, HTTP_307_TEMPORARY_REDIRECT, ) from tests.models import DataclassPerson def test_route_handler_validation_http_method() -> None: # doesn't raise for http methods for value in (*list(HttpMethod), *[x.upper() for x in list(HttpMethod)]): assert route(http_method=value) # type: ignore[arg-type, truthy-bool] # raises for invalid values with pytest.raises(ValidationException): HTTPRouteHandler(http_method="deleze") # type: ignore[arg-type] # also when passing an empty list with pytest.raises(ImproperlyConfiguredException): route(http_method=[], status_code=HTTP_200_OK) # also when passing malformed tokens with pytest.raises(ValidationException): route(http_method=[HttpMethod.GET, "poft"], status_code=HTTP_200_OK) # type: ignore[list-item] async def test_function_validation() -> None: with pytest.raises(ImproperlyConfiguredException): @get(path="/") def method_with_no_annotation(): # type: ignore[no-untyped-def] pass Litestar(route_handlers=[method_with_no_annotation]) method_with_no_annotation.on_registration(Litestar()) with pytest.raises(ImproperlyConfiguredException): @delete(path="/") def method_with_no_content() -> Dict[str, str]: return {} Litestar(route_handlers=[method_with_no_content]) method_with_no_content.on_registration(Litestar()) with pytest.raises(ImproperlyConfiguredException): @get(path="/", status_code=HTTP_304_NOT_MODIFIED) def method_with_not_modified() -> Dict[str, str]: return {} Litestar(route_handlers=[method_with_not_modified]) method_with_not_modified.on_registration(Litestar()) with pytest.raises(ImproperlyConfiguredException): @get(path="/", status_code=HTTP_100_CONTINUE) def method_with_status_lower_than_200() -> Dict[str, str]: return {} Litestar(route_handlers=[method_with_status_lower_than_200]) method_with_status_lower_than_200.on_registration(Litestar()) @get(path="/", status_code=HTTP_307_TEMPORARY_REDIRECT) def redirect_method() -> Redirect: return Redirect("/test") Litestar(route_handlers=[redirect_method]) redirect_method.on_registration(Litestar()) @get(path="/") def file_method() -> File: return File(path=Path("."), filename="test_validations.py") Litestar(route_handlers=[file_method]) file_method.on_registration(Litestar()) assert not file_method.media_type with pytest.raises(ImproperlyConfiguredException): @get(path="/test") def test_function_1(socket: WebSocket) -> None: return None test_function_1.on_registration(Litestar()) with pytest.raises(ImproperlyConfiguredException): @get("/person") def test_function_2(self, data: DataclassPerson) -> None: # type: ignore[no-untyped-def] return None Litestar(route_handlers=[test_function_2]) test_function_2.on_registration(Litestar()) @pytest.mark.parametrize( ("return_annotation", "should_raise"), [ ("None", False), ("Response[None]", False), ("int", True), ("Response[int]", True), ("Response", True), ], ) def test_204_response_annotations( return_annotation: str, should_raise: bool, create_module: Callable[[str], ModuleType] ) -> None: module = create_module( f""" from litestar import get from litestar.response import Response from litestar.status_codes import HTTP_204_NO_CONTENT @get(path="/", status_code=HTTP_204_NO_CONTENT) def no_response_handler() -> {return_annotation}: pass """ ) if should_raise: with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[module.no_response_handler]) return Litestar(route_handlers=[module.no_response_handler]) @pytest.mark.parametrize("decorator", [post, put, patch]) def test_body_param_with_non_bytes_annotation_raises(decorator: Callable[..., Any]) -> None: def handler_fn(body: List[str]) -> None: pass with pytest.raises(ImproperlyConfiguredException, match="Invalid type annotation for 'body' parameter"): Litestar([decorator()(handler_fn)]) @pytest.mark.parametrize("decorator", [post, put, patch]) def test_body_param_with_metadata_allowed(decorator: Callable[..., Any]) -> None: def handler_fn(body: Annotated[bytes, Body(title="something")]) -> None: pass # we expect no error here, even though the type isn't directly 'bytes' but has # metadata attached to it Litestar([decorator()(handler_fn)]) litestar-2.16.0/tests/unit/test_handlers/test_websocket_handlers/000077500000000000000000000000001500564371300253205ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_handlers/test_websocket_handlers/__init__.py000066400000000000000000000000001500564371300274170ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_handlers/test_websocket_handlers/test_handle_websocket.py000066400000000000000000000034401500564371300322330ustar00rootroot00000000000000from typing import List from litestar import Controller, Router, WebSocket, websocket from litestar.testing import create_test_client def test_handle_websocket() -> None: @websocket(path="/") async def simple_websocket_handler(socket: WebSocket) -> None: await socket.accept() data = await socket.receive_json() assert data await socket.send_json({"data": "123"}) await socket.close() client = create_test_client(route_handlers=simple_websocket_handler) with client.websocket_connect("/") as ws: ws.send_json({"data": "123"}) data = ws.receive_json() assert data def test_websocket_signature_namespace() -> None: class MyController(Controller): path = "/ws" signature_namespace = {"c": float} @websocket(path="/", signature_namespace={"d": List[str]}) async def simple_websocket_handler( self, socket: WebSocket, a: "a", # type:ignore[name-defined] # noqa: F821 b: "b", # type:ignore[name-defined] # noqa: F821 c: "c", # type:ignore[name-defined] # noqa: F821 d: "d", # type:ignore[name-defined] # noqa: F821 ) -> None: await socket.accept() data = await socket.receive_json() assert data await socket.send_json({"a": a, "b": b, "c": c, "d": d}) await socket.close() router = Router("/", route_handlers=[MyController], signature_namespace={"b": str}) client = create_test_client(route_handlers=[router], signature_namespace={"a": int}) with client.websocket_connect("/ws?a=1&b=two&c=3.0&d=d") as ws: ws.send_json({"data": "123"}) data = ws.receive_json() assert data == {"a": 1, "b": "two", "c": 3.0, "d": ["d"]} test_handle_websocket_with_future_annotations.py000066400000000000000000000011771500564371300372230ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_handlers/test_websocket_handlersfrom __future__ import annotations from litestar import WebSocket, websocket from litestar.testing import create_test_client def test_handle_websocket() -> None: @websocket(path="/") async def simple_websocket_handler(socket: WebSocket) -> None: await socket.accept() data = await socket.receive_json() assert data await socket.send_json({"data": "123"}) await socket.close() client = create_test_client(route_handlers=simple_websocket_handler) with client.websocket_connect("/") as ws: ws.send_json({"data": "123"}) data = ws.receive_json() assert data litestar-2.16.0/tests/unit/test_handlers/test_websocket_handlers/test_kwarg_handling.py000066400000000000000000000021661500564371300317150ustar00rootroot00000000000000from litestar import WebSocket, websocket from litestar.params import Parameter from litestar.testing import create_test_client def test_handle_websocket_params_parsing() -> None: @websocket(path="/{socket_id:int}") async def websocket_handler( socket: WebSocket, headers: dict, query: dict, cookies: dict, socket_id: int, qp: int, hp: str = Parameter(header="some-header"), ) -> None: assert socket_id assert headers assert query assert cookies assert qp assert hp await socket.accept() data = await socket.receive_json() assert data await socket.send_json({"data": "123"}) await socket.close() client = create_test_client(route_handlers=websocket_handler) # Set cookies on the client to avoid warnings about per-request cookies. client.cookies = {"cookie": "yum"} # type: ignore[assignment] with client.websocket_connect("/1?qp=1", headers={"some-header": "abc"}) as ws: ws.send_json({"data": "123"}) data = ws.receive_json() assert data litestar-2.16.0/tests/unit/test_handlers/test_websocket_handlers/test_listeners.py000066400000000000000000000340561500564371300307510ustar00rootroot00000000000000from contextlib import asynccontextmanager from dataclasses import dataclass, field from typing import Any, AsyncGenerator, Dict, List, Optional, Type, Union, cast from unittest.mock import AsyncMock, MagicMock import pytest from pytest_lazy_fixtures import lf from litestar import Controller, Litestar, Request, WebSocket from litestar.datastructures import State from litestar.di import Provide from litestar.dto import DataclassDTO, dto_field from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers.websocket_handlers import WebsocketListener, websocket_listener from litestar.testing import create_test_client from litestar.types.asgi_types import WebSocketMode @pytest.fixture def listener_class(mock: MagicMock) -> Type[WebsocketListener]: class Listener(WebsocketListener): def on_receive(self, data: str) -> str: # pyright: ignore mock(data) return data return Listener @pytest.fixture def sync_listener_callable(mock: MagicMock) -> websocket_listener: def listener(data: str) -> str: mock(data) return data return websocket_listener("/")(listener) @pytest.fixture def async_listener_callable(mock: MagicMock) -> websocket_listener: async def listener(data: str) -> str: mock(data) return data return websocket_listener("/")(listener) @pytest.mark.parametrize( "listener", [ lf("sync_listener_callable"), lf("async_listener_callable"), lf("listener_class"), ], ) def test_basic_listener(mock: MagicMock, listener: Union[websocket_listener, Type[WebsocketListener]]) -> None: client = create_test_client([listener]) with client.websocket_connect("/") as ws: ws.send_text("foo") assert ws.receive_text() == "foo" ws.send_text("bar") assert ws.receive_text() == "bar" assert mock.call_count == 2 mock.assert_any_call("foo") mock.assert_any_call("bar") @pytest.mark.parametrize("receive_mode", ["text", "binary"]) def test_listener_receive_bytes(receive_mode: WebSocketMode, mock: MagicMock) -> None: @websocket_listener("/", receive_mode=receive_mode) def handler(data: bytes) -> None: mock(data) client = create_test_client([handler]) with client.websocket_connect("/") as ws: ws.send("foo", mode=receive_mode) mock.assert_called_once_with(b"foo") @pytest.mark.parametrize("receive_mode", ["text", "binary"]) def test_listener_receive_string(receive_mode: WebSocketMode, mock: MagicMock) -> None: @websocket_listener("/", receive_mode=receive_mode) def handler(data: str) -> None: mock(data) client = create_test_client([handler]) with client.websocket_connect("/") as ws: ws.send("foo", mode=receive_mode) mock.assert_called_once_with("foo") @pytest.mark.parametrize("receive_mode", ["text", "binary"]) def test_listener_receive_json(receive_mode: WebSocketMode, mock: MagicMock) -> None: @websocket_listener("/", receive_mode=receive_mode) def handler(data: List[str]) -> None: mock(data) client = create_test_client([handler]) with client.websocket_connect("/") as ws: ws.send_json(["foo", "bar"], mode=receive_mode) mock.assert_called_once_with(["foo", "bar"]) @dataclass class User: name: str hidden: str = field(default="super secret", metadata=dto_field("private")) @pytest.mark.parametrize("receive_mode", ["text", "binary"]) def test_listener_receive_with_dto(receive_mode: WebSocketMode) -> None: user_dto = DataclassDTO[User] value: Any = None @websocket_listener("/", receive_mode=receive_mode, dto=user_dto, return_dto=None) def handler(data: User) -> None: nonlocal value value = data client = create_test_client([handler], openapi_config=None) with client.websocket_connect("/") as ws: ws.send_json({"name": "litestar user", "hidden": "whoops"}, mode=receive_mode) assert isinstance(value, User) assert value.name == "litestar user" assert value.hidden == "super secret" @pytest.mark.parametrize("send_mode", ["text", "binary"]) def test_listener_return_bytes(send_mode: WebSocketMode) -> None: @websocket_listener("/", send_mode=send_mode) def handler(data: str) -> bytes: return data.encode("utf-8") client = create_test_client([handler]) with client.websocket_connect("/") as ws: ws.send_text("foo") if send_mode == "text": assert ws.receive_text() == "foo" else: assert ws.receive_bytes() == b"foo" @pytest.mark.parametrize("send_mode", ["text", "binary"]) def test_listener_send_json(send_mode: WebSocketMode) -> None: @websocket_listener("/", send_mode=send_mode) def handler(data: str) -> Dict[str, str]: return {"data": data} client = create_test_client([handler]) with client.websocket_connect("/") as ws: ws.send_text("foo") assert ws.receive_json(mode=send_mode) == {"data": "foo"} @pytest.mark.parametrize("send_mode", ["text", "binary"]) def test_listener_send_with_dto(send_mode: WebSocketMode, mock: MagicMock) -> None: @dataclass class User: name: str hidden: str = field(default="super secret", metadata=dto_field("private")) user_dto = DataclassDTO[User] @websocket_listener("/", send_mode=send_mode, dto=user_dto, signature_namespace={"User": User}) def handler(data: User) -> User: return data client = create_test_client([handler]) with client.websocket_connect("/") as ws: ws.send_json({"name": "litestar user"}) assert ws.receive_json(mode=send_mode) == {"name": "litestar user"} def test_listener_return_none() -> None: @websocket_listener("/") def handler(data: str) -> None: return data # type: ignore[return-value] client = create_test_client([handler]) with client.websocket_connect("/") as ws: ws.send_text("foo") def test_listener_return_optional_none() -> None: @websocket_listener("/") def handler(data: str) -> Optional[str]: return "world" if data == "hello" else None client = create_test_client([handler]) with client.websocket_connect("/") as ws: ws.send_text("hello") assert ws.receive_text() == "world" ws.send_text("goodbye") def test_listener_pass_socket(mock: MagicMock) -> None: @websocket_listener("/") def handler(data: str, socket: WebSocket) -> Dict[str, str]: mock(socket=socket) return {"data": data} client = create_test_client([handler]) with client.websocket_connect("/") as ws: ws.send_text("foo") assert ws.receive_json() == {"data": "foo"} assert isinstance(mock.call_args.kwargs["socket"], WebSocket) def test_listener_pass_additional_dependencies(mock: MagicMock) -> None: async def foo_dependency(state: State) -> int: if not hasattr(state, "foo"): state.foo = 0 state.foo += 1 return cast("int", state.foo) @websocket_listener("/", dependencies={"foo": Provide(foo_dependency)}) def handler(data: str, foo: int) -> Dict[str, Union[str, int]]: return {"data": data, "foo": foo} client = create_test_client([handler]) with client.websocket_connect("/") as ws: ws.send_text("something") ws.send_text("something") assert ws.receive_json() == {"data": "something", "foo": 1} def test_listener_callback_no_data_arg_raises() -> None: with pytest.raises(ImproperlyConfiguredException): @websocket_listener("/") def handler() -> None: ... handler.on_registration(Litestar()) def test_listener_callback_request_and_body_arg_raises() -> None: with pytest.raises(ImproperlyConfiguredException): @websocket_listener("/") def handler_request(data: str, request: Request) -> None: ... handler_request.on_registration(Litestar()) with pytest.raises(ImproperlyConfiguredException): @websocket_listener("/") def handler_body(data: str, body: bytes) -> None: ... handler_body.on_registration(Litestar()) def test_listener_accept_connection_callback() -> None: async def accept_connection(socket: WebSocket) -> None: await socket.accept(headers={"Cookie": "custom-cookie"}) @websocket_listener("/", connection_accept_handler=accept_connection) def handler(data: bytes) -> None: return None client = create_test_client([handler]) with client.websocket_connect("/") as ws: assert ws.extra_headers == [(b"cookie", b"custom-cookie")] def test_connection_callbacks() -> None: on_accept_mock = MagicMock() on_disconnect_mock = MagicMock() def on_accept(socket: WebSocket) -> None: on_accept_mock() def on_disconnect(socket: WebSocket) -> None: on_disconnect_mock() @websocket_listener("/", on_accept=on_accept, on_disconnect=on_disconnect) def handler(data: bytes) -> None: pass client = create_test_client([handler]) with client.websocket_connect("/"): pass on_accept_mock.assert_called_once() on_disconnect_mock.assert_called_once() def test_connection_lifespan() -> None: on_accept = MagicMock() on_disconnect = MagicMock() @asynccontextmanager async def lifespan(socket: WebSocket) -> AsyncGenerator[None, None]: on_accept(socket) try: yield finally: on_disconnect(socket) @websocket_listener("/", connection_lifespan=lifespan) def handler(data: bytes) -> None: pass client = create_test_client([handler]) with client.websocket_connect("/", timeout=1): pass on_accept.assert_called_once() on_disconnect.assert_called_once() def test_listener_in_controller() -> None: # test for https://github.com/litestar-org/litestar/issues/1615 class ClientController(Controller): path: str = "/" @websocket_listener("/ws") async def websocket_handler(self, data: str, socket: WebSocket) -> str: return data with create_test_client(ClientController) as client, client.websocket_connect("/ws") as ws: ws.send_text("foo") data = ws.receive_text(timeout=1) assert data == "foo" def test_lifespan_dependencies() -> None: mock = MagicMock() @asynccontextmanager async def lifespan(name: str, state: State, query: dict) -> AsyncGenerator[None, None]: mock(name=name, state=state, query=query) yield @websocket_listener("/{name:str}", connection_lifespan=lifespan) async def handler(data: str) -> None: pass with create_test_client([handler]) as client, client.websocket_connect("/foo") as ws: ws.send_text("") assert mock.call_args_list[0].kwargs["name"] == "foo" assert isinstance(mock.call_args_list[0].kwargs["state"], State) assert isinstance(mock.call_args_list[0].kwargs["query"], dict) def test_hook_dependencies() -> None: on_accept_mock = MagicMock() on_disconnect_mock = MagicMock() def some_dependency() -> str: return "hello" def on_accept(name: str, state: State, query: dict, some: str) -> None: on_accept_mock(name=name, state=state, query=query, some=some) def on_disconnect(name: str, state: State, query: dict, some: str) -> None: on_disconnect_mock(name=name, state=state, query=query, some=some) @websocket_listener("/{name: str}", on_accept=on_accept, on_disconnect=on_disconnect) def handler(data: bytes) -> None: pass with create_test_client([handler], dependencies={"some": some_dependency}) as client, client.websocket_connect( "/foo" ) as ws: ws.send_text("") on_accept_kwargs = on_accept_mock.call_args_list[0].kwargs assert on_accept_kwargs["name"] == "foo" assert on_accept_kwargs["some"] == "hello" assert isinstance(on_accept_kwargs["state"], State) assert isinstance(on_accept_kwargs["query"], dict) on_disconnect_kwargs = on_disconnect_mock.call_args_list[0].kwargs assert on_disconnect_kwargs["name"] == "foo" assert on_disconnect_kwargs["some"] == "hello" assert isinstance(on_disconnect_kwargs["state"], State) assert isinstance(on_disconnect_kwargs["query"], dict) def test_websocket_listener_class_hook_dependencies() -> None: on_accept_mock = MagicMock() on_disconnect_mock = MagicMock() def some_dependency() -> str: return "hello" class Listener(WebsocketListener): path = "/{name: str}" def on_accept(self, name: str, state: State, query: dict, some: str) -> None: # pyright: ignore on_accept_mock(name=name, state=state, query=query, some=some) def on_disconnect(self, name: str, state: State, query: dict, some: str) -> None: # pyright: ignore on_disconnect_mock(name=name, state=state, query=query, some=some) def on_receive(self, data: bytes) -> None: # pyright: ignore pass with create_test_client([Listener], dependencies={"some": some_dependency}) as client, client.websocket_connect( "/foo" ) as ws: ws.send_text("") on_accept_kwargs = on_accept_mock.call_args_list[0].kwargs assert on_accept_kwargs["name"] == "foo" assert on_accept_kwargs["some"] == "hello" assert isinstance(on_accept_kwargs["state"], State) assert isinstance(on_accept_kwargs["query"], dict) on_disconnect_kwargs = on_disconnect_mock.call_args_list[0].kwargs assert on_disconnect_kwargs["name"] == "foo" assert on_disconnect_kwargs["some"] == "hello" assert isinstance(on_disconnect_kwargs["state"], State) assert isinstance(on_disconnect_kwargs["query"], dict) @pytest.mark.parametrize("hook_name", ["on_accept", "on_disconnect", "connection_accept_handler"]) def test_listeners_lifespan_hooks_and_manager_raises(hook_name: str) -> None: @asynccontextmanager async def lifespan() -> AsyncGenerator[None, None]: yield hook_callback = AsyncMock() with pytest.raises(ImproperlyConfiguredException): @websocket_listener("/", **{hook_name: hook_callback}, connection_lifespan=lifespan) # pyright: ignore def handler(data: bytes) -> None: pass litestar-2.16.0/tests/unit/test_handlers/test_websocket_handlers/test_stream.py000066400000000000000000000122021500564371300302210ustar00rootroot00000000000000from __future__ import annotations import asyncio import dataclasses from typing import AsyncGenerator, Dict, Generator from unittest.mock import MagicMock import pytest from litestar import Controller, Litestar, WebSocket from litestar.dto import DataclassDTO, dto_field from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers.websocket_handlers import websocket_stream from litestar.testing import create_test_client def test_websocket_stream() -> None: @websocket_stream("/") async def handler(socket: WebSocket) -> AsyncGenerator[str, None]: yield "foo" yield "bar" with create_test_client([handler]) as client, client.websocket_connect("/") as ws: assert ws.receive_text(timeout=0.1) == "foo" assert ws.receive_text(timeout=0.1) == "bar" def test_websocket_stream_in_controller() -> None: class MyController(Controller): @websocket_stream("/") async def handler(self, socket: WebSocket) -> AsyncGenerator[str, None]: yield "foo" with create_test_client([MyController]) as client, client.websocket_connect("/") as ws: assert ws.receive_text(timeout=0.1) == "foo" def test_websocket_stream_without_socket() -> None: @websocket_stream("/") async def handler() -> AsyncGenerator[str, None]: yield "foo" with create_test_client([handler]) as client, client.websocket_connect("/") as ws: assert ws.receive_text(timeout=0.1) == "foo" def test_websocket_stream_dependency_injection() -> None: async def provide_hello() -> str: return "hello" # ensure we can inject dependencies @websocket_stream("/1", dependencies={"greeting": provide_hello}) async def handler_one(greeting: str) -> AsyncGenerator[str, None]: yield greeting # ensure dependency injection also works with 'socket' present @websocket_stream("/2", dependencies={"greeting": provide_hello}) async def handler_two(socket: WebSocket, greeting: str) -> AsyncGenerator[str, None]: yield greeting with create_test_client([handler_one, handler_two]) as client: with client.websocket_connect("/1") as ws: assert ws.receive_text(timeout=0.1) == "hello" with client.websocket_connect("/2") as ws: assert ws.receive_text(timeout=0.1) == "hello" def test_websocket_stream_dependencies_cleaned_up_after_stream_close() -> None: mock = MagicMock() async def dep() -> AsyncGenerator[str, None]: yield "foo" mock() @websocket_stream( "/", dependencies={"message": dep}, listen_for_disconnect=False, ) async def handler(socket: WebSocket, message: str) -> AsyncGenerator[str, None]: yield "one" await socket.receive_text() yield message with create_test_client([handler]) as client, client.websocket_connect("/") as ws: assert ws.receive_text(timeout=0.1) == "one" assert mock.call_count == 0 ws.send_text("") assert ws.receive_text(timeout=0.1) == "foo" assert mock.call_count == 1 def test_websocket_stream_handle_disconnect() -> None: @websocket_stream("/") async def handler() -> AsyncGenerator[str, None]: while True: yield "foo" # sleep for longer than our read-timeout to ensure we're disconnecting prematurely await asyncio.sleep(1) with create_test_client([handler]) as client, client.websocket_connect("/") as ws: assert ws.receive_text(timeout=0.1) == "foo" with create_test_client([handler]) as client, client.websocket_connect("/") as ws: # ensure we still disconnect even after receiving some data ws.send_text("") assert ws.receive_text(timeout=0.1) == "foo" def test_websocket_stream_send_json() -> None: @websocket_stream("/") async def handler() -> AsyncGenerator[Dict[str, str], None]: # noqa: UP006 yield {"hello": "there"} yield {"and": "goodbye"} with create_test_client([handler]) as client, client.websocket_connect("/") as ws: assert ws.receive_json(timeout=0.1) == {"hello": "there"} assert ws.receive_json(timeout=0.1) == {"and": "goodbye"} def test_websocket_stream_send_json_with_dto() -> None: @dataclasses.dataclass class Event: id: int = dataclasses.field(metadata=dto_field("private")) content: str @websocket_stream("/", return_dto=DataclassDTO[Event]) async def handler() -> AsyncGenerator[Event, None]: yield Event(id=1, content="hello") with create_test_client([handler], signature_types=[Event]) as client, client.websocket_connect("/") as ws: assert ws.receive_json(timeout=0.1) == {"content": "hello"} def test_raises_if_stream_fn_does_not_return_async_generator() -> None: with pytest.raises(ImproperlyConfiguredException): @websocket_stream("/") # type: ignore[arg-type] def foo() -> Generator[bytes, None, None]: yield b"" Litestar([foo]) with pytest.raises(ImproperlyConfiguredException): @websocket_stream("/") # type: ignore[arg-type] def foo() -> bytes: return b"" Litestar([foo]) litestar-2.16.0/tests/unit/test_handlers/test_websocket_handlers/test_validations.py000066400000000000000000000042531500564371300312520ustar00rootroot00000000000000from typing import Any import pytest from litestar import Litestar, WebSocket, websocket from litestar.exceptions import ImproperlyConfiguredException from litestar.testing import create_test_client def test_raises_when_socket_arg_is_missing() -> None: def fn_without_socket_arg(websocket: WebSocket) -> None: pass with pytest.raises(ImproperlyConfiguredException): websocket(path="/")(fn_without_socket_arg).on_registration(Litestar()) # type: ignore[arg-type] def test_raises_for_return_annotation() -> None: async def fn_with_return_annotation(socket: WebSocket) -> dict: return {} with pytest.raises(ImproperlyConfiguredException): websocket(path="/")(fn_with_return_annotation).on_registration(Litestar()) def test_raises_when_no_function() -> None: websocket_handler_with_no_fn = websocket(path="/") with pytest.raises(ImproperlyConfiguredException): create_test_client(route_handlers=websocket_handler_with_no_fn) def test_raises_when_sync_handler_user() -> None: with pytest.raises(ImproperlyConfiguredException): @websocket(path="/") # type: ignore[arg-type] def sync_websocket_handler(socket: WebSocket) -> None: ... sync_websocket_handler.on_registration(Litestar()) def test_raises_when_data_kwarg_is_used() -> None: with pytest.raises(ImproperlyConfiguredException): @websocket(path="/") async def websocket_handler_with_data_kwarg(socket: WebSocket, data: Any) -> None: ... websocket_handler_with_data_kwarg.on_registration(Litestar()) def test_raises_when_request_kwarg_is_used() -> None: with pytest.raises(ImproperlyConfiguredException): @websocket(path="/") async def websocket_handler_with_request_kwarg(socket: WebSocket, request: Any) -> None: ... websocket_handler_with_request_kwarg.on_registration(Litestar()) def test_raises_when_body_kwarg_is_used() -> None: with pytest.raises(ImproperlyConfiguredException): @websocket(path="/") async def websocket_handler_with_request_kwarg(socket: WebSocket, body: bytes) -> None: ... websocket_handler_with_request_kwarg.on_registration(Litestar()) litestar-2.16.0/tests/unit/test_kwargs/000077500000000000000000000000001500564371300201115ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_kwargs/__init__.py000066400000000000000000000001721500564371300222220ustar00rootroot00000000000000from dataclasses import dataclass @dataclass class Form: name: str age: int programmer: bool value: str litestar-2.16.0/tests/unit/test_kwargs/flower.jpeg000066400000000000000000002162771500564371300222750ustar00rootroot00000000000000JFIF``C  #!!!$'$ & ! C  " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?-;qK\R .)q@ SK77bPqF)Qv)q@ ;PqKv(3Qn)1O% LsOb)RE  ()J(SbRKf)qN.(R\S1ObSF(c j{j*V*=Qq+}6*ҭ:ҝ8S\VDެn)qNSъu- \S(1KZ\Pb7bR❊\Pv\׈+i[3,a&WQ\V5´*+9ADPO :x;[K($_f$c R*w&5uiW*JzgC KID+e9˕"2Y,=ң$@_[SS0(vOƸW☿үNV}8rϚݷiϦ*}ypn )r{v=*ԍ?JM<=;.{?Gd$V-.3-L 4;XYg[_ 8g;mEӭ} qm|z+:ۡ)R,Jc^Ssqs8Shd{`񒖱g[\Qv*&)qNn(?h?b1Oc(?CqE-%!E-%QEQE- *vVnYD^k׼,7j%ש{|en[өEtr}z/=>]_FU# J\*]XSW3񴲹ڨ$<-}L>DgOƺ hWxs4# }Oʺ-cG[9p v6yt摣IЧ+I{vKݽv>ο%4HFeRHThbiW5)&9AH)qE-t.)qK@ \RӱL(S@mF 1we^JPLK֗2q>{ԴR1>UV /f|AHE3TzU8,FA2FC ¨_żI,<>8u>{$.X2Ou<ˋsa٘1Rr|L=jCO䍂a4{FV;QqX+NA=jഒC1NqfFJ*hb|Q\R}>xLQv)h.)qH▗K\R\RR❊1@ .)أ\SIn))ƒiԘc/ S x1Bb.b09PdYqk"U!NS=)qX֚rdYSpG (<5(,֥͊Hӏ41,\\$ gײ6ISybueߏ?J)L|G {gO~ %ԴΟ4֫ч۽{Gqg/TsvmYŚaB&WWq ~5kqOm0yUw_4ȥLkuIn6Vbbq䊽i%u3僄3cu=yk):y3WC3`W=bխ_L[!oʿO䏜8jw0HPZf5j$ƿgB%|BxWдmj@4H}a+UO'p/i-φ],L͢S9sVkAqMTedϪ2-sھ2j攩EtSm\V",ԤȀw\wz"X+,F{޿?f+I=t#N%in6ˑUNK6Vq 5󒓓:1R>y0۶*iBЗ^W5&Bl֫m<؜z\m83Rϐh]5]?+5-"F՜rcYY>W)w.Xa Zr>ɝV)qN.+sbR1N.)Qv)q@SJ7b\PqK\R\RQ~(2~)(1¾ ]F%O!j<%&2\B:/{z}qxLyծ^_7 N1|9kmGq!V۞kZI'== QW %5b مnzа}Ĭ%}ox08W忈f+,OھKh&kWM\JLg a+Qh.Yw5KmmGPnHN^=ZEp \p;KEB+V^ʮuэ.Oh4kB\ſvZڳ"A*] (˳lV>],x~ I՟Ұ?z?h;^=7$YAXd!`)KTkSL~ IaXdSF?cleV"XCꭞnZhK S^#ҼZi{ɗP/s_5UJv=*4cMhoY}15EEi28ӕ#)Һ峵QGՠ&s{GdwZqHWx5Kx&ծ..zmGx95Xּ^)a,̘ߜ֦iwxGMrQrz_¨Jqn߳6VCfιvnPNwקztI(&[s > \\8cRu-"#Q{vH[z%%h$VX2i u_b+23 Oj).ktGݗzs-۫l}SUfy/x źȯ4m9R3<פZ^6څQ"7a^f'zuu(S$xzKX(FY9Ǫv6`AʙiS'_FSܗJ,_i2BMYatBP^i쵋ҥYT\ww,9-IF=x2v#BM/ź=7eh%;ʵ'XuvOj[{X+xF`S敖!y~.cwS3oNNLꖫyớB3bG^$~u+@fv=^c \k 9ajU'>__GXŭ$m,PoGaz~5h/%hXËhzdYwLME*t!E\=k떔-[6%h~ѴO3U).VY~zM٩!"f7rI"GV\Ô-uO9!0oo4&Xb+umZ^l O/*Ԕ5a>[+YWnlErW,OG]UokWޖ/ٳyVk-8pOkZlsM=g8ѡ+̞K BFxX$ ǿH&.{\vOIrtFR-h(˜-Ҫj_dgSKg>8f(?bm;bSF(Q~(7b1@ PqKv(7bP1IIRbՉ[$K3 JѶhN ),36-WshdMD9ݸrG~yJhhKVyG7q TdWHk_x=&x)j^"Yo f:mGf|eJiY.&2m٦*(W)kX[,Qlѧ)cǓHs^jΡU3bgX.zܧμ>/c?R:MHؑ D0ݵ f\GhTp<}+*敏Eg[9m.F!B3q >D -?cX]jБ}n$Jɸ+m[_kc½~ZUp> u&mq~#PK=9v~UvekQ39>Zہ&m#*ڲ/h0zQN',O~)K<T0g .I>oC[glra>̳J9}>zya013NKOR2jFgm'(Qq7ԼiV&'m1#cdK GR0Ņ!iH?=3Y&yWh8tWP|RE֊m].~'/Q9.`Vl֬^i@L3)*##_Qi|V${/j~z|›ǵZbG^$+X3EeLp5.Hoi`Zs\݌A'tFאsڸe瑜4U(3EmS 5(&RYޭOμfSl#,H5xVL ;fDfqT[ L3nžQPHA*éܬ+JMg?UxGgC1qZUT0v45Serї{?z&(<`h83b0zRbS 3b\P6ъb#&ڗmi)1ȩvbIvnkq^ol&x6~޽+NȰ"ߠN5^3 MV~fPB4$;(QX*,ⱛ6WH++ڙ>2Qcߣ %k|/xO{[gè57j=Oqn92"V)ٓ˞!ϨN:DZNTdD }FèaسvA־ּdQ yMg.@EȄX)ǚʫ8>in->/_kkSWКo Epm#!Q;kgnk^+̸Ɨ/. /3GZMb4` ½(R!۾WW4SXVTLc4Ԍf*sv=2o6F~x>-I::W8.-.뷚mF&FYHֽ )a1 57OtiŸ\x\G0JNy\$V|K#,{Ib@ syhiaƍdIT2wZnFkFb銹j4Z^u|M&TJZz"9#eUB+1%;kG7R_2 O/ћ5fO,-ke_t}^/8" _ºF7S"=+uxMڜ(:ٙFkMuh5 ) 7vAЏCЊp3\# '4foOݓ[fi$;f&C)\o#יո'& ;Cb%f<Z>J[}{3'|T1iڐ{1CsdoZ_*KR9- wȥJ#6G&Q)h'*S+>+|/Ŷowd=j5 zWd_J{&$sޙ@$#A>a ۿ  7$1k:b2hY_oҾS3b]uѯ_e嬣Am%X| K; VC}F\JtYubH^߸*& ҩ'@V?^jU'?θtOcI֑.%|Y{f/RQ85{PLhL0`qBJy{GcLmEka27FpHޢ*޲AUATP~X2P֤ZؙuXPxwYۧ ;A]piJGݜ_KK/$£暟{.nm" P*ݯ*g95+B痓>G;ESֵI!ڀzVqf SC{S_3+ւ8s0Dn5FK0b `u_R{skL2zk-JcQA d*#+XѺyz ׊xz[20ְ!L V=ϥc7ך1ڃpij]΃d@{ָkW8dt]{|4p8Û\>b]2J3KG=[R.f ssM$\0a*wti=J]-{WM!NOKݳiߕxf#A#qWxFZkuv|[IGsk'O|;Iז65x4FTQY80=9"vYTOZQon YGzcOR%ju6e(Wz=ͨ{HNop+sK iaݔ3^W=Rn8(Ljp𙤅O`H͖_发*~SV}}xAݒknzU zku_{Ug$]2ӬVUSܭ_xC-2 oTsB4}ova~Trnv|mzŦHDy?}5&8|7ͷs1k'EK~gPSĩ]ڻ$PAFGPk͵X[nU+<%i ТfGܽs޸ka\cUSroL\?aXiwcvsUhuPpHG=Բ~9t`fXJ/ ,ӊh=z uUmQ޼JC$qjwf_iRK>3҄k2gWbDp]!&lkɨmlZS#rW=0SIH Vb (@a\Fi-s wOjTnJmY=} b3ϵ|o0q<>&vsZhKWFM^7vK̿ `=y5{>A?vx]TsEqi:K P 0 qT&b`2 ~O_y~'ӟْ2RIrH|I]qt;;1 8~?J;L9&";6/2tag3Fխ&yf{I~["_EO <>K41ݨJ:s-+[]Wgjk㧋%}~}~*Jj.Zvzڅ9Uҹ!ɾr*{E==u%cې#C y#T#ϋԡ*UӝfX8pWnCcc"uXuH@˵j][˦q+ewc)2>^9 KNrZ̓!FX+>_:Wq[˵a~+(OuSv(dYX`үY MO_%6 \*G6dkGzن W=Cp=kFy6ܟSΨqEj昭1}&_uYBKZp/ ʪEoti4lϩNcQҽ6Kh!*#{(+ԾC:^Q+a{lSשqy!0H,cq7u#?jgɷ>QdwY;Ey2J4U̯Oݍz-%#QsyU}XO*zѷ닃$:s4J<_kR(=_^ mo7,@uGNj/@ VrG|/pw)m'aӅ..N#(z^^<$³vSI#R|,7dJ4e(F;CwNVr0 a^*3Zt$ktd W!]BPJ YjM ^hB2mzh<ݣqZ[ ! !Z:f]ksKe\̗'<LsC;sjkrAAo#[eVn9[p[H`\<i$ I#ՠ2]Z~W(THyWtfke*z)9^ CԵmfp[p;_ , :Եwp+>+\usD88ٞkbqA\F+Yeu ̪8Մ127+jo#c;$,8q]"bRwV3H [b3:UGԧ›ig*de;2D ƾ/5Vu’NG~lz^CE;c{vG5m棦jcqҼG^]coاzXHJRFTuk* nY&hO¨XMp#⽏e$d>ujHݾR_S_¦;qxxxj Uegx76rH9qRkcbW{/=͚1VbIZ<;3gͺ#DV7߾ބ<.WgJTեz\e[W1K_uGRN=Mni:,גyDjaQ\R'94[H h9QU<q-'q]ы =V^v6͛JPB/ڻr\(4*iLoqSӗ{-6:ڙM4]"9}z=mIn#ƾƿ.+>:>ii+Տ2#nv־#𓫇*a޿*Mǹͦii5]?|~ p6~ y1Փi0_ 'z>jFZV_34׽Uu -EzV?( \Ya\{UWhY*۽oR\nTEuyZM;hq 0>Yj yO(Pje>WC4zywΜjom"t #i<㺷b*>@*[\E(u"_,0q"U}Vf(uX(0%cR~N Gn>xi&y1iJrm<~{צd ,y%9մ?Nl:qʸiO"|1s =d Y9/,0zY),K/WZsQV;yDk&|rӝjoVYF#jGhq(\|=5Q:PU{HFtQYAڙC}I(}zQohӖZo;kc+ R_G!u 7d5zx?9خvڳnؽ>ZpVMάZ_ ;SժMZ֤fi͟42s) ZLw5eɩǥڔ f=qR&,ӷܹEG֖D1֫G5:x;~f`z( bKx*0[(%ԕamo[ )G|vIZ!p/^U_:JN4u>'\ޞ֖u*-XĩVЌJOٯK]Im$jz۔!$S >iAh(a^|Z)wڸ!)Mhϩ8I|᧊AqJs.y A93ԞSnw'?Zb#~RI֛UOW_-JPEsWEY"A?u[K'\>#$ת߸zב|Ŋ7n9r^3Եan5-Hxs㠮WeWVk0-W  {[mSu7ؼzNain duǵdK^W^S>txUHO)=[oiZZ1Wk(9k.ӕ/5Xm-.BEq[DEVXIDUlWS9.?'P4ؒb" 5W|e+A᛹'NuoO %Up}?;?>>ORT1cD>5kQcЅn֪[]iׯoqC4mV ݾձ@'}MIlc=hlQKaYS}T y P湡NWԫ-oLK6LpX7du#%^ʓ>Tg˭SJ3 5+I k4\fxVͬ9g =JWL)ja`*'J!77V+aF0Hvf{O0!֫$nm,D7C%41U- Ai䗍}mXs\%1AP5EƏo|exI+_ʹ5񚤑RVLsWO op+٢>`Ƈ,*DTD٣̥Gwo"Qֺ{m>@U2֍ц7GZ+gkZюgT&V/Q#؏*.e6>4mݎ3qU <# Ez ѯ/|ā@p?csk7Ov ʾymc#~?y/ :tlI>.JtSS~g|(5=NXLְWhZm+GwMYr#&G5 eU#jZ}՟r> 6u+;p6~Zg|Oq[ӆ6n}H;E_ZP_Hc3So ݻ[θZጧAkGhmJr WW݆zCA[\m=E:#5tzS n!K IVm2an֖lPKzk Fq$2kX,^uNT9fQoknZGZZ5rHVK! }I}hh&[Y+u na~X}O} Q\Wm>(a/|g{O+ 7EƐ:} kOid2F`;W[{ןę/)aiPݭz?Mi(F{6K[b0A˭F8_0b{5ܾ"-lfEQ\qT?w3 mgϡMJGj+M;7a|qLjō !H.`W\SJVLfmPzԑwQmfbf.iv+iPW6 yMU{ͷAHqҶt^kskgVkaJ擲a);#ЄKpN9^l\i_F&ueV0?ohx4֞<-*4bjvItpr)װx$m7s⼫>[2NkүY{j-89*+2 4Yդڣ%5xuMlSb(6WJ/x^LXlkz5hȌZI,|v*9!'kF nc^6)Ť>-l6^ئI;Z |>4n)\M 3%bvy 5zuxJ.- ʺfF5w;t Rfݻo&!5{Ddk~i<+1i6LLWЪoV8+? :g?~rӤve,qb]бlٛW( oi\_\$N\Ԗۂ}Mf5Qz >;mY'޾|5E./bWY`Bf}0a +G%8_Heݝ߼QՍCmI~+֤r[ɎfOzuyRk1M܌8r2zlq͌UVoCzV% 溠?ul=+BfB9 M}|rCۦYyI_o1ԼeIQXdW@0p}~wu:?GS\Ҥs\w;f~뎇ڝFJx(Oo-a=j/?5KW;f;RM%>R sm^Z~b|Cmu q?mBȸ?5њkZŸ}o7*Et2k+s_^¬x`1kFS|r+*բGBmԐjf)S9SOm=c]E(YXne7zIIeOҸI||&_rrxa־<.NG)k>H0ec"uԏd-äλ}+GCܫY=ծX{RӮ;eه:Da$2qWOcyi[)G+y6Tr{a59[7yqnZ~cawSP_xkP4iqE̞\ȽTIsw(T\ԥc^vnKd珮]Lޚ2и!zn4k=;ZZYXYc  ߬NM9GC[EXR:Kve[r槥O5PҤPfhs$^Gc]^46:ܰ|q:A'[hia5!]\pr+V~ ɯm݄18 *dmV\n,yI`'M>)R)O>ֵM/IV!dsZkN+jV q2 CA=u_VNq8;Jq oWfox^Vy6- 3ȑPjUgJymm7v~[kRf/aq۬Z8!ne#`޸I>(Ah,MӖA%Lis/I +օ据dfB71l'#דY:b\Hb┦W_M5X~5kF{}sÒ/yYh1@V\]6q.YJ\|x_ZկnYn v60.fE0q'pb7ֿn|R-e)5W3qL~aƾ[8Tg)S_{%F!TQW/ù i@D7p5JD8A5m x_ %u%ò|(|μ^o]?Ai/3JCt|.Zj^ֽiN7rH6o?Kysp~k3+rlɳR"?fUV?T)>٘MJlE霚Ε')XƖv(I#.9jY&]֝=KY),=wtS^c.Sث:xzwRQqwv7m<}?ΖMK;E'Ns%෈XWV~ޭ<ʽ^no!GVD#sԒ:;zm)լQCS[*܂8c2LTzX˫ceV.g"r6.Cb^Oem1/@(UHbT`()$QmC$HpI:Y|nǕRnZR|aݤnWDLq#w%ROS]9VW؟ w[EA pٳjQt}JF^LdsϰnXq]iIMO)nH9Ag `t[v߭zWyaR5x9*7OT;׬1pF)b35/| owһXg"LR.'~CS^IӔR՟R+GU.\}8o,vTz PW\5jI֕+JRe#G.w:L 2O]Io,D0,>o]:3*cQ8|WX}c4N:ifSj⽇χ^{̱oZ+<"Yʫ}zꏙaBm=N .6Zй́\G~5dcj s%B9 gݠ޲KM5JPd;y5Cb3Tv>Y#zpk"*I]DNaœ6G5i4U Yٌ²-To,|Y0.W`5XYwB@bCqJYj[ǵ=*  V5njWX}*Mu7r.KWs$e%y5$ARݤ'h^|JL08LiFCNN;@VsFo0SueSk5dhtўOYnI99W$d6+tC2y8Q[R4j[Y婕.yj% .~ *:y!H4]BVSWwlD:>RQ?ݖ08nֽVuKLZ9 Q_'gX̺|ët|ͯ&籅ܩk>pׁuhKHۺz%o}:I j()}nzK -u}7Q4r=0 |+kT|:^Kزw )FvkȷZjtwOU=5ó^hX#,L)q+5 YB687?CV+^OfTN7z"֗?Z{L r1ڻ扣?/ZˆZTަM|ZF(wN5kIz=rيaO׿JdzTĨHjZl۠խ H jaڼN s-|pJ}nsYd^IIkbJbxz^ז˶UXֹh089kъ"!;t=`\CLznRl8:vd,.,|1kzdpEܹE-*׭qg뗋F$ƦPwm Bcִmk[ym$ #YZ9nfҌ%`c+Fǽt.][zUkmB}I6>UvNC#n98ϥ|=qG7V g}=UԓJwVz-;+kzxS}J+YM[daY^$+mB5mhd 2Ē9#8=+YϜFJx֭@ڔ寧oN] Xxҟ:KҩRWU)w̃n|Ɔȥxc*OA.%ĪMQ 9V%S(e\lJ3sҴ#N'2u|T vu1ǀ3OB?^y֭J=ȥV!U '28< u" A%8Y¼hdnl VtXÓ ovrǯ?j+i2w>” KG#)طg[CƛqnE$B㑷+{zĖ]RQ%EXYY XAϷ\s,) %i\nU9ֆjMAeQ\;r=+$/8{`N~| >^ZSxq^E}Jev0oRwH8'+w ڥ՚ZI1+G@py+dxI{hY'n>DN/͝dW?ưo$i&։0 P9fiv̔|~5i˱R.C6yxsg9AY6ĥW.xP=k:^oا۵)6*޼RY;Hѩ}CcRxX渘ݽjWLh؜xKq3Yi&<( 27J0w[\?,wq!- \]F1>xDf?tƕdMT5تqTv61iKwcQ * ~TuhǏ9b3,W/Sjl.TQ;J)aGjyjy_F!׭vե>e]{ GVxW~ؤڟay$j>FAY(G_|Pkbl3;Won#[kK,jW8^h[L+9sSJֵ+t tn⏇̈U m|3H [{yl<*i/ѥ1ȣ| W\jZ_z;W.^xJGh_'ԭG8껺 M,$0; {WGYJbWSu4%_9GwRGU!(=N95T:Iڥq4!X&~]ǵ*t֊'~ <|{ydMOyir6)$_xjQ,vJ>C7> JsQ;T}T{_oGTOr̹&kj6(1^%Q\mt_TTsm>gfn’+H+g4up_9j55lc_'8V5yvGwӌoQwBK"i_2?5u5Si|_1GΥX t^ʢC$ ($:Y.QwMi4)n2qQI4^wx<֖s(?k}nR++/ ;}(m$n&k䟈OZ0s_uOΫn*gѱ]3Wу[eZ^=kN-"GMiHiFkHs sƱU; E9 +Z LpsڋH|浓R {m?}C]ڒʄ|.zovd9)ƣ^ñjME*ڊd*Ob3#E''L>Өsx \^3 kxB%,yW\MEL(VzDS}2cX+~1Ta` VŴgh5ƖHkTEbu1k|D(ĝxMJZ@&f©et!%z;b`Eo:M%;#%>e}!~a:ۍ69mx˵hỒ8+:udE Vzm/Zk^ʟp4Wf<}^ |RդմF[ ab>~49&m2d\ т{k٘Kq ߻|EU0u^jTϯC`iJG6B=cQMB)ON~k6 Ƀ Ӹu+pZ;F{W\G׶V'4RI17[W[1Yo un3B[GhRTf?cpџ"rXJۣկ,'_ǽk_L-煼c]brWrZh?yTu+|)ozqfER?m8o;>͜0h+STsƅj~'̼Jw Q fJHNӴ16[yoO5j .r`cȊ_?~Cj?5|5rou%a#=W֟:֌ocCW x851tzj!6ܞU[].Y7h-p3>,6B=cU?*X"1i:>g_ȱHӑli#)>o3Rj6vIX n*DFHR$\&>W`I;;} ]O:j{3КohKrj#Y@fD_hReb3:Gˆ5#$7~%``$RVWnel.mmf"dS*ݮnonCOOQ]eַPCw9+l3*Bpsd7:~$esϥ\++JR=RjM%Ɵ,0*vbxjL#I-W AlH##5sR[V Wr=}֣tUAm+A[BI%mQ3{U&A_kԼKҴ]$M4>h#?y: /WȞc(ʲh隴 qnMȤG,1TgmM5+p\EҮsSJo35ZkQ[Oip\F c9ixU#[Y)N=u]^GV p~ڡX{g1k8yZL!t=K0U+xVqۯQ|-1v,͟Qr#]*敢jTg3%4m#3FE c4W-\4{5mv8{Km:4TQ'WA0`:_[xGWu>/'QQ ҿ86#Bb9cw=W._lq)bkCNPG־|xΉg:m9:\(F=SIԚ- Ƿ-֥eSsQlg SRT$xujʤPNTMl\!^Nƍ{% l`x1e^8M3]L=JN-d`˜dVneGz=goʨ*sWgN8F#m_/~0[i@!Vp:⾖i`R/|=Vt.{ǐ}jִėq uWXTrnkTg.-ŚW8SsW7Jմ{1rv8YHpN*g1֩I#(Mn6EFG"kUWaZwx>0 ҶmQ+soСd(WB.\lkUm9܊ i0iQ@m3,1dqmPWɣ+8$quQJe"#OFR MQ5)]So9b@sq]߻^z{4c,o5(ti_}irNҺmz|A(rg;)/j뢒5yQ쵒aB롼FY Zʖ=ĜWVC4[)pd`r=5?V)A2[ȡ[w{|Kj6`p`py[ Zחc 7-ﱱ°|U3j1c&)ׇam$m]a09} ӌt굳F0~ sVw%:VuX1^c?x7RMeGuL3ZgV3umWZAa]BHԜ|ҨC$Q gk뵁:~J=-ޝץ^T GO֖WWm/cqd#?}[?¡]>{e$ VzxE"q{!-cqWi)6&мn{fQ\3δdUmbr ji,#[–@ ƈ #g1gW÷7<2c Nx0x>U-)N㆗*Ļ^Huel8 39ݏ~9qeVG T8"ϨGfqC.mә.x9=YCj&gw++Ӓ}+<Ҵ-_# ;h^kXKk{_)!''S{cӄ67w MrFn~b2q_z%,NՎT#q4HUYdsW\(Y]? =ZEUOzt^C|?3LJ0>{f%כe|dӧoʒflI ,{}*NE䷌-N$XW\)8ڨ/9*ws~oHۺddY$SXԃM+^%ZdkCqrP}ާb^}Kq6i<2c9G\'/& O9`:p@mccp&QEޫڰ[z?بװ{&@V% ^>anj3] 6cSz5kJDn-XbV`Aj±Ʊ40車 ]VVJ߉{ޯm㈮ZkՑDG,]]H.+}> KSS)d=kKB @I4% *c3jhM"}'{O˜zWqFJZIyr=jx7m֟l&_ܩxTZz(7AkrZB4X+rC;jΛ>#zEF>l'~GoG?H |?(9}Oʸ#=YZ&3o < AVl@fskS\ZOrx>\ǁBU;&㙫Ŀ ٙqqD]i(Sm9`v-5G!Mr^, Ѷ':~5o?ho-E9].eao¹ >!_H\I/%R1\x^;;'Hqkω񵿋.[kc&,&dj޾!&KҫZXԞzOOsp.*ch_/q渜jZ{6>&W~UWP,JZV/yQxWN}28;W--/~UK9rǹ4=zo1"Mݣps^U- j|o//#xj<-uj\LO~Z2yy**lV85 B7x=­:Z6mO,_325<_\jK><~$!rWsD ̃橞I +J;K8ZYdm^ "eX܍ʣ*x.XΪXj~sȖFfO5n\3n7|tf#0/\MoNTQrqY8٣ǥz晡][+)XH=ʷ-7LI}ׅG._eGeݟ-Ikx}y&|,jXB[4ް3zˆ~/5>?#O_~&.~iϦ¼qF#zTRm ߙE/'񝞣ͦ9*.0\ B[$[gMjG(gkfjL>9G-=$-E<%vwl#۸dQkQz ۊ.s (Ѵ 獽qV-c#)m=n:+STfUњmݖ3GW.Q<D<}0s;Ԓ-Ž4VPӋLd?,&F\}w˽B5It6lpFUi878t\>mjSOK^%T/ĝ(hr?<lffSqӚY$(=o=}R}:qID/ I36Bw~zaUz~VNFUtQ7aVl8"2+"*)Iߙ $#>ƣb_\Ơ= F#lXHu\Lm3"v3lE{o+b$JɅgy+xI7 {KA&H9|p9RO ./cmf7r?,T"r7!«- # B$rKrGSҋ%%iG.<ⲔyrӰ/,zYY#<{MM4F27 'nOnV9!;Pa tqE]b];id$^|~(茯} oI%H |ǁ;zU6kiѣ9K;9V4sA9ٛpc3eqF.in@.լ$gLlV-4z-"I4D8{׼|'Y;;Qtr*LpyϵxCY[,+*yGr@8_A1VzbOd*dsHsdWNSiϾGsVi5۔"ItUl5iO[ZpoI1Ÿǽ~+UGn^iSV6M3crxQ5w's o&iajͳ XcQTU/-3Eq|ϚVlmk±~ͩLmmOw>qY+WtfgMɜ>Z!86}3Yzt]DܚýԚG1ZzLϝIL:,mPۖtaMݙ K#q}uCnOZ=ķk1whRG3|ciun>9x8}>SUG˯ygxſMYmJsw?zxƸ{rYEXLW~Sv#' G WL`>3gy_q7rKwpix?P\Hj+)&O(Gi|Qg^Mp|Sg,rD3Un E5;[Ċ?bHkųOCx&rmN3EzYۺZ{I4>Un(uu$G>fTnne +i~f1 ƧJ 8iTI%~2?U4# 5W8I/L1۲|mOXq4*_@@JXN5)"IAN umnJV oS_xLiI1 {{WKk#DYc##ta|2{f xRYt[?~{pIנkKISC&bFÚQ?5?ExBcQmti|?+JM>)OM|؊ 5T}D1M$!2ު aiZۃ^6.8aTBs"kS&ݞx鏥 c&JO-YR]G'k8cӥR٘2TZZ?{яEg^rQD7R#I(I;Cc eS(kKU>\$+yyq5qc=N`N/{mzTk_#}.|oJmmlTJZ.cԮЩqXXS7IfG*sӆ~_Mnwb W+OV:E֪ͦS vٴw$t& Z| U|y}nN֬ƹUsq;c>eeؘ_haS\ߗy|C]p3Hû;uX axc8^\Ilue2|sZ/4G=RA}+SN*NsF6p[x۴ϑB!gk-}au d-J!\#`9=;iv4)_ 9%kMV+y|o[Ss"(T6oauqbc:v{zsJq*^mk{X㺵eL`o+#9gtڛԝ>*ǰAh,wuM6*4Ȳdq׽a-~d.kvHքRZt{[Cldw]#ɻ4d\ʀ`AUZIVQ^5LOYMiCBcm\m߸^5ޗ[æ%$1@|ՀaX`CU$+)&89c~u2w)zc략nE \I&2!\HC5̳\jQ3-'y Gjv]||r~~zVt7I}#5‚cfhgr#5CV! k[ J\":< X!m _r[?9cD579`GHn#ZId#p!`vw(oB\ $_ZjS}U|Koc9ok;0 nckJ ToLHs93;w_:)EkjKt!R`Ut\~bc#Xs0 ˵[Ke]Ff(\nRzV-^HNYv)C$ 'iIGd]{ tG312gCO}]VzpuKv啻sk.iE]̮;чl{dbwt~q#s\qmH}^1z~2QZÔ8;UBŶ{ێ~i8zέ/|HZXY~ *z8AֲԷ6N\\)g# ~`SR=P~g_\m}!>VK.cR Vfsߊb'!Aquǚ0<7Gp禝*ޗ"]FXO[]R3+#*'˱R7pGϩd*%:9A}O.o&f>a cOҼj:džY4}zK[kG ?{pڥA ?Oۮϫ[`>&)PMܺTKGAn({7nuijG> ռ)j\ s_+KM0SH+ngQ| ,'ȯr0Ufxssz9w;i&D\׳(OxOZ;7vx/'zqH#/+MwZ]ݘ6 VRwzlqXU@;kw$[e`^zb[Ƞ5 5/I-RWmJ|By6ow+4."q^7ML>;bQS3Ζa:*vgb|I$'c-53 |px\C\8l ~ 3ɭW$,0Em&X+.ZiXc嘚nM}-*19I1M&MnEV;heȮh`\.7VMc+Dk[vk&Oh > Ocd4ۀO4ׁ^|f k=j싁=a%'5_O} %sEYwgxI"^d;-nt mloq:%tvs/_ -?|.#[^i?cVQ/3>4O{@Jp5h7\-H?F?┷Zki?KVʼ|V3R?z8l ^u\,o-N2So(m5Mfs4F?ۼ jXf`q%^JV$wRP~Y|GOfgJo/J6)]"cLmf^6C/0)hss\j/OX}LʈW#H u+ .[<2OO7XbZ$U'/}o!ڴ>Ѣ9q]KYI4UKVsxzB~Z $_ⵊy{X8Ќo:OE|Ź4wVZ4 *!rc?xO3{%-<Q+rLXsWѶz>DMq\7܈6=+Oh:Z]?tQDX >k!_ݓ¾|KHv˭xRpiEgMvTF˖T#DM:WdeXDTNJu5aեH`X1$>dFW`6@ǽ~7W|ϒ崛ƏK wbA'j $Һ}?\W 0ܣ!Qߊγ->7eX.Pmڭ݁8rVA v[%?29Ak̟7=;;_{2ե3_u(>i%dU}V]5 !uq\֫[ H;Ÿq3BXyFr' `usYÑ,B墩,w\zq^%ezu]t|ĖIYҙDU"h6H# ࣰ,ӯ5%Y*ΫT FxL?κcf9cA~Qjż"x2/pq}?Dv 7>]1?!Vf]"slslu^E_2|lrb>9^sZr]M$hμ%{@*W-aW,_&#Koz-y3#|-a8ۃk^Mg1dm$>p=jCw,d! ߟj2FYβm3X1!Ǡj%+!}h ʹ)%1卥 0~V'z[ž;/F311ЏƢ!׿ktB1D 8JS駟J^M m-ͷnYw+< "g 895n.oPO9Lzt#z Wi >xե_֊,[[L$ cr85­" _ͭw͌L>Tgߚk%獅]<+` 9'ºY/#7O'++J\Go߱8ZIÕJ~}0hJct*hNkh89ݜcTq9|=+~i(ZZWF}QQN@?_5HsϘRCys+T:ܿZ:nަ[xߩk@C7e^٩̶x;(=r+Ow8If4عZuč7H7k-x_6Xkq۹B[~H埴-=ti`#xtL;$`U?SEƍj:XGIIv*x;:Mz?{ Ԗm^\ KLkʼ]'R^-$S[c^}{js,ѡh|U/tGmp0Ezٯx[E}<ٜ=<~Kj q 9C~fʎu @j (VZxۤ@HːN:_TFކu/*_,9}bR<{ڳgil]_Cz 9[qQTàz4AΑ B}OϥU|I)M_VgOkmG9|(𧀍^Ew#Fe4W_zfShT^U%V?zmE/T}ëb6&+Th+z6el(RE3&~ZZr6ϯϻA 9YpՃMhǚָH-#y'yV =GSgVMT <µ0CǴ5Ifj~!xN6f&i#se&bPǭw`"ғnc ]N+Y/\ޡ{.7 %/|/&5V{-6FXW#hr4_Oz{pVºa JF8e^rKt_θ1Xj žfeBڬ%{Wjxڮ'י*Z*,b8[z&dSgGr$e#~R[ RZnَ5S_~$:RVhVLӬa@*;thH^g)9;m 于=kOkΪ޽SV7.@>?N.d8ZøAI󭃽=cLWY}ȧkCAgW"+ef~W]LmPs$mN.XXK6daXhEw=1Ԕ\k+e3O[ʊ95x-*m=h59\#|=bb/.wgf8$֡%EbTz $o_GN.kg5ܭ z=p0]^ij0DfeIG*AnаăcЊۚ .=T}῏(mB+[./?ɝ}UU85x`[8^~ 49QFIp S'~:'K0Um_qLJ7Q?sv<`# VksjRr[;.MSI6%bK'@x{ӥ>ZUyV_Spua5y{;[--Oo4'n(`_:a#Vc&bKI#>qRsBr./]g/yͤAtVEf#2T-MuH.nexnmj aU_?C ~^Z(Ǣ_)W *հ*g*u؃)p:hHzcOm/`ėѯ?YMsrV6UcY{JOHS{+ni|W1q$ss\&^ ~DD$ʺ ۈ,I[ˎEbO!%aV?1_BK޷7òMY+o9l_q4[$ظ8Hg'H=6K{L+#[I=B+2`32$֝,U:Z'3֫ }RF0iJX GKi_ {kyee ''L-Λ>׶ɨ,OMBN"U8S{;)dp\u*`*wnU#è⫄x%t,/OA9X:_hZ$7ī#E!d)'8+s?Ks-7s}R0`7S:քs1Ӓmv@`g(؂x[J"{u$Yj30=:74](72oZ[p]iI2VU=1`Uk[ Z;uhw}}*0Vʷ9a>hzAi>IYsA]55 UjߗSZ[o=jOZzDE[0_h0M4!#g@ocMZk(+/=;s]l+*yj)ps8Ku}c?E{>URiףCVVWU_B_m,7Wd魀OjЇO m5 n\nO5(mE$'ID"#kz=P,s}:q V˻ɯ.Zy[;A](;d.i]9TR 7p#2HѾnP 4[%%a^$;X﩮?w΋Dqu?rW_tbM+TcVIK,F^_ jk<=48IfskWRcsꖺZy?ys>ASki$^?5E U E5:/iN&oZꚀXӃ=눻fkz +hW`٫w]efcõGou=␩5I_NkEYή?_k=hid`f\NmEjԖ!`%iXדҹV6: xU.\o}GwK޵GN:WzVA]n}Zh;d1|ka#U[on0Eū']gzl~.iRokwǕ$Rv skt, {%h\ >>VE9 FAOLcMZɆk߂QgL{è.f̙=כizx%W8wwN;y:.P)R\X`>x*Nb՝91x'#w=ZZI\l65ڍy5O,kfTkk"޽Omf!f7޾^t-[^K{ WpXr0؟9mncv7'Ts|4)Tlz؜k/Q_n|4JH ^.jf&E+ƾ4ivVzo?ð=kQh˕hxWfڬ K}k˗9&fh -ؖGXI% 5i$Fy+ xX?J産c)TFNIu`[{F߸r -$>"IO8WG|7%Zڥ0ês[L6a} |iJusJ7~ҷ0lY#kŲ8y׈=YĒd `i9.ⲺwHlݕB bݔv>?V_{Z;yt %xXp^lڮ%+W§PR#_rz_3݄/>8N/bhcv@CqgBXT`B17}Ď8dbzap+A|-ʙXcAӍ8iRH x~7{V.VLfo"#qkDx>ulƕ[8*.;G|O]iƜ}M?\az5Oq)/ q&lў~P9FI"Xf$)ګd1&]Z gZ+˲_X|0Y[[o.mh䳙 rBWu=AA,ϾC#iZ%ުDm;+ ._~oX1<mu>己{I<ȭaCN:Ϻex#xݼKOn1?Nޕ>vB[\]a?8[i8|CF\w4Ffk6T.`|>VĩR|yXԪ%eIMdfL wO^-i:6he~l ǜz0$hyEcڷV=R}r9'cOzci=猥.C+Ol5 Ld ݱD9.l%vٝW(ZXq}nFS;zzF޻j6E`H"!>\v<8+|W˲J?4Ykd[YU7_/Ќ/8뜌Uf:u2[\Bwl/#]ZI]Zny|mlk{AkHYO!86sk5ݶm٦Ι.5ֳ5p1HhϡLmxAıU`Gӟ~ki'[Vxe r0\Wַf\ϔ^TQG̺[޺+1:zأ\24]Z°mn{AI3'4ֳSl}gHQ[tsrG^R+q~Zkȯԅ/&0yrTViaW&[R2\1COW#Q_j:KՇup .B0py ݰsQHFT4 `#1#$Fb=ZMjllXl wy'X/^HmY4EO&8T([$g^^ӧWק2!U[l]+'AcGd,B1.T?̿XRy?+&oiR^MqgGZ9mi7hiZ޻"Cg"+\^hq߯z  zNp,0:As^;KxK|a9sp1l[[hL*0f_~[yWzJڧ>/3ݡBcntє2b݈^XA\UsH6kn3;V۹RN\N[3|xGO[Rc 4CdBF+eXdFkʔ9o֪g4op*ém̉k٭ -"2\J>'zr7wAtJmܜZa2ڞ΄ozP\Ӳ7o%+5܊T^}}kkº6G@^|I𷊾gLtotkgŐZͣGoYz2rr~+W|+z-uqe YO6߈;-H}:$x%zՙUT{FWq])(ĂF~I{ YRLgb@v!H;ưXځ- ,[BJ\s, g24(SB/k*˕L* 6SϭQ[jcȏiF?WwoeA5P8wCJiIk5=UUl9]\@d,⡏O]aDQϥlZǍ[jrZufS w^%)]ɘjYQTvfO(ƈX爛]LEδP_Q˩"p:(z~x:_t3JdEWlfEpOKKRA/I75Cw>tAƽy&9ˉ{;XP3LwV+ևO*9sw]G!i7&~GVѵ]R6nFrO_vYH&F,Zqx,B+U2E04#MTk֡,iX]?ÿYb\Y?Rf']N[.`2{dYeyT9##Oioh+|=<0]AGx$Ҿ|ƒڼj#֡|O&M5<#SY3ciTt=&;W S_OxL6Z4Wy~^ Ӓh{3u&GZm.cPOZ7¦g'YJvW֮m"L_5imT8cU]ָ)>qL,Mkxw EfknǁƤ}zu2iXBY+/~;xFy?k.#GlW[[F+8|IDL!@ L31s^uفCT2qԒ=Vm Ee`̀GJ5/ 7EkM8ޓ=Gyb@o? K=ƥ)>eێ#܏z\Y.Ub&rz{ā>8^wխVxZ/Z;u}~ NUoϸTF}),t_0' I{ל-zLrJWBU-__=ڿORmWںrý7B[cvPԁ$my$0vYc@[ H˳d(@Q Iŭ"9#?MY9z$_oM*$l8ԎmED?٤ "! sk[S٭`3n= u i RX%v!{T2ѫ_zN}zi-e_`81אqUm{-_[G{ڳiKo^{)ʚ{'uLrя?ʜdmBѣ|r<َW<~9DpRoM7z$viy;mD$X:Hn'{] o_iikj̒4/%y,՚(ck|}9TpX`O=0OTۤ̿q[*G [FI hH qdcaLd4L_P7Z}ϧˆ0־mxsGD14(]1I93guM Ei_fIe]-!ں7Ct-"T(U@+ᎃKnJTV(~W󨏜opiot[e-O|<.5k&JodW*_ G\fZR4yV*]K+F{l0Y1Zge[JTzXךT[<w]5[10}Xi}vOe-Vy-d1W*} 喿u#ŭQ>컭/ao _ŘͰS5Z9",#$dcu>="]vA7zjp=۽y054w HYOpAkrKR ww/[kvgc*pF_s*O.In>q^9/Vᶳt$מ0xޫh6q CFaT9h/$xқ{Y_~׬j=#!1}s\_#zvvIr i@zW}2N~Jkq>z ޱ4I]j Il$~#^aZJe 8y5&rn/攜z*JrCRb){PSKRs^걘() XFqWf\|qUfN3ɯ7,?g)hzÑ9洧!X}J5jWY!⹉$fS[4+gAZ娚i2c-j|ڳZ*B>xW^J|4j'#moyp~XX52'־ao}ꯋ< o}h¾; g+lf|lѽX]S]i7^%*x'mZ]ΡEjXdn>*A#߇Qe^Mֶ4M0X鱦0ؤ׵Ӵ㯥xe)I,wst#T ㄹ2OA\ޟk>}]=[)]}k󷂣Ou C>xS^u&*AھR@ك|y߆|v3_OAokb#3V1}Q^2J_sfxSva4if;z__E_.RMzǒŏJ>jaѳ̱ҧGj^8 ViXـ8E>dܰ&qei&$cS$bp3^sndzhՙ&$ ǧ'Y? -Wq`?ѧv[ /+k|8cc緹5:v)t;sDr$FXj^1;]Wr]z1qQ^WEd]qVf;o.OsP7rm$(W,Y&IpvMyqwzmIAI=14mNs¸+ԼOBN&C*p09=+&OEofmJe!?kFEٯVxr>k}IY ʖ&arp8W ̔%s_pq-6>fbx5t3cQ).G}Ju5]l=ZT}/EhxUʉ>^W`y JjO#E!)Ajs2cvv}F q[nд&‚E89E~[F쭻񡇨%ǚ?1Ÿ-"-g io&v19NƽY";Mmal(5֟ 6ڴ7QikHF y31r$vVB[TVG8ǭH Ekq K4/6V?:ϕӴchY T3 =CR/y:itY1o}!;nME휁[8}EtǙi]:!9D>x죡=Z+Șodhd(˹O\לŚw,XRź5 WU]*Qos20NRo7VP~M"YE;}EmZcCpچ=wqZ}oMq֚B0Ly ]uۭ`/nX@+>}KX#^KH@*'7rƦ&%ƛnjw_o&gĖvM"/S㟊&>pH v$|rFZZ{۵8q?tksشyt"w]c _>t~R50,CkGg-7E5ЦKOV#v~lbZ"Ϲh;nۍOdv SIs^zX!׊ nyH+T\!yn"J)i9I! EZQUP^/֪^y泷6r pҡeΨ_f:7y"oJ$ WqMtR!KdסxSK[aqph^x ^HRDtTb-wi>qmQ'>wN[V kƝ6s&m ml5BPm63\ZmΊI_SUђ64xtxًlSkvin7ګm~caqQ]51A^}FUh.Y&hlҫ0;Ewх%yeQMOMmQ ,ұv7FGOjݖTҨ]]rJ3G-Yy #y ͝G"¾D5=WNY JQW{ck-`רx0H+Lͼ+ԧ4du tsTgze-)~ *ʐюk~(NFבȥS4<{|3IlBԬ"LJy5Z"&;a(9?1VK{XwP@ ^}<nXAG߮al{޽#)X zO\º#5M'Ε*3~<ռIz>$Mr(ᄝұ :}9KcrZcQ+w{/a>|XYuo; {)m7 <;l֟$I,{ KieW-XUf N #rqmƾdG;}nYh9}A5~ E߰ ~fZĮJu\yT.J_y^{kO[Šފ:U %9 Ҿ_2mV6n'Wco *~wZ<~V@‹Hbc%V˞1)F<'7m༇sS\]#{GmPGƥ-vBrx>3Ė?b{0)N_יRDG,`min1-2eQ*ݞ*j=O<~h6I[ewH!,#5Gv0Fq@1O6̺;SoSV_6!>}뮶&HS;TԥoHo1zj̖Hyjbn6@ZX RJ&5*{͕mC\eTzQhy0<Kfv5.(jq c6$5thvXSXD0#ڔA.tL@:qO]Z8_Ҏ>o~IݽWAHVN#h,?]ܷ}GWϩdR`)/şZ{C̈z`4UW\;Z¸gb~.y+SGMn%~\/oAvJ۷2(3\??ij@ţXiY?^?JT[3|_Ghyd-yGx_X]C!6y8zgEiV~n6LP~CM}6"%رWd-RhbkTo_-\9]8gy7شO8vGJ_t׵'@~\^R+2Sl\k)B}|5: ^}|Txnq xK{{ᣘ`V5&=L}*淣xk͎YN2-荾9+IZuj5쉈lO=W;ޢ #u^e~h;WsY{+94{{} uRjKj.a:vtϥaOc}3Δ3*Źnv6*znoZ$r5\M]Ya$D5S'Ŝ҂9h@jÖ,_ig8,G>]o̒9Q9lU ƻyֶ?8t=6V- .?JhȅbJ)y)rgO.B6i;nWa-iTjĘ^K#E(a^G}+%v%8|c*Cbe̔g[YoP=;vb#}EƼM9R%W9Iwqi~K7#xa<zűo|Ҽ|XwSCoUE: $|5u}:d!mn:ZP#An^NkI2yϵt)* '\xF6 :S3x7VhO꺇t5UtY~+Nk wKnWdyZ#y1=4|m@nz udL9dw| Em [n|~P_Ҷj['Zf? -[MRi _J4c+0Pd7vZeVVyh6wjM>r$ :86=F$-w{vYn^"Z,Ԯc`Vg;s^aωSkxjY-+%ݒ_]u <[ؐ.eF*S,0YūI~F_7{%inJnDOxZ?1sףxO4.i\C ʷgW\ě"unMs9zg:.Xay[Al fO7"N:% acitD'?3SrTDvZ-4$r4a]ke~g,6ɕ`js0:ӨgsNKBik޴c|)yEiOvӊ/5b5 ; _z=Pz6rJ|l+;[˰ j9k7 "v^[ o/3#_GO/H4{PErWY@ ({WUj֥mֵiJ|Ƶ(էSIisOynFʉח׷~iΠLZ*}~R?x[_~2:]Aʿ'c܌ QN@)~`=ٹJ3“j̄i3v?{ۭk[HAx&gs*?1JOtw_Bʼnmݞ H+l7PGҾj##F}{p 匒kxҒ|3唓;Q5wggxm7W1J!ֱ4MR1lAM[Dyއ5N.hڭϋoIj^Jdw-+ŋ.$3q\?3hӣmÝjj*E `"o ee,Tzt51G8F&uA*`p+*ܓ|״7=4;i%o%z{խgS;qGU[-־{=&#-9Jm&d}Z #8NVhvL?νw76հx)"~j2(Ȭs[ܒpJ5ǨZ4OjA歍k)Tm %s;Z>oG#{ :.%dm?)"VG5D«<0 SĺUݯ ,RGM?q']Vv<#py5k]NbzBQy|)3皞Ż$*{4N[A8_Zo$w rmZG]_ F<7zƑǦFnlm^dw8WDi7+.mY}i8Ws1Uou^HZo¨O?6[^kmkqonxA^c⏉*Mk-Gf)]?!Ԛ+%cRQ]_+֝Y{:unj~7X:{ ԑnz^*Xa]֨!@&'MGV,~4}AUY1c@kc²mZW[s{ gqpW+["^ʜp̉~5OX2N|f=;8i%="3I1J㨡5_1l㲳XSZvUo?Ր=_/EEh%)E7j #$r+Y ܖ7`F!_e1]D[pnTq~T-'i7w-C7B'ץ^^X¢Yo  0|d6T+(RS9ʔHn۴sմ,lzZ6<FW_(j^h{{W{◇n'=̓7‚}aq9KuGO4R4omu ir],Q+6aޑ1ӥ|֙׵|N]?/T&+.-$-~%B_8DksYm:1qQsVUmXvqIDƪ\{rDwzlkcYVJ6,(1ڠפt9Z6œީ=Wus:(mr&y>, a#]-kYZ'"9+ε c.&a>±/`5:-}M&>Kx&ytcm=KN5u˘Id_iW0`d~Ѷ" jv6K%i2q劽H-o9߽мIhSn_ zw>|mB_I_$##^lR}JF SVtt-ctwzה[![=r_+"ۺK7 \z.NK9}+*{dZ00`6p;UCyDXdUr̸}̝5jRgds` k \NvneH 8ܱCC94%d#T-UL3Zѻd䊎M5I\S~jESk|R{dxl͊QuJҥ:P:/+uo_kvZD.>謿V mnJx!P^{/[z#xa.eS]Ot nS_TbR3We 3̒M#+ou#82t+p!eB6YiVth|VjB5ls^++@*AX pqZrjj7XNxsI),1qT-yƧnAWqKX?7ZַuI6qRft9ye׹53\-x8˼ku?t㊹ژQvy.&Z |Lz#d_1RUH$;Hۖ銵HEJ<d+[tcJjhsd῝AYB85V*pkJ9ա* iq5Z[=-#<>\,`zzףh.+Ys5z-dVڝǧGi Zۇxx.Zr tvyVq\+JW!6W?Jɲ*=j[}R&4};&|hs}M#x/uB}F;n$=O'xZ̛ZVV=+\=X~[nsr:tqi'xm@>$. W֓4Ju>Z/RqW? uhMy#Kuu!M8;ڥFNhpQ8וjJg$pæY Xkp QUJw*c'8/>=VĪ,?aŤ o@Y1?^yinfPFs+|e_S}Agn¾O1Uccz}_V}IOznf2PWQ_ .xq5jĭ\S+WS#^ΖƷ:6kk3ܜ`ul*C M+mE9_K_]dFK90-#kE[+ ]f>%b& *EbrkqfJnuI^]^dkxS*ɪ{YC)aYʒG;6T/StAfO4uݛi=%.6c5Z^W|g{Y?U \+`]k\$6lp?b2zgog\1S^jVr(*v5j֗5~75>YBYUz{f[޼^d` Oj{Ha \Mޣq.E\O hr#1qDC^wU~Wʝ=зZS$ |d~9"|wz} y:ɮzr=gj%|SSڼ3OknnI_șgKW$/*ƙ5-=jŖ&;k^.x51t=Rj ZA[ռ]seloG#~}+M+ԟ΅^Q Q9s&fY 9n5g}5\ kb !66(^~ҺGi$Q*Pp{K4׊.Uauhm䝧ޘ8 F:,ܶhiҵ}%o,@h.OT#3kxzE~/cӱ;..GV-|A{k܅$z{V1OzKs{D񾇪"*N1~\s\:**?k;죧^koElo0i:̏ 0=_cVۨʐ1Pѕ2q0jp&tAtWG'Rˁٲh~ŝ&"wSvC O .qYw+7~sb$|VdIV{7HѴo5;I8Y$ ~lt_ A*QӵK=N hWL:H2P+ cR4un}6F$;mjLPz~=V&e9 [FHKw&R[}=we]U#%y枖[/Q^{i33}kuC'%Oo:ak< z S3\ezcȾ\JkY]fΓ ]Ƒ;9lbk1.l:9T#jS w,C],Ӵ}+$.#lNs]M*i){\E|/-+ѯ$V#y?}ZIn2޻ٵ Q[Uڤsz{%KS\C?ŷ  o;V6pZ; bX9ܒj S%CwAդ#8Rn vxVpbg-{=y,=(B̙hM%볆ɫ-w Q*+]4K֋Y(1uCG#v<M6rwuq^o P[v-g_a|aYY1q]ZzE3 zopk|7yvngFUx\7y5b֦mu#<{ ^b$RkQ4?C:.42gv zҲZi莏* m ڛc}z6Fxڃ,Eje7mgAn#J:H^-u)|ـ|T+5'): "&ZUMy6$ՙl1Z5˧̊N85\-]4().iuqڜf\u'US#h-IiV 3*<|kHo5ˉ=6q[mkJŮg>Z5X4$d=tQ;WbCQTk&\M{9 jo 42TG9?ig_v׵{O&Oli;_xf/>i։{R=} ,q +JI[~G~ q>̱w`rIONA.9r=q\.⋭`*nąNYOOtt0i^[6jo73; +(`Y藺y`33unfl]|Y9S?cJɪKΝ*,~V.%jv?-Cl.x;9v2I &VSڣԚL>sr.LkIH==}~;B.Sf |TW|/.KIjCx+VZBfG5ŔI6Ot=k_N׼~Y|]Z6Zxqm>]ѕ1^eoE<2(îky)3[;s׭:\8ocn\2IXz6j76YVmz8+`քB#F t\j{'Mu~}w~Z+{LIhvXwaT">U[z CYIF Gݬ+Z[t?T rsi#]dq%֝&^nEZu) nv]H0@nqڳ~`眂0%Ji #м?n7skش*+m8sD컰9FUyr`?gsNjU=Ʊ93p{W|hrsLQ~@;Sk5̷ѓXe9l~Uu_*>3͎ÊlsڤSɇy4Rî=k/2NF? _J0>]g4Nѻa=;Ry|&RǠ}@|;{yKk@+j".qEy-ufpHEgywAdyj"P<1J {.ZҸ/!id}?[y9ֈ-K𖵩XvZ|D+ڹۋ:WC~r/OjqxzgNIՊNڟjxf9!7G2MU-TnRM^]^\ۉA_c]Yu?Sb*~hLOh6bЊۉqKսm_ޮwRZk#ow naMiRo;Z.4ky#C^ "(fduԨQ2H}GC]HdS\$ E`iDcdcgXӊq+O2+ȵ/TG Ȳ=o!QJzfm&vG *£w͎)*lHU6?:(جZZ< *{13o iVzvĊ9,79I8\Qkjr3FBdrkʅ UgkYyͺ9=Leb OZ]E!|xHAC(f^GYiޓY -#nMy8| :\N*,}MGZu_2\HrB=M{:q |z haMmC⇏W.Nb@y,k?kpF˞G7.mY-(7rX_/~+{Z?H< 3Ǿ 4W1?42)|@}"&t@|?Ǻz6ǚВ%km.6D7j_qR_S:iG.}&w.h:sY\C3}؁s*OR,O3k8ҍ:J%dGJIw=SǍ[wLMO忩LUh\x)"wz l_#ml5*iRyx+Dݥ_k56#oDZ6I'yPۆ߽؊{[Y_*% =E}Ḡ5/aӮ2Z-8jv-ׁNDuOCY)j&ff+k~g#MےW9 с1A✏qQhIizaϊ}oҹ|O\WQ٬rN*.iLR)Jd̑W=KBynWgOj2YARVmOy ܟj 桼cpy>Q~aa]Zhs9sD.2G¯s s&2G5V!$+k4oOp]Ìc`潫@յmKf G!#y+ʯ {Һ̓1 ]H>\nE&;|v&BO:p#6Ka6C]uB6QHG$J4hiL~[w+a%۱c y>PMTd Kq32Z3|'aEvMO3Y79-[Pxxz8pާ]*+Nrku Kܯ5Rm"5*S-Vtt5yi5ά =^k\Bج[;UҚ&z'}"orܴGkW]vE.Xv7s+EKcݼ{x@&;Z[<_SIw{m9ǭ%-@cWݱg59-.;8Gֺ6门la3tXRW%.Lނ E_{V圓kECeAZFn VlvWe_JԼխYـ8&h!Trk)v1OqWKj?nmuET7bk94Գf̒9Z KmoAZNиkF;^ޱ-[~`֛6|0rO#9zU^[ߞ璋=gD~'xj屓ּhuVz:Ji[dP-x|ԗPEyqnz5Qv:ACh*<s^%G)I2qZWˬxZ/Zv⧎)Nx6lm-c9XE"FYHli'$l}tR?v|kм=kc / wkb#OK]%YZz5Ųy&.ES=3_Lk֬M(PF2:טj/2XqJ2_ћAJJ4UӖkS\wi1vN4ou+wt9i~*{6;FUHY:Pr4 !9[O@=zq`u<'I^};uR}P.pu1Ж,Eqnvi зv> {8PޭoLK!3(k?Ww{,.߽NLRs{\8ab(B YT=ۻ wZJe`y;c ~MCyhY5JFQ5kR|i##$TnbN5BN"?@/yí KH1 C:3;Z[(D6W>_~_D2E z`̓I+b['596ERa: #~>_G["3g)D=0Gt55bxRiV#5V?1r;"ӣH =$,HP1[W4[b& T,7!8M[I<{z_-^xЛ …H~i3Mp~Sq´rF=E*VG4xKJ6ڞm"rWxUFij\hۚ]i2*jtDңޤh6|5|{,>sڵdUUH/fgm̚{|jM1SH 9 }j cS1MhSK dVrvUo/_Ϛ_>xq?pNA426NwluK#P:r}**Tq<vytU^ 5~ O c.t`[L~Oc5Irv%t>W|9:RXyB (d>޼W>`s̠u?ī`\G =9T5MϢI2ꠃK>dK U$EKyR?NKlz(9"Y<u3^TܡUXhZ2[@ϯzKde]FwN?Xua4+c̬'ջ$iX5'>[!cwjÒL|(IK*Mq"[p'CzǑ|~}ct>\PVRlȚ?|4~j Vv ZVӪ-p*m 2L=B-.% =yVfk 뎔SEo:SOcækf'^ubsT[`ЎV79u'GIb,6G-Idck%S{UKcqJaS.fNSCQ0kec 9nSM7's^5jCJLێA{TGTn[9OnygWaV>+Nʉ[ZM &[k:RGd%"|c᯵^LtV 鮶uwS“ZwF+؍y]>$&jY<3 05:a!_eֽ7$>i>QZZ6!VbO(L.wb!Z%^jq' b[ɯJnblEAutQŰPXF?ۓҼj0Kɧh\G' j^#]{quGZmÐiM<'=k:|Έ9AωvXaȯQm‰mn&#>Eg^\7h[`Qz#o/;=QQM?̡qBo4k79ԟ~>VINzJiڭI+ (8CHsqmF ̲zoBT}H_3im- !ڼZKC!`p+zt)MZ>=F5M xm>}Ӗ\ pYN3]o >I4yn, nW Z=~-\+[WSTpP$|Ts=I*H7aGl]71b3~=ԏfFգ6-$,1s/Ϋ"!W0x[)İpzVٰbԜD r.Ef;縛@>͖N0Jt־w<@!pU%IxkϵOv:cד#?}*ww0SQD1ힴ9({4~mXvNr(ʶ [* RS.oZ5ғv;[ڳ$w)p) o֥Ǖ]{FBّC 1[-i#H 6G Fneey^gfGy)S|sc6;A} :}sU^I$8A_O]n]u:W0/?|a*-L)gwc-Q7[z>L־+>[}kf'Kq(FLz[FY-txQ~XXzggWAۙYd B@f ңiBp0~-#˹d.?jE줒zWOb/4wsZ0J-咧ܓ]', `~gw13VNOiaڗ28R:S.`*Ql qWD,[|,k)ѻK n]}3YD! ꓱWZjkP%OѮSڠ/f[SXӊ[_1KM 6 bFak1p79=Rf˱ܬNUfVwo7oT{uk;byh&|2+֣)w#/'t])xvZ+o5nhe0f+$?z*GElOV6K! r3ފ+h.#s}+c(('𣮙0Ś*̟~+dET7QTIvy(oz-\N)<#QZzJ(Fy(Vr2,[C^k̀K>ՙNbG6 s$žn5i.x{QEwQ:_@v#Gᛛt_I7QEM_z&bKW6cj۝5UԟjWKE㿎(|'ƞ,Uҹn+0!I/$L&{~Q\X[y!P`ߤ>E02fOi$Vq#"U8tg|7:(𳯩IүAE?5fÙJTUQEhT??TQTG4/z(1)ݨDCZ6QVx69\c5-4SEbF̽aǚGdJvs3{ܒ"@`iE0{k p}*p>VlL\F8UE8>#S]3EASY=k.W h=aH;n-$A=3,|}̋F^epgh1맹OcbKNO+litestar-2.16.0/tests/unit/test_kwargs/test_cleanup_group.py000066400000000000000000000065501500564371300243730ustar00rootroot00000000000000from __future__ import annotations import sys from typing import AsyncGenerator, Generator from unittest.mock import MagicMock if sys.version_info < (3, 11): from exceptiongroup import ExceptionGroup import pytest from litestar._kwargs.cleanup import DependencyCleanupGroup from litestar.utils.compat import async_next @pytest.fixture def cleanup_mock() -> MagicMock: return MagicMock() @pytest.fixture def async_cleanup_mock() -> MagicMock: return MagicMock() @pytest.fixture def generator(cleanup_mock: MagicMock) -> Generator[str, None, None]: def func() -> Generator[str, None, None]: try: yield "hello" finally: cleanup_mock() return func() @pytest.fixture def async_generator(async_cleanup_mock: MagicMock) -> AsyncGenerator[str, None]: async def func() -> AsyncGenerator[str, None]: try: yield "world" finally: async_cleanup_mock() return func() def test_add(generator: Generator[str, None, None]) -> None: group = DependencyCleanupGroup() group.add(generator) assert group._generators == [generator] async def test_cleanup(generator: Generator[str, None, None], cleanup_mock: MagicMock) -> None: next(generator) group = DependencyCleanupGroup([generator]) await group.close() cleanup_mock.assert_called_once() assert group._closed async def test_cleanup_throw_multiple_exceptions( generator: Generator[str, None, None], async_generator: AsyncGenerator[str, None], cleanup_mock: MagicMock, async_cleanup_mock: MagicMock, ) -> None: next(generator) await async_next(async_generator) group = DependencyCleanupGroup([generator, async_generator]) await group.close(ValueError()) cleanup_mock.assert_called_once() async_cleanup_mock.assert_called_once() assert group._closed @pytest.mark.parametrize("exit_exception", (None, ValueError())) async def test_exception_during_close( cleanup_mock: MagicMock, async_cleanup_mock: MagicMock, exit_exception: Exception | None, ) -> None: gen_exc = ValueError() def gen_fn() -> Generator[None, None, None]: try: yield finally: cleanup_mock() raise gen_exc # raise an exception here async def async_gen_fn() -> AsyncGenerator[None, None]: try: yield finally: async_cleanup_mock() # we expect this to be called still gen_1 = gen_fn() gen_2 = async_gen_fn() next(gen_1) await async_next(gen_2) group = DependencyCleanupGroup([gen_1, gen_2]) with pytest.raises(ExceptionGroup) as exc: await group.close(exit_exception) assert exc.value.exceptions == (gen_exc,) cleanup_mock.assert_called_once() async_cleanup_mock.assert_called_once() assert group._closed async def test_cleanup_on_closed_raises(generator: Generator[str, None, None]) -> None: next(generator) group = DependencyCleanupGroup([generator]) await group.close() with pytest.raises(RuntimeError): await group.close() async def test_add_on_closed_raises( generator: Generator[str, None, None], async_generator: AsyncGenerator[str, None] ) -> None: next(generator) group = DependencyCleanupGroup([generator]) await group.close() with pytest.raises(RuntimeError): group.add(async_generator) litestar-2.16.0/tests/unit/test_kwargs/test_cookie_params.py000066400000000000000000000040361500564371300243410ustar00rootroot00000000000000from typing import Optional, Type import pytest from typing_extensions import Annotated from litestar import get, post from litestar.params import Parameter, ParameterKwarg from litestar.status_codes import HTTP_200_OK, HTTP_400_BAD_REQUEST from litestar.testing import create_test_client @pytest.mark.parametrize( "t_type,param_dict,param,expected_code", [ ( Optional[str], {}, Parameter(cookie="special-cookie", min_length=1, max_length=2, required=False), HTTP_200_OK, ), (int, {"special-cookie": "123"}, Parameter(cookie="special-cookie", ge=100, le=201), HTTP_200_OK), (int, {"special-cookie": "123"}, Parameter(cookie="special-cookie", ge=100, le=120), HTTP_400_BAD_REQUEST), (int, {}, Parameter(cookie="special-cookie", ge=100, le=120), HTTP_400_BAD_REQUEST), (Optional[int], {}, Parameter(cookie="special-cookie", ge=100, le=120, required=False), HTTP_200_OK), ], ) def test_cookie_params(t_type: Type, param_dict: dict, param: ParameterKwarg, expected_code: int) -> None: test_path = "/test" @get(path=test_path) def test_method(special_cookie: t_type = param) -> None: # type: ignore[valid-type] if special_cookie: assert special_cookie in (param_dict.get("special-cookie"), int(param_dict.get("special-cookie"))) # type: ignore[arg-type] with create_test_client(test_method) as client: # Set cookies on the client to avoid warnings about per-request cookies. client.cookies = param_dict # type: ignore[assignment] response = client.get(test_path) assert response.status_code == expected_code, response.json() def test_cookie_param_with_post() -> None: # https://github.com/litestar-org/litestar/issues/3734 @post() async def handler(data: str, secret: Annotated[str, Parameter(cookie="x-secret")]) -> None: return None with create_test_client([handler], raise_server_exceptions=True) as client: assert client.post("/", json={}).status_code == 400 litestar-2.16.0/tests/unit/test_kwargs/test_defaults.py000066400000000000000000000012051500564371300233270ustar00rootroot00000000000000from litestar import get from litestar.params import Parameter from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client def test_params_default() -> None: test_path = "/test" @get(path=test_path) def test_method( page_size: int = Parameter(query="pageSize", gt=0, le=100, default=10), ) -> None: assert page_size with create_test_client(test_method) as client: response = client.get(f"{test_path}?pageSize=10") assert response.status_code == HTTP_200_OK response = client.get(f"{test_path}") assert response.status_code == HTTP_200_OK litestar-2.16.0/tests/unit/test_kwargs/test_dependency_batches.py000066400000000000000000000060661500564371300253410ustar00rootroot00000000000000from typing import List, Set import pytest from litestar._kwargs.dependencies import Dependency, create_dependency_batches from litestar.di import Provide from litestar.exceptions import HTTPException, ValidationException from litestar.handlers import get from litestar.status_codes import HTTP_400_BAD_REQUEST, HTTP_422_UNPROCESSABLE_ENTITY, HTTP_500_INTERNAL_SERVER_ERROR from litestar.testing import create_test_client async def dummy() -> None: pass DEPENDENCY_A = Dependency("A", Provide(dummy), []) DEPENDENCY_B = Dependency("B", Provide(dummy), []) DEPENDENCY_C1 = Dependency("C1", Provide(dummy), []) DEPENDENCY_C2 = Dependency("C2", Provide(dummy), [DEPENDENCY_C1]) DEPENDENCY_ALL_EXCEPT_A = Dependency("D", Provide(dummy), [DEPENDENCY_B, DEPENDENCY_C1, DEPENDENCY_C2]) @pytest.mark.parametrize( "dependency_tree,expected_batches", [ (set(), []), ({DEPENDENCY_A}, [{DEPENDENCY_A}]), ( {DEPENDENCY_A, DEPENDENCY_B}, [ {DEPENDENCY_A, DEPENDENCY_B}, ], ), ( {DEPENDENCY_C1, DEPENDENCY_C2}, [ {DEPENDENCY_C1}, {DEPENDENCY_C2}, ], ), ( {DEPENDENCY_A, DEPENDENCY_B, DEPENDENCY_C1, DEPENDENCY_C2, DEPENDENCY_ALL_EXCEPT_A}, [ {DEPENDENCY_A, DEPENDENCY_B, DEPENDENCY_C1}, {DEPENDENCY_C2}, {DEPENDENCY_ALL_EXCEPT_A}, ], ), ( {DEPENDENCY_ALL_EXCEPT_A}, [ {DEPENDENCY_B, DEPENDENCY_C1}, {DEPENDENCY_C2}, {DEPENDENCY_ALL_EXCEPT_A}, ], ), ], ) def test_dependency_batches(dependency_tree: Set[Dependency], expected_batches: List[Set[Dependency]]) -> None: calculated_batches = create_dependency_batches(dependency_tree) assert calculated_batches == expected_batches @pytest.mark.parametrize( "exception,status_code,text", [ (ValueError("value_error"), HTTP_500_INTERNAL_SERVER_ERROR, "Exception Group Traceback"), ( HTTPException(status_code=HTTP_422_UNPROCESSABLE_ENTITY, detail="http_exception"), HTTP_422_UNPROCESSABLE_ENTITY, '{"status_code":422,"detail":"http_exception"}', ), ( ValidationException("validation_exception"), HTTP_400_BAD_REQUEST, '{"status_code":400,"detail":"validation_exception"}', ), ], ) def test_dependency_batch_with_exception(exception: Exception, status_code: int, text: str) -> None: def a() -> None: raise exception def c(a: None, b: None) -> None: pass @get(path="/") def handler(c: None) -> None: pass with create_test_client( route_handlers=handler, dependencies={ "a": Provide(a), "b": Provide(dummy), "c": Provide(c), }, ) as client: response = client.get("/") assert response.status_code == status_code assert text in response.text litestar-2.16.0/tests/unit/test_kwargs/test_generator_dependencies.py000066400000000000000000000212531500564371300262210ustar00rootroot00000000000000from typing import Any, AsyncGenerator, Callable, Dict, Generator from unittest.mock import MagicMock import pytest from pytest import FixtureRequest from litestar import Response, WebSocket, get, websocket from litestar.response.base import ASGIResponse from litestar.testing import create_test_client @pytest.fixture def cleanup_mock() -> MagicMock: return MagicMock() @pytest.fixture def exception_mock() -> MagicMock: return MagicMock() @pytest.fixture def finally_mock() -> MagicMock: return MagicMock() @pytest.fixture def generator_dependency( cleanup_mock: MagicMock, exception_mock: MagicMock, finally_mock: MagicMock ) -> Callable[[], Generator[str, None, None]]: def dependency() -> Generator[str, None, None]: try: yield "hello" cleanup_mock() except ValueError: exception_mock() finally: finally_mock() return dependency @pytest.fixture def async_generator_dependency( cleanup_mock: MagicMock, exception_mock: MagicMock, finally_mock: MagicMock ) -> Callable[[], AsyncGenerator[str, None]]: async def dependency() -> AsyncGenerator[str, None]: try: yield "hello" cleanup_mock() except ValueError: exception_mock() finally: finally_mock() return dependency @pytest.mark.parametrize("cache", [False, True]) @pytest.mark.parametrize("dependency_fixture", ["generator_dependency", "async_generator_dependency"]) def test_generator_dependency( cache: bool, request: FixtureRequest, dependency_fixture: str, cleanup_mock: MagicMock, exception_mock: MagicMock, finally_mock: MagicMock, ) -> None: dependency = request.getfixturevalue(dependency_fixture) @get("/", dependencies={"dep": dependency}, cache=cache) def handler(dep: str) -> Dict[str, str]: return {"value": dep} with create_test_client(route_handlers=[handler]) as client: res = client.get("/") assert res.status_code == 200 assert res.json() == {"value": "hello"} cleanup_mock.assert_called_once() finally_mock.assert_called_once() exception_mock.assert_not_called() @pytest.mark.parametrize("dependency_fixture", ["generator_dependency", "async_generator_dependency"]) async def test_generator_dependency_websocket( request: FixtureRequest, dependency_fixture: str, cleanup_mock: MagicMock, exception_mock: MagicMock, finally_mock: MagicMock, ) -> None: dependency = request.getfixturevalue(dependency_fixture) @websocket("/ws", dependencies={"dep": dependency}) async def ws_handler(socket: WebSocket, dep: str) -> None: await socket.accept() await socket.send_json({"value": dep}) await socket.close() with create_test_client(route_handlers=[ws_handler]) as client, client.websocket_connect("/ws") as ws: assert ws.receive_json() == {"value": "hello"} cleanup_mock.assert_called_once() finally_mock.assert_called_once() exception_mock.assert_not_called() @pytest.mark.parametrize("dependency_fixture", ["generator_dependency", "async_generator_dependency"]) def test_generator_dependency_handle_exception_debug_false( request: FixtureRequest, dependency_fixture: str, cleanup_mock: MagicMock, exception_mock: MagicMock, finally_mock: MagicMock, ) -> None: dependency = request.getfixturevalue(dependency_fixture) @get("/", dependencies={"dep": dependency}) def handler(dep: str) -> Dict[str, str]: raise ValueError("foo") with create_test_client(route_handlers=[handler], debug=False) as client: res = client.get("/") assert res.status_code == 500 assert res.json() == {"detail": "Internal Server Error", "status_code": 500} cleanup_mock.assert_not_called() exception_mock.assert_called_once() finally_mock.assert_called_once() @pytest.mark.parametrize("dependency_fixture", ["generator_dependency", "async_generator_dependency"]) def test_generator_dependency_exception_during_cleanup_debug_false( request: FixtureRequest, dependency_fixture: str, cleanup_mock: MagicMock, exception_mock: MagicMock, finally_mock: MagicMock, ) -> None: dependency = request.getfixturevalue(dependency_fixture) cleanup_mock.side_effect = Exception("foo") @get("/", dependencies={"dep": dependency}) def handler(dep: str) -> Dict[str, str]: return {"value": dep} with create_test_client(route_handlers=[handler], debug=False) as client: res = client.get("/") assert res.status_code == 500 assert res.json() == {"status_code": 500, "detail": "Internal Server Error"} cleanup_mock.assert_called_once() finally_mock.assert_called_once() @pytest.mark.parametrize("dependency_fixture", ["generator_dependency", "async_generator_dependency"]) @pytest.mark.usefixtures("disable_warn_sync_to_thread_with_async") def test_generator_dependency_nested( request: FixtureRequest, dependency_fixture: str, cleanup_mock: MagicMock, exception_mock: MagicMock, finally_mock: MagicMock, ) -> None: dependency = request.getfixturevalue(dependency_fixture) async def nested_dependency_one(generator_dep: str) -> str: return generator_dep async def nested_dependency_two(generator_dep: str, nested_one: str) -> str: return generator_dep + nested_one @get( "/", dependencies={ "generator_dep": dependency, "nested_one": nested_dependency_one, "nested_two": nested_dependency_two, }, ) def handler(nested_two: str) -> Dict[str, str]: return {"value": nested_two} with create_test_client(route_handlers=[handler]) as client: res = client.get("/") assert res.status_code == 200 assert res.json() == {"value": "hellohello"} cleanup_mock.assert_called_once() finally_mock.assert_called_once() exception_mock.assert_not_called() @pytest.mark.parametrize("dependency_fixture", ["generator_dependency", "async_generator_dependency"]) def test_generator_dependency_nested_error_during_cleanup( request: FixtureRequest, dependency_fixture: str, cleanup_mock: MagicMock, exception_mock: MagicMock, finally_mock: MagicMock, ) -> None: dependency = request.getfixturevalue(dependency_fixture) cleanup_mock_no_raise = MagicMock() cleanup_mock.side_effect = ValueError() async def other_dependency(generator_dep: str) -> AsyncGenerator[str, None]: try: yield f"{generator_dep}, world" finally: cleanup_mock_no_raise() @get( "/", dependencies={"generator_dep": dependency, "other": other_dependency}, ) def handler(other: str) -> Dict[str, str]: return {"value": other} with create_test_client(route_handlers=[handler]) as client: res = client.get("/") assert res.status_code == 200 assert res.json() == {"value": "hello, world"} cleanup_mock.assert_called_once() finally_mock.assert_called_once() exception_mock.assert_called_once() cleanup_mock_no_raise.assert_called_once() def test_exception_on_response_thrown_into_generators() -> None: counter = 0 async def dependency() -> AsyncGenerator[int, None]: nonlocal counter counter += 1 try: yield counter finally: counter -= 1 class CustomResponse(Response[str]): def to_asgi_response( self, *args: Any, **kwargs: Any, ) -> ASGIResponse: raise Exception("foo") @get("/", dependencies={"dep": dependency}) def handler(dep: int) -> CustomResponse: return CustomResponse("") with create_test_client(route_handlers=[handler]) as client: res = client.get("/") assert res.status_code == 500 assert counter == 0 def test_exception_thrown_during_cleanup_of_exception() -> None: counter = 0 async def dependency() -> AsyncGenerator[int, None]: nonlocal counter counter += 1 try: yield counter finally: counter -= 1 raise ValueError() class CustomResponse(Response[str]): def to_asgi_response( self, *args: Any, **kwargs: Any, ) -> ASGIResponse: raise Exception("foo") @get("/", dependencies={"dep": dependency}) def handler(dep: int) -> CustomResponse: return CustomResponse("") with create_test_client(route_handlers=[handler]) as client: res = client.get("/") assert res.status_code == 500 assert counter == 0 litestar-2.16.0/tests/unit/test_kwargs/test_header_params.py000066400000000000000000000044251500564371300243220ustar00rootroot00000000000000from typing import Dict, Optional, Union import pytest from typing_extensions import Annotated from litestar import get, post from litestar.params import Parameter, ParameterKwarg from litestar.status_codes import HTTP_200_OK, HTTP_400_BAD_REQUEST from litestar.testing import create_test_client @pytest.mark.parametrize( "t_type,param_dict, param, should_raise", [ (str, {"special-header": "123"}, Parameter(header="special-header", min_length=1, max_length=3), False), (str, {"special-header": "123"}, Parameter(header="special-header", min_length=1, max_length=2), True), (str, {}, Parameter(header="special-header", min_length=1, max_length=2), True), (Optional[str], {}, Parameter(header="special-header", min_length=1, max_length=2, required=False), False), (int, {"special-header": "123"}, Parameter(header="special-header", ge=100, le=201), False), (int, {"special-header": "123"}, Parameter(header="special-header", ge=100, le=120), True), (int, {}, Parameter(header="special-header", ge=100, le=120), True), (Optional[int], {}, Parameter(header="special-header", ge=100, le=120, required=False), False), ], ) def test_header_params( t_type: Optional[Union[str, int]], param_dict: Dict[str, str], param: ParameterKwarg, should_raise: bool ) -> None: test_path = "/test" @get(path=test_path) def test_method(special_header: t_type = param) -> None: # type: ignore[valid-type] if special_header: assert special_header in (param_dict.get("special-header"), int(param_dict.get("special-header"))) # type: ignore[arg-type] with create_test_client(test_method) as client: response = client.get(test_path, headers=param_dict) if should_raise: assert response.status_code == HTTP_400_BAD_REQUEST, response.json() else: assert response.status_code == HTTP_200_OK, response.json() def test_header_param_with_post() -> None: # https://github.com/litestar-org/litestar/issues/3734 @post() async def handler(data: str, secret: Annotated[str, Parameter(header="x-secret")]) -> None: return None with create_test_client([handler], raise_server_exceptions=True) as client: assert client.post("/", json={}).status_code == 400 litestar-2.16.0/tests/unit/test_kwargs/test_json_data.py000066400000000000000000000025421500564371300234670ustar00rootroot00000000000000from dataclasses import asdict from msgspec import Struct from litestar import post from litestar.params import Body from litestar.status_codes import HTTP_201_CREATED from litestar.testing import create_test_client from . import Form def test_request_body_json() -> None: @post(path="/test") def test_method(data: Form = Body()) -> None: assert isinstance(data, Form) with create_test_client(test_method) as client: response = client.post("/test", json=asdict(Form(name="Moishe Zuchmir", age=30, programmer=True, value="100"))) assert response.status_code == HTTP_201_CREATED def test_empty_dict_allowed() -> None: @post(path="/test") def test_method(data: dict) -> None: assert isinstance(data, dict) with create_test_client(test_method) as client: response = client.post("/test", json={}) assert response.status_code == HTTP_201_CREATED def test_no_body_with_default() -> None: class Test(Struct, frozen=True): name: str default = Test(name="default") @post(path="/test", signature_types=[Test]) def test_method(data: Test = default) -> Test: return data with create_test_client(test_method) as client: response = client.post("/test") assert response.status_code == HTTP_201_CREATED assert response.json() == {"name": "default"} litestar-2.16.0/tests/unit/test_kwargs/test_layered_params.py000066400000000000000000000207261500564371300245210ustar00rootroot00000000000000from typing import List import pytest from litestar import Controller, Router, get from litestar.params import Parameter from litestar.status_codes import HTTP_200_OK, HTTP_400_BAD_REQUEST from litestar.testing import create_test_client def test_layered_parameters_injected_correctly() -> None: class MyController(Controller): path = "/controller" parameters = {"controller1": Parameter(lt=100), "controller2": Parameter(str, query="controller3")} @get("/{local:int}") def my_handler( self, local: float, controller1: int, controller2: str, router1: str, router2: float, app1: str, app2: List[str], ) -> dict: assert isinstance(local, float) assert isinstance(controller1, int) assert isinstance(controller2, str) assert isinstance(router1, str) assert isinstance(router2, float) assert isinstance(app1, str) assert isinstance(app2, list) return {"message": "ok"} router = Router( path="/router", route_handlers=[MyController], parameters={ "router1": Parameter(str, pattern="^[a-zA-Z]$"), "router2": Parameter(float, multiple_of=5.0, header="router3"), }, ) with create_test_client( route_handlers=router, parameters={ "app1": Parameter(str, cookie="app4"), "app2": Parameter(List[str], min_items=2), "app3": Parameter(bool, required=False), }, ) as client: # Set cookies on the client to avoid warnings about per-request cookies. client.cookies = {"app4": "jeronimo"} # type: ignore[assignment] query = {"controller1": "99", "controller3": "tuna", "router1": "albatross", "app2": ["x", "y"]} headers = {"router3": "10"} response = client.get("/router/controller/1", params=query, headers=headers) assert response.json() == {"message": "ok"} assert response.status_code == HTTP_200_OK @pytest.mark.parametrize( "parameter,param_type", [ ("controller1", "query"), ("controller3", "query"), ("router1", "query"), ("router3", "header"), ("app4", "cookie"), ("app2", "query"), ], ) def test_layered_parameters_validation(parameter: str, param_type: str) -> None: class MyController(Controller): path = "/controller" parameters = {"controller1": Parameter(int, lt=100), "controller2": Parameter(str, query="controller3")} @get("/{local:int}") def my_handler(self) -> dict: return {} router = Router( path="/router", route_handlers=[MyController], parameters={ "router1": Parameter(str, pattern="^[a-zA-Z]$"), "router2": Parameter(float, multiple_of=5.0, header="router3"), }, ) with create_test_client( route_handlers=router, parameters={ "app1": Parameter(str, cookie="app4"), "app2": Parameter(List[str], min_items=2), "app3": Parameter(bool, required=False), }, ) as client: query = {"controller1": "99", "controller3": "tuna", "router1": "albatross", "app2": ["x", "y"]} headers = {"router3": "10"} cookies = {"app4": "jeronimo"} if parameter in headers: headers = {} elif parameter in cookies: cookies = {} else: query.pop(parameter) # Set cookies on the client to avoid warnings about per-request cookies. client.cookies = cookies # type: ignore[assignment] response = client.get("/router/controller/1", params=query, headers=headers) assert response.status_code == HTTP_400_BAD_REQUEST assert response.json()["detail"].startswith(f"Missing required {param_type} parameter '{parameter}' for path") def test_layered_parameters_defaults_and_overrides() -> None: class MyController(Controller): path = "/controller" parameters = {"controller1": Parameter(int, default=50), "controller2": Parameter(str, query="controller3")} @get("/{local:int}") def my_handler( self, local: float, controller1: int, controller2: str = Parameter(str, query="controller4"), app1: str = Parameter(default="moishe"), ) -> dict: assert app1 == "moishe" assert controller2 == "jeronimo" assert controller1 == 50 return {"message": "ok"} router = Router( path="/router", route_handlers=[MyController], ) with create_test_client( route_handlers=router, parameters={ "app1": Parameter(str, default="haim"), }, ) as client: query = {"controller4": "jeronimo"} response = client.get("/router/controller/1", params=query) assert response.json() == {"message": "ok"} assert response.status_code == HTTP_200_OK def test_layered_include_in_schema_parameter() -> None: class IncludedAtController(Controller): path = "included_controller" include_in_schema = True @get("included_handler", include_in_schema=True) async def included_handler(self) -> None: # included at handler layer return None @get("excluded_handler", include_in_schema=False) async def excluded_handler(self) -> None: # excluded at handler layer return None @get("handler") async def route(self) -> None: # included at controller layer return None class ExcludedAtController(Controller): path = "excluded_controller" include_in_schema = False @get("included_handler", include_in_schema=True) async def included_handler(self) -> None: # included at handler layer return None @get("excluded_handler", include_in_schema=False) async def excluded_handler(self) -> None: # excluded at handler layer return None @get("handler") async def route(self) -> None: # excluded at controller layer return None @get("included_handler", include_in_schema=True) async def included_handler() -> None: # included at handler layer return None @get("excluded_handler", include_in_schema=False) async def excluded_handler() -> None: # excluded at handler layer return None @get("handler") async def route() -> None: # included or excluded depending on # the app or router layer setting return None common_routes = [included_handler, excluded_handler, route] IncludedAtRouter = Router( "included_router", route_handlers=common_routes, include_in_schema=True, ) ExcludedAtRouter = Router( "excluded_router", route_handlers=common_routes, include_in_schema=False, ) with create_test_client( [IncludedAtController, ExcludedAtController, IncludedAtRouter, ExcludedAtRouter, *common_routes], include_in_schema=False, ) as client: app = client.app assert app.openapi_schema.paths # routes that must be included assert "/included_controller/included_handler" in app.openapi_schema.paths assert "/included_controller/handler" in app.openapi_schema.paths assert "/excluded_controller/included_handler" in app.openapi_schema.paths assert "/included_router/included_handler" in app.openapi_schema.paths assert "/included_router/handler" in app.openapi_schema.paths assert "/excluded_router/included_handler" in app.openapi_schema.paths assert "/included_handler" in app.openapi_schema.paths # routes that must be excluded assert "/included_controller/excluded_handler" not in app.openapi_schema.paths assert "/excluded_controller/handler" not in app.openapi_schema.paths assert "/excluded_controller/excluded_handler" not in app.openapi_schema.paths assert "/included_router/excluded_handler" not in app.openapi_schema.paths assert "/excluded_router/handler" not in app.openapi_schema.paths assert "/excluded_router/excluded_handler" not in app.openapi_schema.paths assert "/excluded_handler" not in app.openapi_schema.paths assert "/handler" not in app.openapi_schema.paths litestar-2.16.0/tests/unit/test_kwargs/test_msgpack_data.py000066400000000000000000000027341500564371300241460ustar00rootroot00000000000000from msgspec import Struct from typing_extensions import Annotated from litestar import post from litestar.enums import RequestEncodingType from litestar.params import Body from litestar.serialization import encode_msgpack from litestar.status_codes import HTTP_201_CREATED from litestar.testing import create_test_client def test_request_body_msgpack() -> None: test_data = {"name": "Moishe Zuchmir", "age": 30, "programmer": True} @post(path="/header") def test_header(data: dict) -> None: assert isinstance(data, dict) assert data == test_data @post(path="/annotated") def test_annotated(data: dict = Body(media_type=RequestEncodingType.MESSAGEPACK)) -> None: assert isinstance(data, dict) assert data == test_data with create_test_client([test_header, test_annotated]) as client: response = client.post("/annotated", content=encode_msgpack(test_data)) assert response.status_code == HTTP_201_CREATED def test_no_body_with_default() -> None: class Test(Struct, frozen=True): name: str default = Test(name="default") @post(path="/test", signature_types=[Test]) def test_method(data: Annotated[Test, Body(media_type=RequestEncodingType.MESSAGEPACK)] = default) -> Test: return data with create_test_client(test_method) as client: response = client.post("/test") assert response.status_code == HTTP_201_CREATED assert response.json() == {"name": "default"} litestar-2.16.0/tests/unit/test_kwargs/test_multipart_data.py000066400000000000000000000555471500564371300245540ustar00rootroot00000000000000# ruff: noqa: UP006, UP007 from __future__ import annotations from collections import defaultdict from dataclasses import asdict, dataclass from os import path from os.path import dirname, join, realpath from pathlib import Path from typing import Any, DefaultDict, Dict, List, Optional import msgspec import pytest from typing_extensions import Annotated from litestar import Request, post from litestar.datastructures.upload_file import UploadFile from litestar.enums import RequestEncodingType from litestar.params import Body from litestar.status_codes import HTTP_201_CREATED, HTTP_400_BAD_REQUEST from litestar.testing import create_test_client from . import Form @dataclass class FormData: name: UploadFile age: UploadFile programmer: UploadFile @post("/form") async def form_handler(request: Request) -> Dict[str, Any]: data = await request.form() output = {} for key, value in data.items(): if isinstance(value, UploadFile): content = await value.read() output[key] = { "filename": value.filename, "content": content.decode(), "content_type": value.content_type, } else: output[key] = value return output @post("/form") async def form_multi_item_handler(request: Request) -> DefaultDict[str, list]: data = await request.form() output = defaultdict(list) for key, value in data.multi_items(): if isinstance(value, UploadFile): content = await value.read() output[key].append( { "filename": value.filename, "content": content.decode(), "content_type": value.content_type, } ) else: output[key].append(value) return output @post("/form") async def form_with_headers_handler(request: Request) -> Dict[str, Any]: data = await request.form() output = {} for key, value in data.items(): if isinstance(value, UploadFile): content = await value.read() output[key] = { "filename": value.filename, "content": content.decode(), "content_type": value.content_type, "headers": [[name.lower(), value] for name, value in value.headers.items()], } else: output[key] = value return output @pytest.mark.parametrize("t_type", [FormData, Dict[str, UploadFile], List[UploadFile], UploadFile]) def test_request_body_multi_part(t_type: type) -> None: test_path = "/test" data = asdict(Form(name="Moishe Zuchmir", age=30, programmer=True, value="100")) @post(path=test_path, signature_namespace={"t_type": t_type}) def test_method(data: Annotated[t_type, Body(media_type=RequestEncodingType.MULTI_PART)]) -> None: # type: ignore[valid-type] assert data with create_test_client(test_method) as client: response = client.post(test_path, files={k: str(v).encode("utf-8") for k, v in data.items()}) assert response.status_code == HTTP_201_CREATED def test_request_body_multi_part_mixed_field_content_types() -> None: @dataclass() class MultiPartFormWithMixedFields: image: UploadFile tags: List[int] @post(path="/form", signature_types=[MultiPartFormWithMixedFields]) async def test_method(data: MultiPartFormWithMixedFields = Body(media_type=RequestEncodingType.MULTI_PART)) -> None: file_data = await data.image.read() assert file_data == b"data" assert data.tags == [1, 2, 3] with create_test_client(test_method) as client: response = client.post( "/form", files={"image": ("image.png", b"data")}, data={ "tags": ["1", "2", "3"], }, ) assert response.status_code == HTTP_201_CREATED def test_multipart_request_files(tmpdir: Any) -> None: path1 = path.join(tmpdir, "test.txt") Path(path1).write_bytes(b"") with create_test_client(form_handler) as client, open(path1, "rb") as f: response = client.post("/form", files={"test": f}) assert response.json() == { "test": { "filename": "test.txt", "content": "", "content_type": "text/plain", } } def test_multipart_request_files_with_content_type(tmpdir: Any) -> None: path1 = path.join(tmpdir, "test.txt") Path(path1).write_bytes(b"") with create_test_client(form_handler) as client, open(path1, "rb") as f: response = client.post("/form", files={"test": ("test.txt", f, "text/plain")}) assert response.json() == { "test": { "filename": "test.txt", "content": "", "content_type": "text/plain", } } def test_multipart_request_multiple_files(tmpdir: Any) -> None: path1 = path.join(tmpdir, "test1.txt") Path(path1).write_bytes(b"") path2 = path.join(tmpdir, "test2.txt") Path(path2).write_bytes(b"") with create_test_client(form_handler) as client, open(path1, "rb") as f1, open(path2, "rb") as f2: response = client.post("/form", files={"test1": f1, "test2": ("test2.txt", f2, "text/plain")}) assert response.json() == { "test1": {"filename": "test1.txt", "content": "", "content_type": "text/plain"}, "test2": {"filename": "test2.txt", "content": "", "content_type": "text/plain"}, } def test_multipart_request_multiple_files_with_headers(tmpdir: Any) -> None: path1 = path.join(tmpdir, "test1.txt") Path(path1).write_bytes(b"") path2 = path.join(tmpdir, "test2.txt") Path(path2).write_bytes(b"") with create_test_client(form_with_headers_handler) as client, open(path1, "rb") as f1, open(path2, "rb") as f2: response = client.post( "/form", files=[ ("test1", (None, f1)), ("test2", ("test2.txt", f2, "text/plain", {"x-custom": "f2"})), ], ) assert response.json() == { "test1": "", "test2": { "filename": "test2.txt", "content": "", "content_type": "text/plain", "headers": [ ["content-disposition", 'form-data; name="test2"; filename="test2.txt"'], ["x-custom", "f2"], ["content-type", "text/plain"], ], }, } def test_multi_items(tmpdir: Any) -> None: path1 = path.join(tmpdir, "test1.txt") Path(path1).write_bytes(b"") path2 = path.join(tmpdir, "test2.txt") Path(path2).write_bytes(b"") with create_test_client(form_multi_item_handler) as client, open(path1, "rb") as f1, open(path2, "rb") as f2: response = client.post( "/form", data={"test1": "abc"}, files=[("test1", f1), ("test1", ("test2.txt", f2, "text/plain"))], ) assert response.json() == { "test1": [ "abc", {"filename": "test1.txt", "content": "", "content_type": "text/plain"}, {"filename": "test2.txt", "content": "", "content_type": "text/plain"}, ] } def test_multipart_request_mixed_files_and_data() -> None: with create_test_client(form_handler) as client: response = client.post( "/form", content=( # data b"--a7f7ac8d4e2e437c877bb7b8d7cc549c\r\n" b'Content-Disposition: form-data; name="field0"\r\n\r\n' b"value0\r\n" # file b"--a7f7ac8d4e2e437c877bb7b8d7cc549c\r\n" b'Content-Disposition: form-data; name="file"; filename="file.txt"\r\n' b"Content-Type: text/plain\r\n\r\n" b"\r\n" # data b"--a7f7ac8d4e2e437c877bb7b8d7cc549c\r\n" b'Content-Disposition: form-data; name="field1"\r\n\r\n' b"value1\r\n" b"--a7f7ac8d4e2e437c877bb7b8d7cc549c--\r\n" ), headers={"Content-Type": "multipart/form-data; boundary=a7f7ac8d4e2e437c877bb7b8d7cc549c"}, ) assert response.json() == { "file": { "filename": "file.txt", "content": "", "content_type": "text/plain", }, "field0": "value0", "field1": "value1", } def test_multipart_request_with_charset_for_filename() -> None: with create_test_client(form_handler) as client: response = client.post( "/form", content=( # file b"--a7f7ac8d4e2e437c877bb7b8d7cc549c\r\n" b'Content-Disposition: form-data; name="file"; filename="\xe6\x96\x87\xe6\x9b\xb8.txt"\r\n' b"Content-Type: text/plain\r\n\r\n" b"\r\n" b"--a7f7ac8d4e2e437c877bb7b8d7cc549c--\r\n" ), headers={"Content-Type": "multipart/form-data; charset=utf-8; boundary=a7f7ac8d4e2e437c877bb7b8d7cc549c"}, ) assert response.json() == { "file": { "filename": "文書.txt", "content": "", "content_type": "text/plain", } } def test_multipart_request_without_charset_for_filename() -> None: with create_test_client(form_handler) as client: response = client.post( "/form", content=( # file b"--a7f7ac8d4e2e437c877bb7b8d7cc549c\r\n" b'Content-Disposition: form-data; name="file"; filename="\xe7\x94\xbb\xe5\x83\x8f.jpg"\r\n' b"Content-Type: image/jpeg\r\n\r\n" b"\r\n" b"--a7f7ac8d4e2e437c877bb7b8d7cc549c--\r\n" ), headers={"Content-Type": "multipart/form-data; boundary=a7f7ac8d4e2e437c877bb7b8d7cc549c"}, ) assert response.json() == { "file": { "filename": "画像.jpg", "content": "", "content_type": "image/jpeg", } } @pytest.mark.xfail(reason="filename* is deprecated and should not be used according to RFC-7578") def test_multipart_request_with_asterisks_filename() -> None: with create_test_client(form_handler) as client: response = client.post( "/form", content=( # file b"--a7f7ac8d4e2e437c877bb7b8d7cc549c\r\n" b"Content-Disposition: form-data; name='file'; filename*=utf-8''Na%C3%AFve%20file.jpg\r\n" b"Content-Type: image/jpeg\r\n\r\n" b"\r\n" b"--a7f7ac8d4e2e437c877bb7b8d7cc549c--\r\n" ), headers={"Content-Type": "multipart/form-data; boundary=a7f7ac8d4e2e437c877bb7b8d7cc549c"}, ) assert response.json() == { "'file'": {"filename": "Naïve file.jpg", "content": "", "content_type": "image/jpeg"} } def test_multipart_request_with_encoded_value() -> None: with create_test_client(form_handler) as client: response = client.post( "/form", content=( b"--20b303e711c4ab8c443184ac833ab00f\r\n" b"Content-Disposition: form-data; " b'name="value"\r\n\r\n' b"Transf\xc3\xa9rer\r\n" b"--20b303e711c4ab8c443184ac833ab00f--\r\n" ), headers={"Content-Type": "multipart/form-data; charset=utf-8; boundary=20b303e711c4ab8c443184ac833ab00f"}, ) assert response.json() == {"value": "Transférer"} def test_urlencoded_request_data() -> None: with create_test_client(form_handler) as client: response = client.post("/form", data={"some": "data"}) assert response.json() == {"some": "data"} def test_no_request_data() -> None: with create_test_client(form_handler) as client: response = client.post("/form") assert response.json() == {} def test_urlencoded_percent_encoding() -> None: with create_test_client(form_handler) as client: response = client.post("/form", data={"some": "da ta"}) assert response.json() == {"some": "da ta"} def test_urlencoded_percent_encoding_keys() -> None: with create_test_client(form_handler) as client: response = client.post("/form", data={"so me": "data"}) assert response.json() == {"so me": "data"} def test_postman_multipart_form_data() -> None: postman_body = b'----------------------------850116600781883365617864\r\nContent-Disposition: form-data; name="attributes"; filename="test-attribute_5.tsv"\r\nContent-Type: text/tab-separated-values\r\n\r\n"Campaign ID"\t"Plate Set ID"\t"No"\n\r\n----------------------------850116600781883365617864\r\nContent-Disposition: form-data; name="fasta"; filename="test-sequence_correct_5.fasta"\r\nContent-Type: application/octet-stream\r\n\r\n>P23G01_IgG1-1411:H:Q10C3:1/1:NID18\r\nCAGGTATTGAA\r\n\r\n----------------------------850116600781883365617864--\r\n' postman_headers = { "Content-Type": "multipart/form-data; boundary=--------------------------850116600781883365617864", "user-agent": "PostmanRuntime/7.26.0", "accept": "*/*", "cache-control": "no-cache", "host": "10.0.5.13:80", "accept-encoding": "gzip, deflate, br", "connection": "keep-alive", "content-length": "2455", } with create_test_client(form_handler) as client: response = client.post("/form", content=postman_body, headers=postman_headers) assert response.json() == { "attributes": { "filename": "test-attribute_5.tsv", "content": '"Campaign ID"\t"Plate Set ID"\t"No"\n', "content_type": "text/tab-separated-values", }, "fasta": { "filename": "test-sequence_correct_5.fasta", "content": ">P23G01_IgG1-1411:H:Q10C3:1/1:NID18\r\nCAGGTATTGAA\r\n", "content_type": "application/octet-stream", }, } def test_image_upload() -> None: @post("/", signature_types=[UploadFile]) async def hello_world(data: UploadFile = Body(media_type=RequestEncodingType.MULTI_PART)) -> None: await data.read() with open(join(dirname(realpath(__file__)), "flower.jpeg"), "rb") as f, create_test_client( route_handlers=[hello_world] ) as client: data = f.read() response = client.post("/", files={"data": data}) assert response.status_code == HTTP_201_CREATED @pytest.mark.parametrize("optional", [True, False]) @pytest.mark.parametrize("file_count", (1, 2)) def test_upload_multiple_files(file_count: int, optional: bool) -> None: annotation = List[UploadFile] if optional: annotation = Optional[annotation] # type: ignore[misc, assignment] @post("/", signature_namespace={"annotation": annotation}) async def handler(data: annotation = Body(media_type=RequestEncodingType.MULTI_PART)) -> None: # pyright: ignore[reportGeneralTypeIssues] assert len(data) == file_count for file in data: assert await file.read() == b"1" with create_test_client([handler]) as client: files_to_upload = [("file", b"1") for _ in range(file_count)] response = client.post("/", files=files_to_upload) assert response.status_code == HTTP_201_CREATED @dataclass class Files: file_list: List[UploadFile] # https://github.com/litestar-org/litestar/issues/3407 @dataclass class OptionalFiles: file_list: Optional[List[UploadFile]] @pytest.mark.parametrize("file_model", (Files, OptionalFiles)) @pytest.mark.parametrize("file_count", (1, 2)) def test_upload_multiple_files_in_model(file_count: int, file_model: type[Files | OptionalFiles]) -> None: @post("/", signature_namespace={"file_model": file_model}) async def handler(data: file_model = Body(media_type=RequestEncodingType.MULTI_PART)) -> None: # type: ignore[valid-type] assert len(data.file_list) == file_count # type: ignore[attr-defined] for file in data.file_list: # type: ignore[attr-defined] assert await file.read() == b"1" with create_test_client([handler]) as client: files_to_upload = [("file_list", b"1") for _ in range(file_count)] response = client.post("/", files=files_to_upload) assert response.status_code == HTTP_201_CREATED def test_optional_formdata() -> None: @post("/", signature_types=[UploadFile]) async def hello_world(data: Optional[UploadFile] = Body(media_type=RequestEncodingType.MULTI_PART)) -> None: if data is not None: await data.read() with create_test_client(route_handlers=[hello_world]) as client: response = client.post("/") assert response.status_code == HTTP_201_CREATED @pytest.mark.parametrize("limit", (1000, 100, 10)) def test_multipart_form_part_limit(limit: int) -> None: @post("/", signature_types=[UploadFile]) async def hello_world(data: List[UploadFile] = Body(media_type=RequestEncodingType.MULTI_PART)) -> dict: return {"limit": len(data)} with create_test_client(route_handlers=[hello_world], multipart_form_part_limit=limit) as client: data = {str(i): "a" for i in range(limit)} response = client.post("/", files=data) assert response.status_code == HTTP_201_CREATED assert response.json() == {"limit": limit} data = {str(i): "a" for i in range(limit)} data[str(limit + 1)] = "b" response = client.post("/", files=data) assert response.status_code == HTTP_400_BAD_REQUEST def test_multipart_form_part_limit_body_param_precedence() -> None: app_limit = 100 route_limit = 10 @post("/", signature_types=[UploadFile]) async def hello_world( data: List[UploadFile] = Body(media_type=RequestEncodingType.MULTI_PART, multipart_form_part_limit=route_limit), ) -> None: assert len(data) == route_limit with create_test_client(route_handlers=[hello_world], multipart_form_part_limit=app_limit) as client: data = {str(i): "a" for i in range(route_limit)} response = client.post("/", files=data) assert response.status_code == HTTP_201_CREATED data = {str(i): "a" for i in range(route_limit + 1)} response = client.post("/", files=data) assert response.status_code == HTTP_400_BAD_REQUEST @dataclass class ProductForm: name: str int_field: int options: str optional_without_default: Optional[float] optional_with_default: Optional[int] = None def test_multipart_handling_of_none_values() -> None: @post("/", signature_types=[ProductForm]) def handler( data: Annotated[ProductForm, Body(media_type=RequestEncodingType.MULTI_PART)], ) -> None: assert data with create_test_client(route_handlers=[handler]) as client: response = client.post( "/", content=( b"--1f35df74046888ceaa62d8a534a076dd\r\n" b'Content-Disposition: form-data; name="name"\r\n' b"Content-Type: application/octet-stream\r\n\r\n" b"moishe zuchmir\r\n" b"--1f35df74046888ceaa62d8a534a076dd\r\n" b'Content-Disposition: form-data; name="int_field"\r\n' b"Content-Type: application/octet-stream\r\n\r\n" b"1\r\n" b"--1f35df74046888ceaa62d8a534a076dd\r\n" b'Content-Disposition: form-data; name="options"\r\n' b"Content-Type: application/octet-stream\r\n\r\n" b"[1,2,3,4]\r\n" b"--1f35df74046888ceaa62d8a534a076dd\r\n" b'Content-Disposition: form-data; name="optional_without_default"\r\n' b"Content-Type: application/octet-stream\r\n\r\n\r\n" b"--1f35df74046888ceaa62d8a534a076dd\r\n" b'Content-Disposition: form-data; name="optional_with_default"\r\n' b"Content-Type: application/octet-stream\r\n\r\n\r\n" b"--1f35df74046888ceaa62d8a534a076dd--\r\n" ), headers={"Content-Type": "multipart/form-data; boundary=1f35df74046888ceaa62d8a534a076dd"}, ) assert response.status_code == HTTP_201_CREATED class AddProductFormMsgspec(msgspec.Struct): name: str amount: Annotated[int, msgspec.Meta(lt=10, ge=1)] @pytest.mark.parametrize("form_type", [RequestEncodingType.URL_ENCODED, RequestEncodingType.MULTI_PART]) def test_multipart_and_url_encoded_behave_the_same(form_type) -> None: # type: ignore[no-untyped-def] @post(path="/form", signature_namespace={"form_object": AddProductFormMsgspec, "form_type": form_type}) async def form_(request: Request, data: Annotated[AddProductFormMsgspec, Body(media_type=form_type)]) -> int: assert isinstance(data.name, str) return data.amount with create_test_client( route_handlers=[ form_, ] ) as client: if form_type == RequestEncodingType.URL_ENCODED: response = client.post( "/form", data={ "name": 1, "amount": 1, }, ) else: response = client.post( "/form", content=( b"--1f35df74046888ceaa62d8a534a076dd\r\n" b'Content-Disposition: form-data; name="name"\r\n' b"Content-Type: application/octet-stream\r\n\r\n" b"1\r\n" b"--1f35df74046888ceaa62d8a534a076dd\r\n" b'Content-Disposition: form-data; name="amount"\r\n' b"Content-Type: application/octet-stream\r\n\r\n" b"1\r\n" b"--1f35df74046888ceaa62d8a534a076dd--\r\n" ), headers={"Content-Type": "multipart/form-data; boundary=1f35df74046888ceaa62d8a534a076dd"}, ) assert response.status_code == HTTP_201_CREATED def test_invalid_multipart_raises_client_error() -> None: with create_test_client(form_handler) as client: response = client.post( "/form", content=( b"--20b303e711c4ab8c443184ac833ab00f\r\n" b"Content-Disposition: form-data; " b'name="value"\r\n\r\n' b"--20b303e711c4ab8c44318833ab00f--\r\n" ), headers={"Content-Type": "multipart/form-data; charset=utf-8; boundary=20b303e711c4ab8c443184ac833ab00f"}, ) assert response.status_code == HTTP_400_BAD_REQUEST litestar-2.16.0/tests/unit/test_kwargs/test_path_params.py000066400000000000000000000147001500564371300240230ustar00rootroot00000000000000from datetime import date, datetime, time, timedelta from decimal import Decimal from pathlib import Path from typing import Any, Optional from unittest.mock import MagicMock from uuid import UUID, uuid1, uuid4 import pytest from litestar import Litestar, MediaType, get, post from litestar.exceptions import ImproperlyConfiguredException from litestar.params import Parameter from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_400_BAD_REQUEST from litestar.testing import create_test_client @pytest.mark.parametrize( "params_dict,should_raise", [ ( { "version": 1.0, "service_id": 1, "user_id": "abc", "order_id": str(uuid4()), }, False, ), ( { "version": 4.1, "service_id": 1, "user_id": "abc", "order_id": str(uuid4()), }, True, ), ( { "version": 0.2, "service_id": 101, "user_id": "abc", "order_id": str(uuid4()), }, True, ), ( { "version": 0.2, "service_id": 1, "user_id": "abcdefghijklm", "order_id": str(uuid4()), }, True, ), ( { "version": 0.2, "service_id": 1, "user_id": "abc", "order_id": str(uuid1()), }, False, ), ], ) def test_path_params(params_dict: dict, should_raise: bool) -> None: test_path = "{version:float}/{service_id:int}/{user_id:str}/{order_id:uuid}" @get(path=test_path) def test_method( order_id: UUID, version: float = Parameter(gt=0.1, le=4.0), service_id: int = Parameter(gt=0, le=100), user_id: str = Parameter(min_length=1, max_length=10), ) -> None: assert version assert service_id assert user_id assert order_id with create_test_client(test_method) as client: response = client.get( f"{params_dict['version']}/{params_dict['service_id']}/{params_dict['user_id']}/{params_dict['order_id']}" ) if should_raise: assert response.status_code == HTTP_400_BAD_REQUEST, response.json() else: assert response.status_code == HTTP_200_OK, response.json() @pytest.mark.parametrize( "path", [ "/{param}", "/{param:foo}", "/{param:int:int}", "/{:int}", "/{param:}", "/{ :int}", "/{:}", "/{::}", "/{}", ], ) def test_path_param_validation(path: str) -> None: @get(path=path) def test_method() -> None: raise AssertionError("should not be called") with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[test_method]) def test_duplicate_path_param_validation() -> None: @get(path="/{param:int}/foo/{param:int}") def test_method() -> None: raise AssertionError("should not be called") with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[test_method]) def test_path_param_defined_in_layered_params_error() -> None: @get(path="/{param:int}") def test_method(param: int) -> None: raise AssertionError("should not be called") with pytest.raises(ImproperlyConfiguredException) as exc_info: Litestar(route_handlers=[test_method], parameters={"param": Parameter(gt=3)}) assert "Kwarg resolution ambiguity detected for the following keys: param." in str(exc_info.value) @pytest.mark.parametrize( "param_type_name, param_type_class, value, expected_value", [ ["str", str, "abc", "abc"], ["int", int, "1", 1], ["float", float, "1.01", 1.01], ["uuid", UUID, "0fcb1054c56e4dd4a127f70a97d1fc21", UUID("0fcb1054c56e4dd4a127f70a97d1fc21")], ["uuid", UUID, "542226d1-7199-41a0-9cba-aaa6d85932a3", UUID("542226d1-7199-41a0-9cba-aaa6d85932a3")], ["decimal", Decimal, "1.00001", Decimal("1.00001")], ["date", date, "2023-07-15", date(year=2023, month=7, day=15)], ["time", time, "01:02:03", time(1, 2, 3)], ["datetime", datetime, "2023-07-15T15:45:34.073314", datetime.fromisoformat("2023-07-15T15:45:34.073314")], ["timedelta", timedelta, "86400.0", timedelta(days=1)], ["timedelta", timedelta, "P1D", timedelta(days=1)], ["timedelta", timedelta, "PT1H1S", timedelta(hours=1, seconds=1)], ["path", Path, "/1/2/3/4/some-file.txt", Path("/1/2/3/4/some-file.txt")], ["path", Path, "1/2/3/4/some-file.txt", Path("/1/2/3/4/some-file.txt")], ], ) def test_path_param_type_resolution( param_type_name: str, param_type_class: Any, value: str, expected_value: Any ) -> None: mock = MagicMock() @get("/some/test/path/{test:" + param_type_name + "}") def handler(test: param_type_class) -> None: mock(test) with create_test_client(handler) as client: response = client.get(f"/some/test/path/{value}") assert response.status_code == HTTP_200_OK mock.assert_called_once_with(expected_value) def test_differently_named_path_params_on_same_level() -> None: @get("/{name:str}", media_type=MediaType.TEXT) def get_greeting(name: str) -> str: return f"Hello, {name}!" @post("/{title:str}", media_type=MediaType.TEXT) def post_greeting(title: str) -> str: return f"Hello, {title}!" with create_test_client(route_handlers=[get_greeting, post_greeting]) as client: response = client.get("/Moishe") assert response.status_code == HTTP_200_OK assert response.text == "Hello, Moishe!" response = client.post("/Moishe") assert response.status_code == HTTP_201_CREATED assert response.text == "Hello, Moishe!" def test_optional_path_parameter() -> None: @get(path=["/", "/{message:str}"], media_type=MediaType.TEXT, sync_to_thread=False) def handler(message: Optional[str]) -> str: return message or "no message" with create_test_client(route_handlers=[handler]) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "no message" response = client.get("/hello") assert response.status_code == HTTP_200_OK assert response.text == "hello" litestar-2.16.0/tests/unit/test_kwargs/test_query_params.py000066400000000000000000000156541500564371300242450ustar00rootroot00000000000000from datetime import datetime from typing import ( Any, Dict, List, Optional, Tuple, Union, ) from urllib.parse import urlencode import pytest from typing_extensions import Annotated from litestar import MediaType, Request, get, post from litestar.datastructures import MultiDict from litestar.di import Provide from litestar.params import Parameter from litestar.status_codes import HTTP_200_OK, HTTP_400_BAD_REQUEST from litestar.testing import create_test_client @pytest.mark.parametrize( "params_dict,should_raise", [ ( { "page": 1, "pageSize": 1, "brands": ["Nike", "Adidas"], }, False, ), ( { "page": 1, "pageSize": 1, "brands": ["Nike", "Adidas", "Rebok"], }, False, ), ( { "page": 1, "pageSize": 1, }, True, ), ( { "page": 1, "pageSize": 1, "brands": ["Nike", "Adidas", "Rebok", "Polgat"], }, True, ), ( { "page": 1, "pageSize": 101, "brands": ["Nike", "Adidas", "Rebok"], }, True, ), ( { "page": 1, "pageSize": 1, "brands": [], }, True, ), ( { "page": 1, "pageSize": 1, "brands": ["Nike", "Adidas", "Rebok"], "from_date": datetime.now().timestamp(), }, False, ), ( { "page": 1, "pageSize": 1, "brands": ["Nike", "Adidas", "Rebok"], "from_date": datetime.now().timestamp(), "to_date": datetime.now().timestamp(), }, False, ), ( { "page": 1, "pageSize": 1, "brands": ["Nike"], "from_date": datetime.now().timestamp(), "to_date": datetime.now().timestamp(), }, False, ), ], ) def test_query_params(params_dict: dict, should_raise: bool) -> None: test_path = "/test" @get(path=test_path) def test_method( page: int, page_size: int = Parameter(query="pageSize", gt=0, le=100), brands: List[str] = Parameter(min_items=1, max_items=3), from_date: Optional[datetime] = None, to_date: Optional[datetime] = None, ) -> None: assert page assert page_size assert brands assert from_date or from_date is None assert to_date or to_date is None with create_test_client(test_method) as client: response = client.get(f"{test_path}?{urlencode(params_dict, doseq=True)}") if should_raise: assert response.status_code == HTTP_400_BAD_REQUEST, response.json() else: assert response.status_code == HTTP_200_OK, response.json() @pytest.mark.parametrize( "expected_type,provided_value,default,expected_response_code", [ (Union[int, List[int]], [1, 2, 3], None, HTTP_200_OK), (Union[int, List[int]], [1], None, HTTP_200_OK), ], ) def test_query_param_arrays(expected_type: Any, provided_value: Any, default: Any, expected_response_code: int) -> None: test_path = "/test" @get(test_path) def test_method_with_default(param: Any = default) -> None: return None @get(test_path) def test_method_without_default(param: Any) -> None: return None test_method = test_method_without_default if default is ... else test_method_with_default # Set the type annotation of 'param' in a way mypy can deal with test_method.fn.__annotations__["param"] = expected_type with create_test_client(test_method) as client: params = urlencode({"param": provided_value}, doseq=True) response = client.get(f"{test_path}?{params}") assert response.status_code == expected_response_code def test_query_kwarg() -> None: test_path = "/test" params = urlencode( { "a": ["foo", "bar"], "b": "qux", }, doseq=True, ) @get(test_path) def handler(a: List[str], b: List[str], query: MultiDict) -> None: assert isinstance(query, MultiDict) assert {k: query.getall(k) for k in query} == {"a": ["foo", "bar"], "b": ["qux"]} assert isinstance(a, list) assert isinstance(b, list) assert a == ["foo", "bar"] assert b == ["qux"] with create_test_client(handler) as client: response = client.get(f"{test_path}?{params}") assert response.status_code == HTTP_200_OK, response.json() @pytest.mark.parametrize( "values", ( (("first", "x@test.com"), ("second", "aaa")), (("first", "&@A.ac"), ("second", "aaa")), (("first", "a@A.ac&"), ("second", "aaa")), (("first", "a@A&.ac"), ("second", "aaa")), ), ) def test_query_parsing_of_escaped_values(values: Tuple[Tuple[str, str], Tuple[str, str]]) -> None: # https://github.com/litestar-org/litestar/issues/915 request_values: Dict[str, Any] = {} @get(path="/handler") def handler(request: Request, first: str, second: str) -> None: request_values["first"] = first request_values["second"] = second request_values["query"] = request.query_params params = dict(values) with create_test_client(handler) as client: response = client.get("/handler", params=params) assert response.status_code == HTTP_200_OK assert request_values["first"] == params["first"] assert request_values["second"] == params["second"] assert request_values["query"].get("first") == params["first"] assert request_values["query"].get("second") == params["second"] def test_query_param_dependency_with_alias() -> None: async def qp_dependency(page_size: int = Parameter(query="pageSize", gt=0, le=100)) -> int: return page_size @get("/", media_type=MediaType.TEXT) def handler(page_size_dep: int) -> str: return str(page_size_dep) with create_test_client(handler, dependencies={"page_size_dep": Provide(qp_dependency)}) as client: response = client.get("/?pageSize=1") assert response.status_code == HTTP_200_OK, response.text assert response.text == "1" def test_query_params_with_post() -> None: # https://github.com/litestar-org/litestar/issues/3734 @post() async def handler(data: str, secret: Annotated[str, Parameter(query="x-secret")]) -> None: return None with create_test_client([handler], raise_server_exceptions=True) as client: assert client.post("/", json={}).status_code == 400 litestar-2.16.0/tests/unit/test_kwargs/test_reserved_kwargs_injection.py000066400000000000000000000231021500564371300267570ustar00rootroot00000000000000from typing import Any, List, Optional, Type, cast import msgspec.json import pytest from litestar import ( Controller, HttpMethod, Litestar, MediaType, Request, delete, get, patch, post, put, ) from litestar.datastructures.state import ImmutableState, State from litestar.exceptions import ImproperlyConfiguredException from litestar.status_codes import ( HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, ) from litestar.testing import create_test_client from litestar.types import Scope from tests.models import DataclassPerson, DataclassPersonFactory class CustomState(State): called: bool msg: str def test_application_immutable_state_injection() -> None: @get("/", media_type=MediaType.TEXT) def route_handler(state: ImmutableState) -> str: assert state return cast("str", state.msg) with create_test_client(route_handler, state=State({"called": False})) as client: client.app.state.msg = "hello" assert not client.app.state.called response = client.get("/") assert response.status_code == HTTP_200_OK @pytest.mark.parametrize("state_typing", (State, CustomState)) def test_application_state_injection(state_typing: Type[State]) -> None: @get("/", media_type=MediaType.TEXT) def route_handler(state: state_typing) -> str: # type: ignore[valid-type] assert state state.called = True # type: ignore[attr-defined] return cast("str", state.msg) # type: ignore[attr-defined] with create_test_client(route_handler, state=State({"called": False})) as client: client.app.state.msg = "hello" assert not client.app.state.called response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "hello" assert client.app.state.called person_instance = DataclassPersonFactory.build() @pytest.mark.parametrize( "decorator, http_method, expected_status_code", [ (post, HttpMethod.POST, HTTP_201_CREATED), (put, HttpMethod.PUT, HTTP_200_OK), (patch, HttpMethod.PATCH, HTTP_200_OK), (delete, HttpMethod.DELETE, HTTP_204_NO_CONTENT), ], ) def test_data_using_model(decorator: Any, http_method: Any, expected_status_code: Any) -> None: test_path = "/person" class MyController(Controller): path = test_path @decorator() def test_method(self, data: DataclassPerson) -> None: assert data == person_instance with create_test_client(MyController) as client: response = client.request(http_method, test_path, json=msgspec.to_builtins(person_instance)) assert response.status_code == expected_status_code @pytest.mark.parametrize( "decorator, http_method, expected_status_code", [ (post, HttpMethod.POST, HTTP_201_CREATED), (put, HttpMethod.PUT, HTTP_200_OK), (patch, HttpMethod.PATCH, HTTP_200_OK), (delete, HttpMethod.DELETE, HTTP_204_NO_CONTENT), ], ) def test_data_using_list_of_models(decorator: Any, http_method: Any, expected_status_code: Any) -> None: test_path = "/person" people = DataclassPersonFactory.batch(size=5) class MyController(Controller): path = test_path @decorator() def test_method(self, data: List[DataclassPerson]) -> None: assert data == people with create_test_client(MyController) as client: response = client.request(http_method, test_path, json=msgspec.to_builtins(people)) assert response.status_code == expected_status_code @pytest.mark.parametrize("media_type", [MediaType.JSON, MediaType.MESSAGEPACK]) def test_request_with_invalid_data(media_type: MediaType) -> None: @post() def test_handler(data: Any) -> Any: return data with create_test_client(test_handler) as client: response = client.post("/", content=b"abc", headers={"Content-Type": media_type}) assert response.status_code == HTTP_400_BAD_REQUEST @pytest.mark.parametrize( "decorator, http_method, expected_status_code", [ (get, HttpMethod.GET, HTTP_200_OK), (post, HttpMethod.POST, HTTP_201_CREATED), (put, HttpMethod.PUT, HTTP_200_OK), (patch, HttpMethod.PATCH, HTTP_200_OK), (delete, HttpMethod.DELETE, HTTP_204_NO_CONTENT), ], ) def test_path_params(decorator: Any, http_method: Any, expected_status_code: Any) -> None: test_path = "/person" class MyController(Controller): path = test_path @decorator(path="/{person_id:str}") def test_method(self, person_id: str) -> None: assert person_id == person_instance.id with create_test_client(MyController) as client: response = client.request(http_method, f"{test_path}/{person_instance.id}") assert response.status_code == expected_status_code @pytest.mark.parametrize( "decorator, http_method, expected_status_code", [ (get, HttpMethod.GET, HTTP_200_OK), (post, HttpMethod.POST, HTTP_201_CREATED), (put, HttpMethod.PUT, HTTP_200_OK), (patch, HttpMethod.PATCH, HTTP_200_OK), (delete, HttpMethod.DELETE, HTTP_204_NO_CONTENT), ], ) def test_query_params(decorator: Any, http_method: Any, expected_status_code: Any) -> None: @decorator("/person") def handler(first: str, second: List[str], third: int, fourth: Optional[str] = None) -> None: assert first == "foo" assert second == ["a", "b"] assert third == 2 assert fourth is None with create_test_client(handler) as client: response = client.request(http_method, "/person", params={"first": "foo", "second": ["a", "b"], "third": "2"}) assert response.status_code == expected_status_code @pytest.mark.parametrize( "decorator, http_method, expected_status_code", [ (get, HttpMethod.GET, HTTP_200_OK), (post, HttpMethod.POST, HTTP_201_CREATED), (put, HttpMethod.PUT, HTTP_200_OK), (patch, HttpMethod.PATCH, HTTP_200_OK), (delete, HttpMethod.DELETE, HTTP_204_NO_CONTENT), ], ) def test_header_params(decorator: Any, http_method: Any, expected_status_code: Any) -> None: test_path = "/person" request_headers = { "application-type": "web", "site": "www.example.com", "user-agent": "some-thing", "accept": "*/*", } class MyController(Controller): path = test_path @decorator() def test_method(self, headers: dict) -> None: for key, value in request_headers.items(): assert headers[key] == value with create_test_client(MyController) as client: response = client.request(http_method, test_path, headers=request_headers) assert response.status_code == expected_status_code @pytest.mark.parametrize( "decorator, http_method, expected_status_code", [ (get, HttpMethod.GET, HTTP_200_OK), (post, HttpMethod.POST, HTTP_201_CREATED), (put, HttpMethod.PUT, HTTP_200_OK), (patch, HttpMethod.PATCH, HTTP_200_OK), (delete, HttpMethod.DELETE, HTTP_204_NO_CONTENT), ], ) def test_request(decorator: Any, http_method: Any, expected_status_code: Any) -> None: test_path = "/person" class MyController(Controller): path = test_path @decorator() def test_method(self, request: Request) -> None: assert isinstance(request, Request) with create_test_client(MyController) as client: response = client.request(http_method, test_path) assert response.status_code == expected_status_code @pytest.mark.parametrize( "decorator, http_method, expected_status_code", [ (get, HttpMethod.GET, HTTP_200_OK), (post, HttpMethod.POST, HTTP_201_CREATED), (put, HttpMethod.PUT, HTTP_200_OK), (patch, HttpMethod.PATCH, HTTP_200_OK), (delete, HttpMethod.DELETE, HTTP_204_NO_CONTENT), ], ) def test_scope(decorator: Any, http_method: Any, expected_status_code: Any) -> None: test_path = "/person" class MyController(Controller): path = test_path @decorator() def test_method(self, scope: Scope) -> None: assert isinstance(scope, dict) with create_test_client(MyController) as client: response = client.request(http_method, test_path) assert response.status_code == expected_status_code @pytest.mark.parametrize( "decorator, http_method, expected_status_code", [ (get, HttpMethod.GET, HTTP_200_OK), (post, HttpMethod.POST, HTTP_201_CREATED), (put, HttpMethod.PUT, HTTP_200_OK), (patch, HttpMethod.PATCH, HTTP_200_OK), (delete, HttpMethod.DELETE, HTTP_204_NO_CONTENT), ], ) def test_body(decorator: Any, http_method: Any, expected_status_code: Any) -> None: test_path = "/person" class MyController(Controller): path = test_path @decorator() async def test_method(self, request: Request[Any, Any, Any], body: bytes) -> None: assert body == await request.body() with create_test_client(MyController) as client: response = client.request(http_method, test_path) assert response.status_code == expected_status_code def test_improper_use_of_state_kwarg() -> None: """Test error condition of State kwarg with unexpected type..""" test_path = "/bad-state" class MyController(Controller): path = test_path @get() async def test_method(self, state: str) -> None: return None with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[MyController], openapi_config=None) litestar-2.16.0/tests/unit/test_kwargs/test_url_encoded_data.py000066400000000000000000000020731500564371300250000ustar00rootroot00000000000000from dataclasses import asdict from typing import Optional from litestar import post from litestar.enums import RequestEncodingType from litestar.params import Body from litestar.status_codes import HTTP_201_CREATED from litestar.testing import create_test_client from . import Form def test_request_body_url_encoded() -> None: @post(path="/test") def test_method(data: Form = Body(media_type=RequestEncodingType.URL_ENCODED)) -> None: assert isinstance(data, Form) with create_test_client(test_method) as client: response = client.post("/test", data=asdict(Form(name="Moishe Zuchmir", age=30, programmer=True, value="100"))) assert response.status_code == HTTP_201_CREATED def test_optional_request_body_url_encoded() -> None: @post(path="/test") def test_method(data: Optional[Form] = Body(media_type=RequestEncodingType.URL_ENCODED)) -> None: assert data is None with create_test_client(test_method) as client: response = client.post("/test", data={}) assert response.status_code == HTTP_201_CREATED litestar-2.16.0/tests/unit/test_kwargs/test_validations.py000066400000000000000000000105341500564371300240420ustar00rootroot00000000000000from typing import Any, Callable, Dict import pytest from litestar import Litestar, get, post, websocket from litestar.constants import RESERVED_KWARGS from litestar.di import Provide from litestar.enums import RequestEncodingType from litestar.exceptions import ImproperlyConfiguredException from litestar.params import Body, BodyKwarg, Parameter async def my_dependency() -> int: return 1 @pytest.mark.parametrize("param_field", ["query", "header", "cookie"]) def test_path_param_and_param_with_same_key_raises(param_field: str) -> None: @get("/{my_key:str}") def handler(my_key: str = Parameter(**{param_field: "my_key"})) -> None: # type: ignore[arg-type] pass with pytest.raises(ImproperlyConfiguredException): Litestar([handler]) def test_path_param_and_dependency_with_same_key_raises() -> None: @get("/{my_key:str}", dependencies={"my_key": Provide(my_dependency)}) def handler(my_key: str) -> None: pass with pytest.raises(ImproperlyConfiguredException): Litestar([handler]) @pytest.mark.parametrize("param_field", ["query", "header", "cookie"]) def test_dependency_and_aliased_param_raises(param_field: str) -> None: @get("/", dependencies={"my_key": Provide(my_dependency)}) def handler(my_key: str = Parameter(**{param_field: "my_key"})) -> None: # type: ignore[arg-type] pass with pytest.raises(ImproperlyConfiguredException): Litestar([handler]) @pytest.mark.parametrize("reserved_kwarg", sorted(RESERVED_KWARGS)) def test_raises_when_reserved_kwargs_are_misused(reserved_kwarg: str) -> None: decorator = post if reserved_kwarg != "socket" else websocket local = dict(locals(), **globals()) exec(f"async def test_fn({reserved_kwarg}: int) -> None: pass", local, local) handler_with_path_param = decorator("/{" + reserved_kwarg + ":int}")(local["test_fn"]) with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[handler_with_path_param]) exec(f"async def test_fn({reserved_kwarg}: int) -> None: pass", local, local) handler_with_dependency = decorator("/", dependencies={reserved_kwarg: Provide(my_dependency)})(local["test_fn"]) with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[handler_with_dependency]) exec(f"async def test_fn({reserved_kwarg}: int = Parameter(query='my_param')) -> None: pass", local, local) handler_with_aliased_param = decorator("/")(local["test_fn"]) with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[handler_with_aliased_param]) def url_encoded_dependency(data: Dict[str, Any] = Body(media_type=RequestEncodingType.URL_ENCODED)) -> Dict[str, Any]: assert data return data def multi_part_dependency(data: Dict[str, Any] = Body(media_type=RequestEncodingType.MULTI_PART)) -> Dict[str, Any]: assert data return data def json_dependency(data: Dict[str, Any] = Body()) -> Dict[str, Any]: assert data return data @pytest.mark.parametrize( "body, dependency", [ (Body(), json_dependency), (Body(media_type=RequestEncodingType.MULTI_PART), multi_part_dependency), (Body(media_type=RequestEncodingType.URL_ENCODED), url_encoded_dependency), ], ) def test_dependency_data_kwarg_validation_success_scenarios(body: BodyKwarg, dependency: Callable) -> None: @post("/", dependencies={"first": Provide(dependency)}) def handler(first: Dict[str, Any], data: Any = body) -> None: pass Litestar(route_handlers=[handler]) @pytest.mark.parametrize( "body, dependency", [ [Body(), url_encoded_dependency], [Body(), multi_part_dependency], [Body(media_type=RequestEncodingType.URL_ENCODED), json_dependency], [Body(media_type=RequestEncodingType.URL_ENCODED), multi_part_dependency], [Body(media_type=RequestEncodingType.MULTI_PART), json_dependency], [Body(media_type=RequestEncodingType.MULTI_PART), url_encoded_dependency], ], ) def test_dependency_data_kwarg_validation_failure_scenarios(body: BodyKwarg, dependency: Callable) -> None: @post("/", dependencies={"first": Provide(dependency, sync_to_thread=False)}) def handler(first: Dict[str, Any], data: Any = body) -> None: assert first assert data with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[handler]) litestar-2.16.0/tests/unit/test_logging/000077500000000000000000000000001500564371300202415ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_logging/__init__.py000066400000000000000000000000001500564371300223400ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_logging/test_logging_config.py000066400000000000000000000556711500564371300246430ustar00rootroot00000000000000import importlib import logging import sys import time from importlib.util import find_spec from logging.handlers import QueueHandler from queue import Queue from typing import TYPE_CHECKING, Any, Dict, Generator, Optional, Set, Type, Union, cast from unittest.mock import MagicMock, patch import pytest from _pytest.logging import LogCaptureHandler, _LiveLoggingNullHandler from litestar import Request, get from litestar.exceptions import HTTPException, ImproperlyConfiguredException, NotFoundException from litestar.logging.config import ( LoggingConfig, _get_default_handlers, _get_default_logging_module, default_handlers, default_picologging_handlers, ) from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from tests.helpers import cleanup_logging_impl if TYPE_CHECKING: from _pytest.capture import CaptureFixture @pytest.fixture(autouse=True) def cleanup_logging() -> Generator: _ = pytest.importorskip("picologging") with cleanup_logging_impl(): yield def test__get_default_handlers() -> None: assert _get_default_handlers(logging_module="logging") == default_handlers assert _get_default_handlers(logging_module="picologging") == default_picologging_handlers def test__get_default_logging_module() -> None: assert find_spec("picologging") # picologging should be installed in the test environment, simply checking that assert _get_default_logging_module() == "picologging" with patch("litestar.logging.config.find_spec") as find_spec_mock: find_spec_mock.return_value = None assert _get_default_logging_module() == "logging" @pytest.mark.parametrize( "logging_module, dict_config_callable, expected_called, expected_default_handlers", [ ["logging", "logging.config.dictConfig", True, default_handlers], ["logging", "picologging.config.dictConfig", False, default_handlers], ["picologging", "picologging.config.dictConfig", True, default_picologging_handlers], ["picologging", "logging.config.dictConfig", False, default_picologging_handlers], ], ) def test_correct_dict_config_called( logging_module: str, dict_config_callable: str, expected_called: bool, expected_default_handlers: Dict[str, Dict[str, Any]], ) -> None: with patch(dict_config_callable) as dict_config_mock: log_config = LoggingConfig(logging_module=logging_module) log_config.configure() if expected_called: assert dict_config_mock.called else: assert not dict_config_mock.called @pytest.mark.parametrize( "picologging_exists, expected_default_handlers", [ [True, default_picologging_handlers], [False, default_handlers], ], ) def test_correct_default_handlers_set(picologging_exists: bool, expected_default_handlers: Any) -> None: with patch("litestar.logging.config.find_spec") as find_spec_mock: find_spec_mock.return_value = picologging_exists log_config = LoggingConfig() assert log_config.handlers == expected_default_handlers @pytest.mark.parametrize( "logging_module, expected_handlers", [ ["logging", default_handlers], ["picologging", default_picologging_handlers], ], ) def test_correct_default_handlers_set_logging_module(logging_module: str, expected_handlers: Any) -> None: log_config = LoggingConfig(logging_module=logging_module) assert log_config.handlers == expected_handlers @pytest.mark.parametrize( "logging_module, dict_config_not_called", [ ["logging", "picologging.config.dictConfig"], ["picologging", "logging.config.dictConfig"], ], ) def test_dictconfig_on_startup(logging_module: str, dict_config_not_called: str) -> None: with patch(f"{logging_module}.config.dictConfig") as dict_config_mock: with patch(dict_config_not_called) as dict_config_not_called_mock: test_logger = LoggingConfig( logging_module=logging_module, loggers={"app": {"level": "INFO", "handlers": ["console"]}}, ) with create_test_client([], on_startup=[test_logger.configure], logging_config=None): assert dict_config_mock.called assert dict_config_mock.call_count == 1 assert dict_config_not_called_mock.call_count == 0 @pytest.mark.parametrize( "logging_module_str, expected_handler_class_str, expected_listener_class_str", [ [ "logging", "logging.handlers.QueueHandler" if sys.version_info >= (3, 12, 0) else "litestar.logging.standard.QueueListenerHandler", "litestar.logging.standard.LoggingQueueListener", ], [ "picologging", "litestar.logging.picologging.QueueListenerHandler", "picologging.handlers.QueueListener", # pyright: ignore[reportGeneralTypeIssues,reportAttributeAccessIssue] ], ], ) def test_default_queue_listener_handler( logging_module_str: str, expected_handler_class_str: Union[str, Any], expected_listener_class_str: str, capsys: "CaptureFixture[str]", ) -> None: logging_module = importlib.import_module(logging_module_str) if expected_handler_class_str == "litestar.logging.standard.QueueListenerHandler": from litestar.logging.standard import QueueListenerHandler expected_handler_class = QueueListenerHandler elif expected_handler_class_str == "litestar.logging.picologging.QueueListenerHandler": from litestar.logging.picologging import QueueListenerHandler expected_handler_class = QueueListenerHandler elif expected_handler_class_str == "logging.handlers.QueueHandler": from logging.handlers import QueueHandler as QueueListenerHandler expected_handler_class = QueueListenerHandler else: expected_handler_class = importlib.import_module(expected_handler_class_str) if expected_listener_class_str == "litestar.logging.standard.LoggingQueueListener": from litestar.logging.standard import LoggingQueueListener expected_listener_class = LoggingQueueListener elif expected_listener_class_str == "picologging.handlers.QueueListener": from picologging.handlers import QueueListener # pyright: ignore[reportMissingImports] expected_listener_class = QueueListener else: expected_listener_class = importlib.import_module(expected_listener_class_str) def wait_log_queue(queue: Any, sleep_time: float = 0.1, max_retries: int = 5) -> None: retry = 0 while queue.qsize() > 0 and retry < max_retries: retry += 1 time.sleep(sleep_time) def assert_log(queue: Any, expected: str, count: Optional[int] = None) -> None: wait_log_queue(queue) log_output = capsys.readouterr().err.strip() if count is not None: assert len(log_output.split("\n")) == count assert log_output == expected get_logger = LoggingConfig( logging_module=logging_module.__name__, formatters={"standard": {"format": "%(levelname)s :: %(name)s :: %(message)s"}}, loggers={ "test_logger": { "level": "INFO", "handlers": ["queue_listener"], "propagate": False, }, }, ).configure() logger = get_logger("test_logger") assert type(logger) is logging_module.Logger handler = logger.handlers[0] # pyright: ignore[reportGeneralTypeIssues,reportAttributeAccessIssue] assert type(handler) is expected_handler_class assert type(handler.queue) is Queue assert type(handler.listener) is expected_listener_class assert type(handler.listener.handlers[0]) is logging_module.StreamHandler logger.info("Testing now!") assert_log(handler.queue, expected="INFO :: test_logger :: Testing now!", count=1) var = "test_var" logger.info("%s", var) assert_log(handler.queue, expected="INFO :: test_logger :: test_var", count=1) def test_get_logger_without_logging_config() -> None: with create_test_client(logging_config=None) as client: with pytest.raises( ImproperlyConfiguredException, match="cannot call '.get_logger' without passing 'logging_config' to the Litestar constructor first", ): client.app.get_logger() @pytest.mark.parametrize( "logging_module_str, expected_handler_class_str", [ [ "logging", "logging.handlers.QueueHandler" if sys.version_info >= (3, 12, 0) else "litestar.logging.standard.QueueListenerHandler", ], ["picologging", "litestar.logging.picologging.QueueListenerHandler"], ], ) def test_default_loggers(logging_module_str: str, expected_handler_class_str: str) -> None: logging_module = importlib.import_module(logging_module_str) if expected_handler_class_str == "litestar.logging.standard.QueueListenerHandler": from litestar.logging.standard import QueueListenerHandler expected_handler_class = QueueListenerHandler elif expected_handler_class_str == "litestar.logging.picologging.QueueListenerHandler": from litestar.logging.picologging import QueueListenerHandler expected_handler_class = QueueListenerHandler elif expected_handler_class_str == "logging.handlers.QueueHandler": from logging.handlers import QueueHandler as QueueListenerHandler expected_handler_class = QueueListenerHandler else: expected_handler_class = importlib.import_module(expected_handler_class_str) with create_test_client(logging_config=LoggingConfig(logging_module=logging_module_str)) as client: root_logger = client.app.get_logger() assert isinstance(root_logger, logging_module.Logger) assert root_logger.name == "root" assert type(root_logger.handlers[0]) is expected_handler_class litestar_logger = client.app.logger assert type(litestar_logger) is logging_module.Logger assert litestar_logger.name == "litestar" assert type(litestar_logger.handlers[0]) is expected_handler_class # same handler instance assert root_logger.handlers[0] is litestar_logger.handlers[0] @pytest.mark.parametrize( "logging_module_str, expected_handler_class_str", [ [ "logging", "logging.handlers.QueueHandler" if sys.version_info >= (3, 12, 0) else "litestar.logging.standard.QueueListenerHandler", ], ["picologging", "litestar.logging.picologging.QueueListenerHandler"], ], ) def test_connection_logger(logging_module_str: str, expected_handler_class_str: str) -> None: logging_module = importlib.import_module(logging_module_str) if expected_handler_class_str == "litestar.logging.standard.QueueListenerHandler": from litestar.logging.standard import QueueListenerHandler expected_handler_class = QueueListenerHandler elif expected_handler_class_str == "litestar.logging.picologging.QueueListenerHandler": from litestar.logging.picologging import QueueListenerHandler expected_handler_class = QueueListenerHandler elif expected_handler_class_str == "logging.handlers.QueueHandler": from logging.handlers import QueueHandler as QueueListenerHandler expected_handler_class = QueueListenerHandler else: expected_handler_class = importlib.import_module(expected_handler_class_str) @get("/") def handler(request: Request) -> Dict[str, bool]: return {"isinstance": isinstance(request.logger.handlers[0], expected_handler_class)} # type: ignore[attr-defined] with create_test_client( route_handlers=[handler], logging_config=LoggingConfig(logging_module=logging_module.__name__), ) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.json()["isinstance"] @pytest.mark.parametrize("logging_module_str", ["logging", "picologging", None]) def test_validation(logging_module_str: Optional[str]) -> None: logging_module = importlib.import_module(logging_module_str) if logging_module_str else None if logging_module is None: logging_config = LoggingConfig( formatters={}, handlers={}, loggers={}, ) else: logging_config = LoggingConfig( logging_module=logging_module.__name__, formatters={}, handlers={}, loggers={}, ) expected_default_handlers = _get_default_handlers(logging_config.logging_module) assert logging_config.formatters["standard"] assert len(logging_config.formatters) == 1 assert logging_config.handlers["queue_listener"] == expected_default_handlers["queue_listener"] assert logging_config.handlers["console"] == expected_default_handlers["console"] assert len(logging_config.handlers) == 2 assert logging_config.loggers["litestar"] assert logging_config.loggers["litestar"]["handlers"] == ["queue_listener"] assert len(logging_config.loggers) == 1 @pytest.mark.parametrize( "logging_module_str, expected_handler_class_str", [ [ "logging", "logging.handlers.QueueHandler" if sys.version_info >= (3, 12, 0) else "litestar.logging.standard.QueueListenerHandler", ], ["picologging", "litestar.logging.picologging.QueueListenerHandler"], ], ) def test_root_logger(logging_module_str: str, expected_handler_class_str: str) -> None: logging_module = importlib.import_module(logging_module_str) if expected_handler_class_str == "litestar.logging.standard.QueueListenerHandler": from litestar.logging.standard import QueueListenerHandler expected_handler_class = QueueListenerHandler elif expected_handler_class_str == "litestar.logging.picologging.QueueListenerHandler": from litestar.logging.picologging import QueueListenerHandler expected_handler_class = QueueListenerHandler elif expected_handler_class_str == "logging.handlers.QueueHandler": from logging.handlers import QueueHandler as QueueListenerHandler expected_handler_class = QueueListenerHandler else: expected_handler_class = importlib.import_module(expected_handler_class_str) logging_config = LoggingConfig(logging_module=logging_module.__name__) get_logger = logging_config.configure() root_logger = get_logger() assert root_logger.name == "root" # type: ignore[attr-defined] assert isinstance(root_logger, logging_module.Logger) root_logger_handler = root_logger.handlers[0] # pyright: ignore[reportGeneralTypeIssues,reportAttributeAccessIssue] assert root_logger_handler.name == "queue_listener" assert isinstance(root_logger_handler, cast("Any", expected_handler_class)) @pytest.mark.parametrize("logging_module_str", ["logging", "picologging"]) def test_root_logger_no_config(logging_module_str: str) -> None: logging_module = importlib.import_module(logging_module_str) logging_config = LoggingConfig(logging_module=logging_module_str, configure_root_logger=False) get_logger = logging_config.configure() root_logger = get_logger() assert isinstance(root_logger, logging_module.Logger) handlers = root_logger.handlers # pyright: ignore[reportGeneralTypeIssues,reportAttributeAccessIssue] if logging_module == logging: # pytest automatically configures some handlers for handler in handlers: assert isinstance(handler, (_LiveLoggingNullHandler, LogCaptureHandler)) else: assert len(handlers) == 0 @pytest.mark.parametrize( "logging_module_str, configure_root_logger, expected_root_logger_handler_class_str", [ [ "logging", True, "logging.handlers.QueueHandler" if sys.version_info >= (3, 12, 0) else "litestar.logging.standard.QueueListenerHandler", ], ["logging", False, None], ["picologging", True, "litestar.logging.picologging.QueueListenerHandler"], ["picologging", False, None], ], ) def test_customizing_handler( logging_module_str: str, configure_root_logger: bool, expected_root_logger_handler_class_str: "Optional[str]", capsys: "CaptureFixture[str]", ) -> None: logging_module = importlib.import_module(logging_module_str) if expected_root_logger_handler_class_str is None: expected_root_logger_handler_class = None elif expected_root_logger_handler_class_str == "litestar.logging.standard.QueueListenerHandler": from litestar.logging.standard import QueueListenerHandler expected_root_logger_handler_class = QueueListenerHandler elif expected_root_logger_handler_class_str == "litestar.logging.picologging.QueueListenerHandler": from litestar.logging.picologging import QueueListenerHandler expected_root_logger_handler_class = QueueListenerHandler elif expected_root_logger_handler_class_str == "logging.handlers.QueueHandler": from logging.handlers import QueueHandler as QueueListenerHandler expected_root_logger_handler_class = QueueListenerHandler else: expected_root_logger_handler_class = importlib.import_module(expected_root_logger_handler_class_str) log_format = "%(levelname)s :: %(name)s :: %(message)s" logging_config = LoggingConfig( logging_module=logging_module.__name__, formatters={ "standard": {"format": log_format}, }, handlers={ "console_stdout": { "class": f"{logging_module.__name__}.StreamHandler", "stream": "ext://sys.stdout", "level": "DEBUG", "formatter": "standard", }, }, loggers={ "test_logger": { "level": "DEBUG", "handlers": ["console_stdout"], "propagate": False, }, "litestar": { "level": "DEBUG", "handlers": ["console_stdout"], "propagate": False, }, }, configure_root_logger=configure_root_logger, ) # picologging seems to be broken, cannot make it log on stdout? # https://github.com/microsoft/picologging/issues/205 if logging_module_str == "picologging": del logging_config.handlers["console_stdout"]["stream"] get_logger = logging_config.configure() root_logger = get_logger() if configure_root_logger is True: assert isinstance(root_logger, logging_module.Logger) assert root_logger.level == logging_module.INFO # pyright: ignore[reportGeneralTypeIssues,reportAttributeAccessIssue] root_logger_handler = root_logger.handlers[0] # pyright: ignore[reportGeneralTypeIssues,reportAttributeAccessIssue] assert root_logger_handler.name == "queue_listener" assert type(root_logger_handler) is expected_root_logger_handler_class if type(root_logger_handler) is QueueHandler: formatter = root_logger_handler.listener.handlers[0].formatter # type: ignore[attr-defined] else: formatter = root_logger_handler.formatter if formatter is not None: assert formatter._fmt == log_format else: # Root logger shouldn't be configured but pytest adds some handlers (for the standard `logging` module) for handler in root_logger.handlers: # type: ignore[attr-defined] assert isinstance(handler, (_LiveLoggingNullHandler, LogCaptureHandler)) def assert_logger(logger: Any) -> None: assert type(logger) is logging_module.Logger assert logger.level == logging_module.DEBUG assert len(logger.handlers) == 1 assert type(logger.handlers[0]) is logging_module.StreamHandler assert logger.handlers[0].name == "console_stdout" assert logger.handlers[0].formatter._fmt == log_format logger.info("Hello from '%s'", logging_module.__name__) if logging_module_str == "picologging": log_output = capsys.readouterr().err.strip() else: log_output = capsys.readouterr().out.strip() assert log_output == f"INFO :: {logger.name} :: Hello from '{logging_module.__name__}'" assert_logger(get_logger("litestar")) assert_logger(get_logger("test_logger")) @pytest.mark.parametrize("logging_module", ["logging", "picologging"]) def test_excluded_fields(logging_module: str) -> None: # according to https://docs.python.org/3/library/logging.config.html#dictionary-schema-details allowed_fields = { "version", "formatters", "filters", "handlers", "loggers", "root", "incremental", "disable_existing_loggers", } if logging_module == "picologging": allowed_fields.remove("incremental") with patch(f"{logging_module}.config.dictConfig") as dict_config_mock: LoggingConfig(logging_module=logging_module).configure() assert dict_config_mock.called for key in dict_config_mock.call_args.args[0].keys(): assert key in allowed_fields @pytest.mark.parametrize( "traceback_line_limit, expected_warning_deprecation_called", [ [-1, False], [20, True], ], ) def test_traceback_line_limit_deprecation(traceback_line_limit: int, expected_warning_deprecation_called: bool) -> None: with patch("litestar.logging.config.warn_deprecation") as mock_warning_deprecation: LoggingConfig(traceback_line_limit=traceback_line_limit) assert mock_warning_deprecation.called is expected_warning_deprecation_called @pytest.mark.parametrize( "disable_stack_trace, exception_to_raise, handler_called", [ # will log the stack trace [set(), HTTPException, True], [set(), ValueError, True], [{400}, HTTPException, True], [{NameError}, ValueError, True], [{400, NameError}, ValueError, True], # will not log the stack trace [{NotFoundException}, HTTPException, False], [{404}, HTTPException, False], [{ValueError}, ValueError, False], [{400, ValueError}, ValueError, False], [{404, NameError}, HTTPException, False], ], ) def test_disable_stack_trace( disable_stack_trace: Set[Union[int, Type[Exception]]], exception_to_raise: Type[Exception], handler_called: bool, ) -> None: mock_handler = MagicMock() logging_config = LoggingConfig(disable_stack_trace=disable_stack_trace, exception_logging_handler=mock_handler) @get("/error") async def error_route() -> None: raise exception_to_raise with create_test_client([error_route], logging_config=logging_config, debug=True) as client: if exception_to_raise is HTTPException: _ = client.get("/404-error") else: _ = client.get("/error") if handler_called: assert mock_handler.called, "Exception logging handler should have been called" else: assert not mock_handler.called, "Exception logging handler should not have been called" litestar-2.16.0/tests/unit/test_logging/test_structlog_config.py000066400000000000000000000221611500564371300252270ustar00rootroot00000000000000import datetime import sys from typing import Callable, Set, Type, Union from unittest.mock import MagicMock, patch import pytest import structlog from pytest import CaptureFixture from structlog import BytesLoggerFactory, get_logger from structlog.processors import JSONRenderer from structlog.types import BindableLogger, WrappedLogger from litestar import get from litestar.exceptions import HTTPException, NotFoundException from litestar.logging.config import LoggingConfig, StructlogEventFilter, StructLoggingConfig, default_json_serializer from litestar.plugins.structlog import StructlogConfig, StructlogPlugin from litestar.serialization import decode_json from litestar.testing import create_test_client # structlog.testing.capture_logs changes the processors # Because we want to test processors, use capsys instead def test_event_filter() -> None: """Functionality test for the event filter processor.""" event_filter = StructlogEventFilter(["a_key"]) log_event = {"a_key": "a_val", "b_key": "b_val"} log_event = event_filter(..., "", log_event) # type:ignore[assignment] assert log_event == {"b_key": "b_val"} def test_set_level_custom_logger_factory() -> None: """Functionality test for the event filter processor.""" def custom_logger_factory() -> Callable[..., WrappedLogger]: """Set the default logger factory for structlog. Returns: An optional logger factory. """ return BytesLoggerFactory() log_config = StructLoggingConfig(logger_factory=custom_logger_factory, wrapper_class=structlog.stdlib.BoundLogger) logger = get_logger() assert logger.bind().__class__.__name__ != "BoundLoggerFilteringAtDebug" log_config.set_level(logger, 10) logger.info("a message") assert logger.bind().__class__.__name__ == "BoundLoggerFilteringAtDebug" def test_structlog_plugin(capsys: CaptureFixture) -> None: with create_test_client([], plugins=[StructlogPlugin()]) as client: assert client.app.logger assert isinstance(client.app.logger.bind(), BindableLogger) client.app.logger.info("message", key="value") log_messages = [decode_json(value=x) for x in capsys.readouterr().out.splitlines()] assert len(log_messages) == 1 # Format should be: {event: message, key: value, level: info, timestamp: isoformat} log_messages[0].pop("timestamp") # Assume structlog formats timestamp correctly assert log_messages[0] == {"event": "message", "key": "value", "level": "info"} def test_structlog_plugin_config(capsys: CaptureFixture) -> None: config = StructlogConfig() with create_test_client([], plugins=[StructlogPlugin(config=config)]) as client: assert client.app.logger assert isinstance(client.app.logger.bind(), BindableLogger) client.app.logger.info("message", key="value") log_messages = [decode_json(value=x) for x in capsys.readouterr().out.splitlines()] assert len(log_messages) == 1 assert client.app.plugins.get(StructlogPlugin)._config == config def test_structlog_plugin_config_custom_standard_logger() -> None: standard_logging_config = LoggingConfig() structlog_logging_config = StructLoggingConfig(standard_lib_logging_config=standard_logging_config) config = StructlogConfig(structlog_logging_config=structlog_logging_config) with create_test_client([], plugins=[StructlogPlugin(config=config)]) as client: assert client.app.plugins.get(StructlogPlugin)._config == config assert ( client.app.plugins.get(StructlogPlugin)._config.structlog_logging_config.standard_lib_logging_config == standard_logging_config ) def test_structlog_plugin_config_custom() -> None: structlog_logging_config = StructLoggingConfig(standard_lib_logging_config=None) config = StructlogConfig(structlog_logging_config=structlog_logging_config) with create_test_client([], plugins=[StructlogPlugin(config=config)]) as client: assert client.app.plugins.get(StructlogPlugin)._config == config assert client.app.plugins.get(StructlogPlugin)._config.structlog_logging_config == structlog_logging_config assert ( client.app.plugins.get(StructlogPlugin)._config.structlog_logging_config.standard_lib_logging_config is not None ) def test_structlog_plugin_config_with_existing_logging_config(capsys: CaptureFixture) -> None: existing_log_config = StructLoggingConfig() standard_logging_config = LoggingConfig() structlog_logging_config = StructLoggingConfig(standard_lib_logging_config=standard_logging_config) config = StructlogConfig(structlog_logging_config=structlog_logging_config) with create_test_client([], logging_config=existing_log_config, plugins=[StructlogPlugin(config=config)]) as client: assert client.app.plugins.get(StructlogPlugin)._config == config assert "Found pre-configured" in capsys.readouterr().out def test_structlog_config_no_tty_default(capsys: CaptureFixture) -> None: with create_test_client([], logging_config=StructLoggingConfig()) as client: assert client.app.logger assert isinstance(client.app.logger.bind(), BindableLogger) client.app.logger.info("message", key="value") log_messages = [decode_json(value=x) for x in capsys.readouterr().out.splitlines()] assert len(log_messages) == 1 # Format should be: {event: message, key: value, level: info, timestamp: isoformat} log_messages[0].pop("timestamp") # Assume structlog formats timestamp correctly assert log_messages[0] == {"event": "message", "key": "value", "level": "info"} def test_structlog_config_tty_default(capsys: CaptureFixture, monkeypatch: pytest.MonkeyPatch) -> None: from sys import stderr monkeypatch.setattr(stderr, "isatty", lambda: True) with create_test_client([], logging_config=StructLoggingConfig()) as client: assert client.app.logger assert isinstance(client.app.logger.bind(), BindableLogger) client.app.logger.info("message", key="value") log_messages = capsys.readouterr().out.splitlines() assert len(log_messages) == 1 if sys.platform.startswith("win"): assert log_messages[0].startswith(str(datetime.datetime.now().year)) else: assert log_messages[0].startswith("\x1b[") def test_structlog_config_specify_processors(capsys: CaptureFixture) -> None: logging_config = StructLoggingConfig(processors=[JSONRenderer(serializer=default_json_serializer)]) with create_test_client([], logging_config=logging_config) as client: assert client.app.logger assert isinstance(client.app.logger.bind(), BindableLogger) client.app.logger.info("message1", key="value1") # Log twice to make sure issue #882 doesn't appear again client.app.logger.info("message2", key="value2") log_messages = [decode_json(value=x) for x in capsys.readouterr().out.splitlines()] assert log_messages == [ {"key": "value1", "event": "message1"}, {"key": "value2", "event": "message2"}, ] @pytest.mark.parametrize( "isatty, pretty_print_tty, expected_as_json", [ (True, True, False), (True, False, True), (False, True, True), (False, False, True), ], ) def test_structlog_config_as_json(isatty: bool, pretty_print_tty: bool, expected_as_json: bool) -> None: with patch("litestar.logging.config.sys.stderr.isatty") as isatty_mock: isatty_mock.return_value = isatty logging_config = StructLoggingConfig(pretty_print_tty=pretty_print_tty) assert logging_config.as_json() is expected_as_json @pytest.mark.parametrize( "disable_stack_trace, exception_to_raise, handler_called", [ # will log the stack trace [set(), HTTPException, True], [set(), ValueError, True], [{400}, HTTPException, True], [{NameError}, ValueError, True], [{400, NameError}, ValueError, True], # will not log the stack trace [{NotFoundException}, HTTPException, False], [{404}, HTTPException, False], [{ValueError}, ValueError, False], [{400, ValueError}, ValueError, False], [{404, NameError}, HTTPException, False], ], ) def test_structlog_disable_stack_trace( disable_stack_trace: Set[Union[int, Type[Exception]]], exception_to_raise: Type[Exception], handler_called: bool, ) -> None: mock_handler = MagicMock() logging_config = StructLoggingConfig( disable_stack_trace=disable_stack_trace, exception_logging_handler=mock_handler ) @get("/error") async def error_route() -> None: raise exception_to_raise with create_test_client([error_route], logging_config=logging_config, debug=True) as client: if exception_to_raise is HTTPException: _ = client.get("/404-error") else: _ = client.get("/error") if handler_called: assert mock_handler.called, "Structlog exception handler should have been called" else: assert not mock_handler.called, "Structlog exception handler should not have been called" litestar-2.16.0/tests/unit/test_middleware/000077500000000000000000000000001500564371300207305ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_middleware/__init__.py000066400000000000000000000000001500564371300230270ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_middleware/test_allowed_hosts_middleware.py000066400000000000000000000132741500564371300274140ustar00rootroot00000000000000from typing import TYPE_CHECKING, Any, cast import pytest from litestar import get from litestar.config.allowed_hosts import AllowedHostsConfig from litestar.exceptions import ImproperlyConfiguredException from litestar.middleware import MiddlewareProtocol from litestar.middleware.allowed_hosts import AllowedHostsMiddleware from litestar.status_codes import HTTP_200_OK, HTTP_400_BAD_REQUEST from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.types import Receive, Scope, Send class DummyApp(MiddlewareProtocol): # pyright: ignore async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: return def test_allowed_hosts_middleware() -> None: @get(path="/") def handler() -> None: ... client = create_test_client(route_handlers=[handler], allowed_hosts=["*.example.com", "moishe.zuchmir.com"]) unpacked_middleware = [] cur = client.app.asgi_router.root_route_map_node.children["/"].asgi_handlers["GET"][0] while hasattr(cur, "app"): unpacked_middleware.append(cur) cur = cast("Any", cur.app) unpacked_middleware.append(cur) allowed_hosts_middleware, *_ = unpacked_middleware assert isinstance(allowed_hosts_middleware, AllowedHostsMiddleware) assert allowed_hosts_middleware.allowed_hosts_regex.pattern == ".*\\.example.com$|moishe.zuchmir.com" # type: ignore[union-attr] def test_allowed_hosts_middleware_hosts_regex() -> None: config = AllowedHostsConfig(allowed_hosts=["*.example.com", "moishe.zuchmir.com"]) middleware = AllowedHostsMiddleware(app=DummyApp(), config=config) # type: ignore[abstract] assert middleware.allowed_hosts_regex is not None assert middleware.allowed_hosts_regex.pattern == ".*\\.example.com$|moishe.zuchmir.com" assert middleware.allowed_hosts_regex.fullmatch("www.example.com") assert middleware.allowed_hosts_regex.fullmatch("other.example.com") assert middleware.allowed_hosts_regex.fullmatch("x.y.z.example.com") assert middleware.allowed_hosts_regex.fullmatch("moishe.zuchmir.com") assert not middleware.allowed_hosts_regex.fullmatch("www.example.x.com") assert not middleware.allowed_hosts_regex.fullmatch("josh.zuchmir.com") assert not middleware.allowed_hosts_regex.fullmatch("x.moishe.zuchmir.com") assert not middleware.allowed_hosts_regex.fullmatch("moishe.zuchmir.x.com") def test_allowed_hosts_middleware_redirect_regex() -> None: config = AllowedHostsConfig( allowed_hosts=["*.example.com", "www.moishe.zuchmir.com", "www.yada.bada.bing.io", "example.com"] ) middleware = AllowedHostsMiddleware(app=DummyApp(), config=config) # type: ignore[abstract] assert middleware.redirect_domains is not None assert middleware.redirect_domains.pattern == "moishe.zuchmir.com|yada.bada.bing.io" assert middleware.redirect_domains.fullmatch("moishe.zuchmir.com") assert middleware.redirect_domains.fullmatch("yada.bada.bing.io") def test_middleware_allowed_hosts() -> None: @get("/") def handler() -> dict: return {"hello": "world"} config = AllowedHostsConfig(allowed_hosts=["*.example.com", "moishe.zuchmir.com"]) with create_test_client(handler, allowed_hosts=config) as client: client.base_url = "http://x.example.com" # type: ignore[assignment] response = client.get("/") assert response.status_code == HTTP_200_OK client.base_url = "http://x.y.example.com" # type: ignore[assignment] response = client.get("/") assert response.status_code == HTTP_200_OK client.base_url = "http://moishe.zuchmir.com" # type: ignore[assignment] response = client.get("/") assert response.status_code == HTTP_200_OK client.base_url = "http://x.moishe.zuchmir.com" # type: ignore[assignment] response = client.get("/") assert response.status_code == HTTP_400_BAD_REQUEST client.base_url = "http://x.example.x.com" # type: ignore[assignment] response = client.get("/") assert response.status_code == HTTP_400_BAD_REQUEST def test_middleware_allow_all() -> None: @get("/") def handler() -> dict: return {"hello": "world"} # contrived case - but if "*" is in hosts, we allow all. config = AllowedHostsConfig(allowed_hosts=["*", "*.example.com", "moishe.zuchmir.com"]) with create_test_client(handler, allowed_hosts=config) as client: client.base_url = "http://any.domain.allowed.com" # type: ignore[assignment] response = client.get("/") assert response.status_code == HTTP_200_OK def test_middleware_redirect_on_www_by_default() -> None: @get("/") def handler() -> dict: return {"hello": "world"} config = AllowedHostsConfig(allowed_hosts=["www.moishe.zuchmir.com"]) with create_test_client(handler, allowed_hosts=config) as client: client.base_url = "http://moishe.zuchmir.com" # type: ignore[assignment] response = client.get("/") assert response.status_code == HTTP_200_OK assert str(response.url) == "http://www.moishe.zuchmir.com/" def test_middleware_does_not_redirect_when_off() -> None: @get("/") def handler() -> dict: return {"hello": "world"} config = AllowedHostsConfig(allowed_hosts=["www.moishe.zuchmir.com"], www_redirect=False) with create_test_client(handler, allowed_hosts=config) as client: client.base_url = "http://moishe.zuchmir.com" # type: ignore[assignment] response = client.get("/") assert response.status_code == HTTP_400_BAD_REQUEST def test_validation_raises_for_wrong_wildcard_domain() -> None: with pytest.raises(ImproperlyConfiguredException): AllowedHostsConfig(allowed_hosts=["www.moishe.*.com"]) litestar-2.16.0/tests/unit/test_middleware/test_base_authentication_middleware.py000066400000000000000000000207041500564371300305520ustar00rootroot00000000000000from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Dict import pytest from litestar import Litestar, get, websocket from litestar.connection import Request, WebSocket from litestar.enums import HttpMethod from litestar.exceptions import PermissionDeniedException, WebSocketDisconnect from litestar.middleware.authentication import ( AbstractAuthenticationMiddleware, AuthenticationResult, ) from litestar.middleware.base import DefineMiddleware from litestar.status_codes import ( HTTP_200_OK, HTTP_403_FORBIDDEN, HTTP_500_INTERNAL_SERVER_ERROR, ) from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.connection import ASGIConnection async def dummy_app(scope: Any, receive: Any, send: Any) -> None: return None @dataclass class User: name: str id: int @dataclass class Auth: props: str user = User(name="moishe", id=100) auth = Auth(props="abc") state: Dict[str, AuthenticationResult] = {} class AuthMiddleware(AbstractAuthenticationMiddleware): async def authenticate_request(self, connection: "ASGIConnection") -> AuthenticationResult: param = connection.headers.get("Authorization") if param in state: return state.pop(param) raise PermissionDeniedException("unauthenticated") def test_authentication_middleware_http_routes() -> None: @get(path="/") def http_route_handler(request: Request[User, Auth, Any]) -> None: assert isinstance(request.user, User) assert isinstance(request.auth, Auth) client = create_test_client(route_handlers=[http_route_handler], middleware=[AuthMiddleware]) token = "abc" error_response = client.get("/", headers={"Authorization": token}) assert error_response.status_code == HTTP_403_FORBIDDEN state[token] = AuthenticationResult(user=user, auth=auth) success_response = client.get("/", headers={"Authorization": token}) assert success_response.status_code == HTTP_200_OK def test_authentication_middleware_not_installed_raises_for_user_scope_http() -> None: @get(path="/") def http_route_handler_user_scope(request: Request[User, None, Any]) -> None: assert request.user client = create_test_client(route_handlers=[http_route_handler_user_scope]) error_response = client.get("/", headers={"Authorization": "nope"}) assert error_response.status_code == HTTP_500_INTERNAL_SERVER_ERROR def test_authentication_middleware_not_installed_raises_for_auth_scope_http() -> None: @get(path="/") def http_route_handler_auth_scope(request: Request[None, Auth, Any]) -> None: assert request.auth client = create_test_client(route_handlers=[http_route_handler_auth_scope]) error_response = client.get("/", headers={"Authorization": "nope"}) assert error_response.status_code == HTTP_500_INTERNAL_SERVER_ERROR @websocket(path="/") async def websocket_route_handler(socket: WebSocket[User, Auth, Any]) -> None: await socket.accept() assert isinstance(socket.user, User) assert isinstance(socket.auth, Auth) assert isinstance(socket.app, Litestar) await socket.send_json({"data": "123"}) await socket.close() def test_authentication_middleware_websocket_routes() -> None: token = "abc" client = create_test_client(route_handlers=websocket_route_handler, middleware=[AuthMiddleware]) with pytest.raises(WebSocketDisconnect), client.websocket_connect("/", headers={"Authorization": token}) as ws: assert ws.receive_json() state[token] = AuthenticationResult(user=user, auth=auth) with client.websocket_connect("/", headers={"Authorization": token}) as ws: assert ws.receive_json() def test_authentication_middleware_not_installed_raises_for_user_scope_websocket() -> None: @websocket(path="/") async def route_handler(socket: WebSocket[User, Auth, Any]) -> None: await socket.accept() assert isinstance(socket.user, User) client = create_test_client(route_handlers=route_handler) with pytest.raises(WebSocketDisconnect), client.websocket_connect("/", headers={"Authorization": "yep"}) as ws: ws.receive_json() def test_authentication_middleware_not_installed_raises_for_auth_scope_websocket() -> None: @websocket(path="/") async def route_handler(socket: WebSocket[User, Auth, Any]) -> None: await socket.accept() assert isinstance(socket.auth, Auth) client = create_test_client(route_handlers=route_handler) with pytest.raises(WebSocketDisconnect), client.websocket_connect("/", headers={"Authorization": "yep"}) as ws: ws.receive_json() def test_authentication_middleware_exclude() -> None: auth_mw = DefineMiddleware(AuthMiddleware, exclude=["north", "south"]) @get("/north/{value:int}") def north_handler(value: int) -> Dict[str, int]: return {"value": value} @get("/south") def south_handler() -> None: return None @get("/west") def west_handler() -> None: return None with create_test_client( route_handlers=[north_handler, south_handler, west_handler], middleware=[auth_mw], ) as client: response = client.get("/north/1") assert response.status_code == HTTP_200_OK response = client.get("/south") assert response.status_code == HTTP_200_OK response = client.get("/west") assert response.status_code == HTTP_403_FORBIDDEN def test_authentication_middleware_exclude_from_auth() -> None: auth_mw = DefineMiddleware(AuthMiddleware, exclude=["south", "east"]) @get("/north/{value:int}", exclude_from_auth=True) def north_handler(value: int) -> Dict[str, int]: return {"value": value} @get("/south") def south_handler() -> None: return None @get("/west") def west_handler() -> None: return None @get("/east", exclude_from_auth=True) def east_handler() -> None: return None with create_test_client( route_handlers=[north_handler, south_handler, west_handler, east_handler], middleware=[auth_mw], ) as client: response = client.get("/north/1") assert response.status_code == HTTP_200_OK response = client.get("/south") assert response.status_code == HTTP_200_OK response = client.get("/east") assert response.status_code == HTTP_200_OK response = client.get("/west") assert response.status_code == HTTP_403_FORBIDDEN def test_authentication_middleware_exclude_from_auth_custom_key() -> None: auth_mw = DefineMiddleware(AuthMiddleware, exclude=["south", "east"], exclude_from_auth_key="my_exclude_key") @get("/north/{value:int}", my_exclude_key=True) def north_handler(value: int) -> Dict[str, int]: return {"value": value} @get("/south") def south_handler() -> None: return None @get("/west") def west_handler() -> None: return None @get("/east", my_exclude_key=True) def east_handler() -> None: return None with create_test_client( route_handlers=[north_handler, south_handler, west_handler, east_handler], middleware=[auth_mw], ) as client: response = client.get("/north/1") assert response.status_code == HTTP_200_OK response = client.get("/south") assert response.status_code == HTTP_200_OK response = client.get("/east") assert response.status_code == HTTP_200_OK response = client.get("/west") assert response.status_code == HTTP_403_FORBIDDEN def test_authentication_exclude_http_methods() -> None: auth_mw = DefineMiddleware(AuthMiddleware, exclude_http_methods=[HttpMethod.GET]) @get("/") def exclude_get_handler() -> None: return None with create_test_client(route_handlers=[exclude_get_handler], middleware=[auth_mw]) as client: response = client.get("/") assert response.status_code == HTTP_200_OK response = client.options("/") assert response.status_code == HTTP_403_FORBIDDEN def test_authentication_exclude_http_methods_default() -> None: auth_mw = DefineMiddleware(AuthMiddleware) @get("/") def exclude_get_handler() -> None: return None with create_test_client(route_handlers=[exclude_get_handler], middleware=[auth_mw]) as client: response = client.get("/") assert response.status_code == HTTP_403_FORBIDDEN # OPTIONS should be excluded by default response = client.options("/") assert response.is_success litestar-2.16.0/tests/unit/test_middleware/test_base_middleware.py000066400000000000000000000275741500564371300254670ustar00rootroot00000000000000from typing import TYPE_CHECKING, List, Tuple, Union from warnings import catch_warnings import pytest from litestar import MediaType, asgi, get from litestar.datastructures.headers import MutableScopeHeaders from litestar.exceptions import LitestarWarning, ValidationException from litestar.middleware import AbstractMiddleware, ASGIMiddleware, DefineMiddleware from litestar.response.base import ASGIResponse from litestar.status_codes import HTTP_400_BAD_REQUEST from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.types import ASGIApp, Message, Receive, Scope, Send def test_custom_middleware() -> None: class SubclassMiddleware(AbstractMiddleware): async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: async def _send(message: "Message") -> None: if message["type"] == "http.response.start": headers = MutableScopeHeaders(message) headers.add("test", str(123)) await send(message) await self.app(scope, receive, _send) @get("/") def handler() -> dict: return {"hello": "world"} with create_test_client(handler, middleware=[DefineMiddleware(SubclassMiddleware)]) as client: response = client.get("/") assert response.headers["test"] == "123" def test_raises_exception() -> None: class SubclassMiddleware(AbstractMiddleware): async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: raise ValidationException(detail="nope") @get("/") def handler() -> dict: return {"hello": "world"} with create_test_client(handler, middleware=[DefineMiddleware(SubclassMiddleware)]) as client: response = client.get("/") assert response.status_code == HTTP_400_BAD_REQUEST def test_exclude_by_pattern() -> None: class SubclassMiddleware(AbstractMiddleware): exclude = r"^/123" async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: async def _send(message: "Message") -> None: if message["type"] == "http.response.start": headers = MutableScopeHeaders(message) headers.add("test", str(123)) await send(message) await self.app(scope, receive, _send) @get("/123") def first_handler() -> dict: return {"hello": "world"} @get("/456") def second_handler() -> dict: return {"hello": "world"} @asgi("/mount", is_mount=True) async def handler(scope: "Scope", receive: "Receive", send: "Send") -> None: response = ASGIResponse(body=b"ok", media_type=MediaType.TEXT) await response(scope, receive, send) with create_test_client( [first_handler, second_handler, handler], middleware=[DefineMiddleware(SubclassMiddleware)] ) as client: response = client.get("/123") assert "test" not in response.headers response = client.get("/456") assert "test" in response.headers response = client.get("/mount/123") assert "test" in response.headers def test_exclude_by_pattern_list() -> None: class SubclassMiddleware(AbstractMiddleware): exclude = ["123", "456"] async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: async def _send(message: "Message") -> None: if message["type"] == "http.response.start": headers = MutableScopeHeaders(message) headers.add("test", str(123)) await send(message) await self.app(scope, receive, _send) @get("/123") def first_handler() -> dict: return {"hello": "world"} @get("/456") def second_handler() -> dict: return {"hello": "world"} @get("/789") def third_handler() -> dict: return {"hello": "world"} with create_test_client( [first_handler, second_handler, third_handler], middleware=[DefineMiddleware(SubclassMiddleware)] ) as client: response = client.get("/123") assert "test" not in response.headers response = client.get("/456") assert "test" not in response.headers response = client.get("/789") assert "test" in response.headers @pytest.mark.parametrize("excludes", ["/", ["/", "/foo"], "/*", "/.*"]) def test_exclude_by_pattern_warns_if_exclude_all(excludes: Union[str, List[str]]) -> None: class SubclassMiddleware(AbstractMiddleware): exclude = excludes async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: await self.app(scope, receive, send) with pytest.warns(LitestarWarning, match="Middleware 'SubclassMiddleware' exclude pattern"): create_test_client(middleware=[SubclassMiddleware]) def test_exclude_doesnt_warn_on_non_greedy_pattern() -> None: class SubclassMiddleware(AbstractMiddleware): exclude = "^/$" async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: await self.app(scope, receive, send) with catch_warnings(record=True) as warnings: create_test_client(middleware=[SubclassMiddleware]) assert len(warnings) == 0 def test_exclude_by_opt_key() -> None: class SubclassMiddleware(AbstractMiddleware): exclude_opt_key = "exclude_route" async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: async def _send(message: "Message") -> None: if message["type"] == "http.response.start": headers = MutableScopeHeaders(message) headers.add("test", str(123)) await send(message) await self.app(scope, receive, _send) @get("/", exclude_route=True) def handler() -> dict: return {"hello": "world"} with create_test_client(handler, middleware=[DefineMiddleware(SubclassMiddleware)]) as client: response = client.get("/") assert "test" not in response.headers def test_abstract_middleware_deprecation_warning() -> None: with pytest.warns(DeprecationWarning, match="AbstractMiddleware"): class MyMiddleware(AbstractMiddleware): pass def test_asgi_middleware() -> None: class SubclassMiddleware(ASGIMiddleware): async def handle(self, scope: "Scope", receive: "Receive", send: "Send", next_app: "ASGIApp") -> None: async def _send(message: "Message") -> None: if message["type"] == "http.response.start": headers = MutableScopeHeaders(message) headers.add("test", str(123)) await send(message) await next_app(scope, receive, _send) @get("/") def handler() -> dict: return {"hello": "world"} with create_test_client(handler, middleware=[SubclassMiddleware()]) as client: response = client.get("/") assert response.headers["test"] == "123" def test_asgi_middleware_raises_exception() -> None: class SubclassMiddleware(ASGIMiddleware): async def handle(self, scope: "Scope", receive: "Receive", send: "Send", next_app: "ASGIApp") -> None: raise ValidationException(detail="nope") @get("/") def handler() -> dict: return {"hello": "world"} with create_test_client(handler, middleware=[SubclassMiddleware()]) as client: response = client.get("/") assert response.status_code == HTTP_400_BAD_REQUEST def test_asgi_middleware_exclude_by_pattern() -> None: class SubclassMiddleware(ASGIMiddleware): def __init__(self) -> None: self.exclude_path_pattern = r"^/123" async def handle(self, scope: "Scope", receive: "Receive", send: "Send", next_app: "ASGIApp") -> None: async def _send(message: "Message") -> None: if message["type"] == "http.response.start": headers = MutableScopeHeaders(message) headers.add("test", str(123)) await send(message) await next_app(scope, receive, _send) @get("/123") def first_handler() -> dict: return {"hello": "world"} @get("/456") def second_handler() -> dict: return {"hello": "world"} @asgi("/mount", is_mount=True) async def handler(scope: "Scope", receive: "Receive", send: "Send") -> None: response = ASGIResponse(body=b"ok", media_type=MediaType.TEXT) await response(scope, receive, send) with create_test_client([first_handler, second_handler, handler], middleware=[SubclassMiddleware()]) as client: response = client.get("/123") assert "test" not in response.headers response = client.get("/456") assert "test" in response.headers response = client.get("/mount/123") assert "test" in response.headers def test_asgi_middleware_exclude_by_pattern_tuple() -> None: class SubclassMiddleware(ASGIMiddleware): exclude_path_pattern = ("123", "456") async def handle(self, scope: "Scope", receive: "Receive", send: "Send", next_app: "ASGIApp") -> None: async def _send(message: "Message") -> None: if message["type"] == "http.response.start": headers = MutableScopeHeaders(message) headers.add("test", str(123)) await send(message) await next_app(scope, receive, _send) @get("/123") def first_handler() -> dict: return {"hello": "world"} @get("/456") def second_handler() -> dict: return {"hello": "world"} @get("/789") def third_handler() -> dict: return {"hello": "world"} with create_test_client( [first_handler, second_handler, third_handler], middleware=[SubclassMiddleware()] ) as client: response = client.get("/123") assert "test" not in response.headers response = client.get("/456") assert "test" not in response.headers response = client.get("/789") assert "test" in response.headers @pytest.mark.parametrize("excludes", ["/", ("/", "/foo"), "/*", "/.*"]) def test_asgi_middleware_exclude_by_pattern_warns_if_exclude_all(excludes: Union[str, Tuple[str, ...]]) -> None: class SubclassMiddleware(ASGIMiddleware): exclude_path_pattern = excludes async def handle(self, scope: "Scope", receive: "Receive", send: "Send", next_app: "ASGIApp") -> None: await next_app(scope, receive, send) with pytest.warns(LitestarWarning, match="Middleware 'SubclassMiddleware' exclude pattern"): create_test_client(middleware=[SubclassMiddleware()]) def test_asgi_middleware_exclude_doesnt_warn_on_non_greedy_pattern() -> None: class SubclassMiddleware(ASGIMiddleware): exclude_path_pattern = "^/$" async def handle(self, scope: "Scope", receive: "Receive", send: "Send", next_app: "ASGIApp") -> None: await next_app(scope, receive, send) with catch_warnings(record=True) as warnings: create_test_client(middleware=[SubclassMiddleware()]) assert len(warnings) == 0 def test_asgi_middleware_exclude_by_opt_key() -> None: class SubclassMiddleware(ASGIMiddleware): exclude_opt_key = "exclude_route" async def handle(self, scope: "Scope", receive: "Receive", send: "Send", next_app: "ASGIApp") -> None: async def _send(message: "Message") -> None: if message["type"] == "http.response.start": headers = MutableScopeHeaders(message) headers.add("test", str(123)) await send(message) await next_app(scope, receive, send) @get("/", exclude_route=True) def handler() -> dict: return {"hello": "world"} with create_test_client(handler, middleware=[SubclassMiddleware()]) as client: response = client.get("/") assert "test" not in response.headers litestar-2.16.0/tests/unit/test_middleware/test_compression_middleware.py000066400000000000000000000264611500564371300271100ustar00rootroot00000000000000import zlib from io import BytesIO from typing import AsyncIterator, Callable, Literal, Union from unittest.mock import MagicMock import pytest from litestar import MediaType, WebSocket, get, websocket from litestar.config.compression import CompressionConfig from litestar.enums import CompressionEncoding from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers import HTTPRouteHandler from litestar.middleware.compression import CompressionMiddleware from litestar.middleware.compression.facade import CompressionFacade from litestar.response.streaming import Stream from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from litestar.types.asgi_types import ASGIApp, HTTPResponseBodyEvent, HTTPResponseStartEvent, Message, Scope BrotliMode = Literal["text", "generic", "font"] @pytest.fixture() def handler() -> HTTPRouteHandler: @get(path="/", media_type=MediaType.TEXT) def handler_fn() -> str: return "_litestar_" * 4000 return handler_fn async def streaming_iter(content: bytes, count: int) -> AsyncIterator[bytes]: for _ in range(count): yield content def test_compression_disabled_for_unsupported_client(handler: HTTPRouteHandler) -> None: with create_test_client(route_handlers=[handler], compression_config=CompressionConfig(backend="brotli")) as client: response = client.get("/", headers={"accept-encoding": "deflate"}) assert response.status_code == HTTP_200_OK assert response.text == "_litestar_" * 4000 assert "Content-Encoding" not in response.headers assert int(response.headers["Content-Length"]) == 40000 @pytest.mark.parametrize( "backend, compression_encoding", (("brotli", CompressionEncoding.BROTLI), ("gzip", CompressionEncoding.GZIP)) ) def test_regular_compressed_response( backend: Literal["gzip", "brotli"], compression_encoding: CompressionEncoding, handler: HTTPRouteHandler ) -> None: with create_test_client(route_handlers=[handler], compression_config=CompressionConfig(backend="brotli")) as client: response = client.get("/", headers={"Accept-Encoding": str(compression_encoding.value)}) assert response.status_code == HTTP_200_OK assert response.text == "_litestar_" * 4000 assert response.headers["Content-Encoding"] == compression_encoding assert int(response.headers["Content-Length"]) < 40000 @pytest.mark.parametrize( "backend, compression_encoding", (("brotli", CompressionEncoding.BROTLI), ("gzip", CompressionEncoding.GZIP)) ) def test_compression_works_for_streaming_response( backend: Literal["gzip", "brotli"], compression_encoding: CompressionEncoding ) -> None: @get("/streaming-response") def streaming_handler() -> Stream: return Stream(streaming_iter(content=b"_litestar_" * 400, count=10)) with create_test_client( route_handlers=[streaming_handler], compression_config=CompressionConfig(backend=backend) ) as client: response = client.get("/streaming-response", headers={"Accept-Encoding": str(compression_encoding.value)}) assert response.status_code == HTTP_200_OK assert response.text == "_litestar_" * 4000 assert response.headers["Content-Encoding"] == compression_encoding assert "Content-Length" not in response.headers @pytest.mark.parametrize( "backend, compression_encoding", (("brotli", CompressionEncoding.BROTLI), ("gzip", CompressionEncoding.GZIP)) ) def test_compression_skips_small_responses( backend: Literal["gzip", "brotli"], compression_encoding: CompressionEncoding ) -> None: @get(path="/no-compression", media_type=MediaType.TEXT) def no_compress_handler() -> str: return "_litestar_" with create_test_client( route_handlers=[no_compress_handler], compression_config=CompressionConfig(backend=backend) ) as client: response = client.get("/no-compression", headers={"Accept-Encoding": str(compression_encoding.value)}) assert response.status_code == HTTP_200_OK assert response.text == "_litestar_" assert "Content-Encoding" not in response.headers assert int(response.headers["Content-Length"]) == 10 def test_brotli_with_gzip_fallback_enabled(handler: HTTPRouteHandler) -> None: with create_test_client( route_handlers=[handler], compression_config=CompressionConfig(backend="brotli", brotli_gzip_fallback=True) ) as client: response = client.get("/", headers={"accept-encoding": CompressionEncoding.GZIP.value}) assert response.status_code == HTTP_200_OK assert response.text == "_litestar_" * 4000 assert response.headers["Content-Encoding"] == CompressionEncoding.GZIP assert int(response.headers["Content-Length"]) < 40000 def test_brotli_gzip_fallback_disabled(handler: HTTPRouteHandler) -> None: with create_test_client( route_handlers=[handler], compression_config=CompressionConfig(backend="brotli", brotli_gzip_fallback=False), ) as client: response = client.get("/", headers={"accept-encoding": "gzip"}) assert response.status_code == HTTP_200_OK assert response.text == "_litestar_" * 4000 assert "Content-Encoding" not in response.headers assert int(response.headers["Content-Length"]) == 40000 async def test_skips_for_websocket() -> None: @websocket("/") async def websocket_handler(socket: WebSocket) -> None: data = await socket.receive_json() await socket.send_json(data) await socket.close() with create_test_client( route_handlers=[websocket_handler], compression_config=CompressionConfig(backend="brotli", brotli_gzip_fallback=False), ).websocket_connect("/") as ws: assert b"content-encoding" not in dict(ws.scope["headers"]) @pytest.mark.parametrize("minimum_size, should_raise", ((0, True), (1, False), (-1, True), (100, False))) def test_config_minimum_size_validation(minimum_size: int, should_raise: bool) -> None: if should_raise: with pytest.raises(ImproperlyConfiguredException): CompressionConfig(backend="brotli", brotli_gzip_fallback=False, minimum_size=minimum_size) else: CompressionConfig(backend="brotli", brotli_gzip_fallback=False, minimum_size=minimum_size) @pytest.mark.parametrize( "gzip_compress_level, should_raise", ((0, False), (1, False), (-1, True), (10, True), (9, False)) ) def test_config_gzip_compress_level_validation(gzip_compress_level: int, should_raise: bool) -> None: if should_raise: with pytest.raises(ImproperlyConfiguredException): CompressionConfig(backend="gzip", brotli_gzip_fallback=False, gzip_compress_level=gzip_compress_level) else: CompressionConfig(backend="gzip", brotli_gzip_fallback=False, gzip_compress_level=gzip_compress_level) @pytest.mark.parametrize("brotli_quality, should_raise", ((0, False), (1, False), (-1, True), (12, True), (11, False))) def test_config_brotli_quality_validation(brotli_quality: int, should_raise: bool) -> None: if should_raise: with pytest.raises(ImproperlyConfiguredException): CompressionConfig(backend="brotli", brotli_gzip_fallback=False, brotli_quality=brotli_quality) else: CompressionConfig(backend="brotli", brotli_gzip_fallback=False, brotli_quality=brotli_quality) @pytest.mark.parametrize("brotli_lgwin, should_raise", ((9, True), (10, False), (-1, True), (25, True), (24, False))) def test_config_brotli_lgwin_validation(brotli_lgwin: int, should_raise: bool) -> None: if should_raise: with pytest.raises(ImproperlyConfiguredException): CompressionConfig(backend="brotli", brotli_gzip_fallback=False, brotli_lgwin=brotli_lgwin) else: CompressionConfig(backend="brotli", brotli_gzip_fallback=False, brotli_lgwin=brotli_lgwin) @pytest.mark.parametrize( "backend, compression_encoding", (("brotli", CompressionEncoding.BROTLI), ("gzip", CompressionEncoding.GZIP)) ) async def test_compression_streaming_response_emitted_messages( backend: Literal["gzip", "brotli"], compression_encoding: Literal[CompressionEncoding.BROTLI, CompressionEncoding.GZIP], create_scope: Callable[..., Scope], mock_asgi_app: ASGIApp, ) -> None: mock = MagicMock() async def fake_send(message: Message) -> None: mock(message) wrapped_send = CompressionMiddleware( mock_asgi_app, CompressionConfig(backend=backend) ).create_compression_send_wrapper(fake_send, compression_encoding, create_scope()) await wrapped_send(HTTPResponseStartEvent(type="http.response.start", status=200, headers={})) # first body message always has compression headers (at least for gzip) await wrapped_send(HTTPResponseBodyEvent(type="http.response.body", body=b"abc", more_body=True)) # second body message with more_body=True will be empty if zlib buffers output and is not flushed await wrapped_send(HTTPResponseBodyEvent(type="http.response.body", body=b"abc", more_body=True)) assert mock.mock_calls[-1].args[0]["body"] # send a more_body=False so resources close properly await wrapped_send(HTTPResponseBodyEvent(type="http.response.body", body=b"", more_body=False)) @pytest.mark.parametrize( "backend, compression_encoding", (("brotli", CompressionEncoding.BROTLI), ("gzip", CompressionEncoding.GZIP)) ) def test_dont_recompress_cached(backend: Literal["gzip", "brotli"], compression_encoding: CompressionEncoding) -> None: mock = MagicMock(return_value="_litestar_" * 4000) @get(path="/", media_type=MediaType.TEXT, cache=True) def handler_fn() -> str: return mock() # type: ignore[no-any-return] with create_test_client( route_handlers=[handler_fn], compression_config=CompressionConfig(backend=backend) ) as client: client.get("/", headers={"Accept-Encoding": str(compression_encoding.value)}) response = client.get("/", headers={"Accept-Encoding": str(compression_encoding.value)}) assert mock.call_count == 1 assert response.status_code == HTTP_200_OK assert response.text == "_litestar_" * 4000 assert response.headers["Content-Encoding"] == compression_encoding assert int(response.headers["Content-Length"]) < 40000 def test_compression_with_custom_backend(handler: HTTPRouteHandler) -> None: class ZlibCompression(CompressionFacade): encoding = "deflate" def __init__( self, buffer: BytesIO, compression_encoding: Union[Literal[CompressionEncoding.GZIP], str], config: CompressionConfig, ) -> None: self.buffer = buffer self.compression_encoding = compression_encoding self.config = config def write(self, body: bytes) -> None: self.buffer.write(zlib.compress(body, level=self.config.backend_config["level"])) def close(self) -> None: ... zlib_config = {"level": 9} config = CompressionConfig(backend="deflate", compression_facade=ZlibCompression, backend_config=zlib_config) with create_test_client([handler], compression_config=config) as client: response = client.get("/", headers={"Accept-Encoding": "deflate"}) assert response.status_code == HTTP_200_OK assert response.text == "_litestar_" * 4000 assert response.headers["Content-Encoding"] == "deflate" assert int(response.headers["Content-Length"]) < 40000 litestar-2.16.0/tests/unit/test_middleware/test_cors_middleware.py000066400000000000000000000126141500564371300255100ustar00rootroot00000000000000from typing import Any, Dict, List, Literal, Mapping, Optional, Union, cast import pytest from litestar import get from litestar.config.cors import CORSConfig from litestar.middleware._internal.cors import CORSMiddleware from litestar.status_codes import HTTP_200_OK, HTTP_404_NOT_FOUND from litestar.testing import create_test_client from litestar.types.asgi_types import Method def test_setting_cors_middleware() -> None: cors_config = CORSConfig() # pyright: ignore assert cors_config.allow_credentials is False assert cors_config.allow_headers == ["*"] assert cors_config.allow_methods == ["*"] assert cors_config.allow_origins == ["*"] assert cors_config.allow_origin_regex is None assert cors_config.max_age == 600 assert cors_config.expose_headers == [] with create_test_client(cors_config=cors_config) as client: unpacked_middleware = [] cur = client.app.asgi_handler while hasattr(cur, "app"): unpacked_middleware.append(cur) cur = cast("Any", cur.app) unpacked_middleware.append(cur) assert len(unpacked_middleware) == 4 cors_middleware = cast("Any", unpacked_middleware[0]) assert isinstance(cors_middleware, CORSMiddleware) assert cors_middleware.config.allow_headers == ["*"] assert cors_middleware.config.allow_methods == ["*"] assert cors_middleware.config.allow_origins == cors_config.allow_origins assert cors_middleware.config.allow_origin_regex == cors_config.allow_origin_regex @pytest.mark.parametrize("origin", [None, "http://www.example.com", "https://moishe.zuchmir.com"]) @pytest.mark.parametrize("allow_origins", ["*", "http://www.example.com", "https://moishe.zuchmir.com"]) @pytest.mark.parametrize("allow_credentials", [True, False]) @pytest.mark.parametrize( "expose_headers", [["x-first-header", "x-second-header", "x-third-header"], ["*"], ["x-first-header"]] ) @pytest.mark.parametrize( "allow_headers", [["x-first-header", "x-second-header", "x-third-header"], ["*"], ["x-first-header"]] ) @pytest.mark.parametrize("allow_methods", [["GET", "POST", "PUT", "DELETE"], ["GET", "POST"], ["GET"]]) def test_cors_simple_response( origin: Optional[str], allow_origins: List[str], allow_credentials: bool, expose_headers: List[str], allow_headers: List[str], allow_methods: List[Union[Literal["*"], "Method"]], ) -> None: @get("/") def handler() -> Dict[str, str]: return {"hello": "world"} cors_config = CORSConfig( allow_origins=allow_origins, allow_credentials=allow_credentials, expose_headers=expose_headers, allow_headers=allow_headers, allow_methods=allow_methods, ) with create_test_client(handler, cors_config=cors_config) as client: headers: Mapping[str, str] = {"Origin": origin} if origin else {} response = client.get("/", headers=headers) assert response.status_code == HTTP_200_OK assert response.json() == {"hello": "world"} assert cors_config.expose_headers == expose_headers assert cors_config.allow_origins == allow_origins assert cors_config.allow_credentials == allow_credentials assert cors_config.allow_headers == allow_headers assert cors_config.allow_methods == allow_methods if origin: if cors_config.is_allow_all_origins: assert response.headers.get("Access-Control-Allow-Origin") == "*" if cors_config.allow_credentials: assert response.headers.get("Access-Control-Allow-Credentials") == "true" if cors_config.expose_headers: assert response.headers.get("Access-Control-Expose-Headers") == ", ".join( sorted(set(cors_config.expose_headers)) ) if cors_config.allow_headers: assert response.headers.get("Access-Control-Allow-Headers") == ", ".join( sorted(set(cors_config.allow_headers)) ) if cors_config.allow_methods: assert response.headers.get("Access-Control-Allow-Methods") == ", ".join( sorted(set(cors_config.allow_methods)) ) else: assert "Access-Control-Allow-Origin" not in response.headers assert "Access-Control-Allow-Credentials" not in response.headers assert "Access-Control-Expose-Headers" not in response.headers assert "Access-Control-Allow-Headers" not in response.headers assert "Access-Control-Allow-Methods" not in response.headers @pytest.mark.parametrize("origin, should_apply_cors", (("http://www.example.com", True), (None, False))) def test_cors_applied_on_exception_response_if_origin_is_present( origin: Optional[str], should_apply_cors: bool ) -> None: @get("/") def handler() -> Dict[str, str]: return {"hello": "world"} cors_config = CORSConfig(allow_origins=["http://www.example.com"]) with create_test_client(handler, cors_config=cors_config) as client: headers: Mapping[str, str] = {"Origin": origin} if origin else {} response = client.get("/abc", headers=headers) assert response.status_code == HTTP_404_NOT_FOUND if should_apply_cors: assert response.headers.get("Access-Control-Allow-Origin") == origin else: assert not response.headers.get("Access-Control-Allow-Origin") litestar-2.16.0/tests/unit/test_middleware/test_csrf_middleware.py000066400000000000000000000241231500564371300254750ustar00rootroot00000000000000import html from os import urandom from pathlib import Path from typing import Any, Optional import pytest from bs4 import BeautifulSoup from litestar import MediaType, WebSocket, delete, get, patch, post, put, websocket from litestar.config.csrf import CSRFConfig from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine from litestar.enums import RequestEncodingType from litestar.handlers import HTTPRouteHandler from litestar.params import Body from litestar.response.template import Template from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_403_FORBIDDEN from litestar.template.config import TemplateConfig from litestar.testing import create_test_client def handler_fn() -> None: pass @pytest.fixture def get_handler() -> HTTPRouteHandler: return get()(handler_fn) @pytest.fixture def post_handler() -> HTTPRouteHandler: return post()(handler_fn) @pytest.fixture def put_handler() -> HTTPRouteHandler: return put()(handler_fn) @pytest.fixture def delete_handler() -> HTTPRouteHandler: return delete()(handler_fn) @pytest.fixture def patch_handler() -> HTTPRouteHandler: return patch()(handler_fn) def test_csrf_successful_flow(get_handler: HTTPRouteHandler, post_handler: HTTPRouteHandler) -> None: with create_test_client( route_handlers=[get_handler, post_handler], csrf_config=CSRFConfig(secret="secret") ) as client: response = client.get("/") assert response.status_code == HTTP_200_OK csrf_token: Optional[str] = response.cookies.get("csrftoken") assert csrf_token is not None set_cookie_header = response.headers.get("set-cookie") assert set_cookie_header is not None assert set_cookie_header.split("; ") == [ f"csrftoken={csrf_token}", "Path=/", "SameSite=lax", ] response = client.post("/", headers={"x-csrftoken": csrf_token}) assert response.status_code == HTTP_201_CREATED @pytest.mark.parametrize( "method", ["POST", "PUT", "DELETE", "PATCH"], ) def test_unsafe_method_fails_without_csrf_header( method: str, get_handler: HTTPRouteHandler, post_handler: HTTPRouteHandler, put_handler: HTTPRouteHandler, delete_handler: HTTPRouteHandler, patch_handler: HTTPRouteHandler, ) -> None: with create_test_client( route_handlers=[get_handler, post_handler, put_handler, delete_handler, patch_handler], csrf_config=CSRFConfig(secret="secret"), ) as client: response = client.get("/") assert response.status_code == HTTP_200_OK csrf_token: Optional[str] = response.cookies.get("csrftoken") assert csrf_token is not None response = client.request(method, "/") assert response.status_code == HTTP_403_FORBIDDEN assert response.json() == {"detail": "CSRF token verification failed", "status_code": 403} def test_invalid_csrf_token(get_handler: HTTPRouteHandler, post_handler: HTTPRouteHandler) -> None: with create_test_client( route_handlers=[get_handler, post_handler], csrf_config=CSRFConfig(secret="secret") ) as client: response = client.get("/") assert response.status_code == HTTP_200_OK csrf_token: Optional[str] = response.cookies.get("csrftoken") assert csrf_token is not None response = client.post("/", headers={"x-csrftoken": f"{csrf_token}invalid"}) assert response.status_code == HTTP_403_FORBIDDEN assert response.json() == {"detail": "CSRF token verification failed", "status_code": 403} def test_csrf_token_too_short(get_handler: HTTPRouteHandler, post_handler: HTTPRouteHandler) -> None: with create_test_client( route_handlers=[get_handler, post_handler], csrf_config=CSRFConfig(secret="secret") ) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert "csrftoken" in response.cookies response = client.post("/", headers={"x-csrftoken": "too-short"}) assert response.status_code == HTTP_403_FORBIDDEN assert response.json() == {"detail": "CSRF token verification failed", "status_code": 403} def test_websocket_ignored() -> None: @websocket(path="/") async def websocket_handler(socket: WebSocket) -> None: await socket.accept() await socket.send_json({"data": "123"}) await socket.close() with create_test_client( route_handlers=[websocket_handler], csrf_config=CSRFConfig(secret="secret") ) as client, client.websocket_connect("/") as ws: response = ws.receive_json() assert response is not None def test_custom_csrf_config(get_handler: HTTPRouteHandler, post_handler: HTTPRouteHandler) -> None: with create_test_client( base_url="http://test.com", route_handlers=[get_handler, post_handler], csrf_config=CSRFConfig( secret="secret", cookie_name="custom-csrftoken", header_name="x-custom-csrftoken", ), ) as client: response = client.get("/") assert response.status_code == HTTP_200_OK csrf_token: Optional[str] = response.cookies.get("custom-csrftoken") assert csrf_token is not None set_cookie_header = response.headers.get("set-cookie") assert set_cookie_header is not None assert set_cookie_header.split("; ") == [ f"custom-csrftoken={csrf_token}", "Path=/", "SameSite=lax", ] response = client.post("/", headers={"x-custom-csrftoken": csrf_token}) assert response.status_code == HTTP_201_CREATED @pytest.mark.parametrize( "engine, template", ( (JinjaTemplateEngine, "{{csrf_input}}"), (MakoTemplateEngine, "${csrf_input}"), ), ) def test_csrf_form_parsing(engine: Any, template: str, tmp_path: Path) -> None: @get(path="/", media_type=MediaType.HTML) def handler() -> Template: return Template(template_name="abc.html") @post("/") def form_handler(data: dict = Body(media_type=RequestEncodingType.URL_ENCODED)) -> dict: return data with create_test_client( route_handlers=[handler, form_handler], template_config=TemplateConfig( directory=tmp_path, engine=engine, ), csrf_config=CSRFConfig(secret=str(urandom(10))), ) as client: url = f"{client.base_url!s}/" Path(tmp_path / "abc.html").write_text( f'
{template}
' ) _ = client.get("/") response = client.get("/") html_soup = BeautifulSoup(html.unescape(response.text), features="html.parser") data = {"_csrf_token": html_soup.body.div.form.input.attrs.get("value")} # type: ignore[union-attr] response = client.post("/", data=data) assert response.status_code == HTTP_201_CREATED assert response.json() == data def test_csrf_middleware_exclude_from_check_via_opts() -> None: @post("/", exclude_from_csrf=True) def post_handler(data: dict = Body(media_type=RequestEncodingType.URL_ENCODED)) -> dict: return data with create_test_client( route_handlers=[post_handler], csrf_config=CSRFConfig(secret=str(urandom(10))), ) as client: data = {"field": "value"} response = client.post("/", data=data) assert response.status_code == HTTP_201_CREATED assert response.json() == data def test_csrf_middleware_exclude_from_check() -> None: @post("/protected-handler") def post_handler(data: dict = Body(media_type=RequestEncodingType.URL_ENCODED)) -> dict: return data @post("/unprotected-handler") def post_handler2(data: dict = Body(media_type=RequestEncodingType.URL_ENCODED)) -> dict: return data with create_test_client( route_handlers=[post_handler, post_handler2], csrf_config=CSRFConfig(secret=str(urandom(10)), exclude=["unprotected-handler"]), ) as client: data = {"field": "value"} response = client.post("/protected-handler", data=data) assert response.status_code == HTTP_403_FORBIDDEN response = client.post("/unprotected-handler", data=data) assert response.status_code == HTTP_201_CREATED assert response.json() == data def test_csrf_middleware_exclude_from_set_cookies() -> None: # https://github.com/litestar-org/litestar/issues/3688 # middleware should be bypassed completely when excluded, so no cookies should be set @get("/protected-handler") def get_handler() -> dict: return {} @get("/unprotected-handler") def get_handler2() -> dict: return {} with create_test_client( route_handlers=[get_handler, get_handler2], csrf_config=CSRFConfig(secret=str(urandom(10)), exclude=["unprotected-handler"]), ) as client: response = client.get("/unprotected-handler") assert response.status_code == HTTP_200_OK assert "set-cookie" not in response.headers response = client.get("/protected-handler") assert response.status_code == HTTP_200_OK assert "set-cookie" in response.headers def test_csrf_middleware_configure_name_for_exclude_from_check_via_opts() -> None: @post("/handler", exclude_from_csrf=True) def post_handler(data: dict = Body(media_type=RequestEncodingType.URL_ENCODED)) -> dict: return data @post("/handler2", custom_exclude_from_csrf=True) def post_handler2(data: dict = Body(media_type=RequestEncodingType.URL_ENCODED)) -> dict: return data with create_test_client( route_handlers=[post_handler, post_handler2], csrf_config=CSRFConfig(secret=str(urandom(10)), exclude_from_csrf_key="custom_exclude_from_csrf"), ) as client: data = {"field": "value"} response = client.post("/handler", data=data) assert response.status_code == HTTP_403_FORBIDDEN data = {"field": "value"} response = client.post("/handler2", data=data) assert response.status_code == HTTP_201_CREATED assert response.json() == data litestar-2.16.0/tests/unit/test_middleware/test_exception_handler_middleware.py000066400000000000000000000365701500564371300302440ustar00rootroot00000000000000from inspect import getinnerframes from typing import TYPE_CHECKING, Any, Callable, Generator, Optional from unittest.mock import MagicMock import pydantic import pytest from pytest_mock import MockerFixture from starlette.exceptions import HTTPException as StarletteHTTPException from structlog.testing import capture_logs from litestar import Litestar, MediaType, Request, Response, get from litestar.exceptions import HTTPException, InternalServerException, LitestarException, ValidationException from litestar.exceptions.responses._debug_response import get_symbol_name from litestar.logging.config import LoggingConfig, StructLoggingConfig from litestar.middleware._internal.exceptions.middleware import ( ExceptionHandlerMiddleware, _starlette_exception_handler, get_exception_handler, ) from litestar.status_codes import HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR from litestar.testing import TestClient, create_test_client from litestar.types import ExceptionHandlersMap from litestar.types.asgi_types import HTTPReceiveMessage, HTTPScope, Message, Receive, Scope, Send from litestar.utils.scope.state import ScopeState from tests.helpers import cleanup_logging_impl if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture from litestar.types import Scope from litestar.types.callable_types import GetLogger async def dummy_app(scope: Any, receive: Any, send: Any) -> None: return None @pytest.fixture(autouse=True) def cleanup_logging() -> Generator: with cleanup_logging_impl(): yield @pytest.fixture() def app() -> Litestar: return Litestar() @pytest.fixture() def middleware() -> ExceptionHandlerMiddleware: return ExceptionHandlerMiddleware(dummy_app, None) @pytest.fixture() def scope(create_scope: Callable[..., HTTPScope], app: Litestar) -> HTTPScope: return create_scope(app=app) def test_default_handle_http_exception_handling_extra_object( scope: HTTPScope, middleware: ExceptionHandlerMiddleware ) -> None: response = middleware.default_http_exception_handler( Request(scope=scope), HTTPException(detail="litestar_exception", extra={"key": "value"}) ) assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert response.content == { "detail": "Internal Server Error", "extra": {"key": "value"}, "status_code": 500, } def test_default_handle_http_exception_handling_extra_none( scope: HTTPScope, middleware: ExceptionHandlerMiddleware ) -> None: response = middleware.default_http_exception_handler( Request(scope=scope), HTTPException(detail="litestar_exception") ) assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert response.content == {"detail": "Internal Server Error", "status_code": 500} def test_default_handle_litestar_http_exception_handling( scope: HTTPScope, middleware: ExceptionHandlerMiddleware ) -> None: response = middleware.default_http_exception_handler( Request(scope=scope), HTTPException(detail="litestar_exception") ) assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert response.content == {"detail": "Internal Server Error", "status_code": 500} def test_default_handle_litestar_http_exception_extra_list( scope: HTTPScope, middleware: ExceptionHandlerMiddleware ) -> None: response = middleware.default_http_exception_handler( Request(scope=scope), HTTPException(detail="litestar_exception", extra=["extra-1", "extra-2"]) ) assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert response.content == { "detail": "Internal Server Error", "extra": ["extra-1", "extra-2"], "status_code": 500, } def test_default_handle_starlette_http_exception_handling( scope: HTTPScope, middleware: ExceptionHandlerMiddleware ) -> None: response = middleware.default_http_exception_handler( Request(scope=scope), StarletteHTTPException(detail="litestar_exception", status_code=HTTP_500_INTERNAL_SERVER_ERROR), ) assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert response.content == {"detail": "Internal Server Error", "status_code": 500} def test_default_handle_python_http_exception_handling( scope: HTTPScope, middleware: ExceptionHandlerMiddleware ) -> None: response = middleware.default_http_exception_handler(Request(scope=scope), AttributeError("oops")) assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert response.content == { "detail": "Internal Server Error", "status_code": HTTP_500_INTERNAL_SERVER_ERROR, } def test_exception_handler_middleware_exception_handlers_mapping() -> None: mock = MagicMock() @get("/") def handler(request: Request) -> None: mock(ScopeState.from_scope(request.scope).exception_handlers) def exception_handler(request: Request, exc: Exception) -> Response: return Response(content={"an": "error"}, status_code=HTTP_500_INTERNAL_SERVER_ERROR) app = Litestar(route_handlers=[handler], exception_handlers={Exception: exception_handler}, openapi_config=None) with TestClient(app) as client: client.get("/") mock.assert_called_once() assert mock.call_args[0][0] == { Exception: exception_handler, StarletteHTTPException: _starlette_exception_handler, } def test_exception_handler_middleware_handler_response_type_encoding( scope: HTTPScope, middleware: ExceptionHandlerMiddleware ) -> None: class ErrorMessage(pydantic.BaseModel): message: str @get("/") def handler(_: Request) -> None: raise Exception def exception_handler(_: Request, _e: Exception) -> Response: return Response(content=ErrorMessage(message="the error message"), status_code=HTTP_500_INTERNAL_SERVER_ERROR) app = Litestar(route_handlers=[handler], exception_handlers={Exception: exception_handler}, openapi_config=None) with TestClient(app) as client: response = client.get("/") assert response.json() == {"message": "the error message"} def test_exception_handler_middleware_handler_response_type_encoding_no_route_handler( scope: HTTPScope, middleware: ExceptionHandlerMiddleware ) -> None: class ErrorMessage(pydantic.BaseModel): message: str @get("/") def handler(_: Request) -> None: raise Exception def exception_handler(_: Request, _e: Exception) -> Response: return Response(content=ErrorMessage(message="the error message"), status_code=HTTP_500_INTERNAL_SERVER_ERROR) app = Litestar(route_handlers=[handler], exception_handlers={Exception: exception_handler}, openapi_config=None) with TestClient(app) as client: response = client.get("/not-found") assert response.json() == {"message": "the error message"} def test_exception_handler_middleware_calls_app_level_after_exception_hook() -> None: @get("/test") def handler() -> None: raise RuntimeError() async def after_exception_hook_handler(exc: Exception, scope: "Scope") -> None: app = scope.get("app") assert isinstance(exc, RuntimeError) assert app assert not app.state.called app.state.called = True with create_test_client(handler, after_exception=[after_exception_hook_handler]) as client: setattr(client.app.state, "called", False) assert not client.app.state.called response = client.get("/test") assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert client.app.state.called @pytest.mark.parametrize( "is_debug, logging_config, should_log", [ (True, LoggingConfig(log_exceptions="debug"), True), (False, LoggingConfig(log_exceptions="debug"), False), (True, LoggingConfig(log_exceptions="always"), True), (False, LoggingConfig(log_exceptions="always"), True), (True, LoggingConfig(log_exceptions="never"), False), (False, LoggingConfig(log_exceptions="never"), False), (True, None, False), (False, None, False), ], ) def test_exception_handler_default_logging( get_logger: "GetLogger", caplog: "LogCaptureFixture", is_debug: bool, logging_config: Optional[LoggingConfig], should_log: bool, ) -> None: @get("/test") def handler() -> None: raise ValueError("Test debug exception") app = Litestar([handler], logging_config=logging_config, debug=is_debug) with caplog.at_level("ERROR", "litestar"), TestClient(app=app) as client: client.app.logger = get_logger("litestar") response = client.get("/test") assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR if is_debug: assert "Test debug exception" in response.text else: assert "Internal Server Error" in response.text if should_log: assert len(caplog.records) == 1 assert caplog.records[0].levelname == "ERROR" assert caplog.records[0].message.startswith("Uncaught exception (connection_type=http, path=/test):") else: assert not caplog.records assert "Uncaught exception" not in response.text @pytest.mark.parametrize( "is_debug, logging_config, should_log", [ (True, StructLoggingConfig(log_exceptions="debug"), True), (False, StructLoggingConfig(log_exceptions="debug"), False), (True, StructLoggingConfig(log_exceptions="always"), True), (False, StructLoggingConfig(log_exceptions="always"), True), (True, StructLoggingConfig(log_exceptions="never"), False), (False, StructLoggingConfig(log_exceptions="never"), False), (True, None, False), (False, None, False), ], ) def test_exception_handler_struct_logging( get_logger: "GetLogger", is_debug: bool, logging_config: Optional[LoggingConfig], should_log: bool, ) -> None: @get("/test") def handler() -> None: raise ValueError("Test debug exception") app = Litestar([handler], logging_config=logging_config, debug=is_debug) with TestClient(app=app) as client, capture_logs() as cap_logs: response = client.get("/test") assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR if is_debug: assert "Test debug exception" in response.text else: assert "Internal Server Error" in response.text if should_log: assert len(cap_logs) == 1 assert cap_logs[0].get("connection_type") == "http" assert cap_logs[0].get("path") == "/test" assert cap_logs[0].get("event") == "Uncaught exception" assert cap_logs[0].get("log_level") == "error" else: assert not cap_logs def handler(_: Any, __: Any) -> Any: return None def handler_2(_: Any, __: Any) -> Any: return None @pytest.mark.parametrize( ["mapping", "exc", "expected"], [ ({}, Exception, None), ({HTTP_400_BAD_REQUEST: handler}, ValidationException(), handler), ({InternalServerException: handler}, InternalServerException(), handler), ({HTTP_500_INTERNAL_SERVER_ERROR: handler}, Exception(), handler), ({TypeError: handler}, TypeError(), handler), ({Exception: handler}, ValidationException(), handler), ({ValueError: handler}, ValidationException(), handler), ({ValidationException: handler}, Exception(), None), ({HTTP_500_INTERNAL_SERVER_ERROR: handler}, ValidationException(), None), ({HTTP_500_INTERNAL_SERVER_ERROR: handler, HTTPException: handler_2}, ValidationException(), handler_2), ({HTTPException: handler, ValidationException: handler_2}, ValidationException(), handler_2), ({HTTPException: handler, ValidationException: handler_2}, InternalServerException(), handler), ({HTTP_500_INTERNAL_SERVER_ERROR: handler, HTTPException: handler_2}, InternalServerException(), handler), ], ) def test_get_exception_handler(mapping: ExceptionHandlersMap, exc: Exception, expected: Any) -> None: assert get_exception_handler(mapping, exc) == expected @pytest.mark.filterwarnings("ignore::litestar.utils.warnings.LitestarWarning:") def test_pdb_on_exception(mocker: MockerFixture) -> None: @get("/test") def handler() -> None: raise ValueError("Test debug exception") mock_post_mortem = mocker.patch("litestar.app.pdb.post_mortem") app = Litestar([handler], pdb_on_exception=True) with TestClient(app=app) as client: response = client.get("/test") assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR mock_post_mortem.assert_called_once() def test_get_debug_from_scope(get_logger: "GetLogger", caplog: "LogCaptureFixture") -> None: @get("/test") def handler() -> None: raise ValueError("Test debug exception") app = Litestar([handler], debug=False) app.debug = True with caplog.at_level("ERROR", "litestar"), TestClient(app=app) as client: client.app.logger = get_logger("litestar") response = client.get("/test") assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert "Test debug exception" in response.text assert len(caplog.records) == 1 assert caplog.records[0].levelname == "ERROR" assert caplog.records[0].message.startswith("Uncaught exception (connection_type=http, path=/test):") def test_get_symbol_name_where_type_doesnt_support_bool() -> None: class Test: def __bool__(self) -> bool: raise TypeError("This type doesn't support bool") def method(self) -> None: raise RuntimeError("Oh no!") exc = None try: Test().method() except Exception as e: exc = e if exc is not None and exc.__traceback__ is not None: frame = getinnerframes(exc.__traceback__, 2)[-1] assert get_symbol_name(frame) == "Test.method" @pytest.mark.parametrize("media_type", list(MediaType)) def test_serialize_custom_types(media_type: MediaType) -> None: # ensure type encoders are passed down to the created response so custom types that # might end up as part of a ValidationException are handled properly # https://github.com/litestar-org/litestar/issues/2867 # https://github.com/litestar-org/litestar/issues/3192 class Foo: def __init__(self, value: str) -> None: self.value = value @get(media_type=media_type) def handler() -> None: raise ValidationException(extra={"foo": Foo("bar")}) with create_test_client([handler], type_encoders={Foo: lambda f: f.value}) as client: res = client.get("/") assert res.json()["extra"] == {"foo": "bar"} async def test_exception_handler_middleware_response_already_started(scope: HTTPScope) -> None: assert not ScopeState.from_scope(scope).response_started async def mock_receive() -> HTTPReceiveMessage: # type: ignore[empty-body] pass mock = MagicMock() async def mock_send(message: Message) -> None: mock(message) start_message: Message = {"type": "http.response.start", "status": 200, "headers": []} async def asgi_app(scope: Scope, receive: Receive, send: Send) -> None: await send(start_message) raise RuntimeError("Test exception") mw = ExceptionHandlerMiddleware(asgi_app, None) with pytest.raises(LitestarException): await mw(scope, mock_receive, mock_send) mock.assert_called_once_with(start_message) assert ScopeState.from_scope(scope).response_started litestar-2.16.0/tests/unit/test_middleware/test_logging_middleware.py000066400000000000000000000276031500564371300261740ustar00rootroot00000000000000from logging import INFO from typing import TYPE_CHECKING, Any, Dict, Generator import pytest from structlog.testing import capture_logs from typing_extensions import Annotated from litestar import Response, get, post from litestar.config.compression import CompressionConfig from litestar.connection import Request from litestar.datastructures import Cookie, UploadFile from litestar.enums import RequestEncodingType from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers import HTTPRouteHandler from litestar.logging.config import LoggingConfig, StructLoggingConfig from litestar.middleware import logging as middleware_logging from litestar.middleware.logging import LoggingMiddlewareConfig from litestar.params import Body from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED from litestar.testing import create_test_client from tests.helpers import cleanup_logging_impl if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture from pytest import MonkeyPatch from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.types.callable_types import GetLogger pytestmark = pytest.mark.usefixtures("reset_httpx_logging") @pytest.fixture(autouse=True) def cleanup_logging() -> Generator: with cleanup_logging_impl(): yield @pytest.fixture def handler() -> HTTPRouteHandler: @get("/") def handler_fn() -> Response: return Response( content={"hello": "world"}, headers={"token": "123", "regular": "abc"}, cookies=[Cookie(key="first-cookie", value="abc"), Cookie(key="second-cookie", value="xxx")], ) return handler_fn def test_logging_middleware_config_validation() -> None: with pytest.raises(ImproperlyConfiguredException): LoggingMiddlewareConfig(response_log_fields=None) # type: ignore[arg-type] with pytest.raises(ImproperlyConfiguredException): LoggingMiddlewareConfig(request_log_fields=None) # type: ignore[arg-type] def test_logging_middleware_regular_logger( get_logger: "GetLogger", caplog: "LogCaptureFixture", handler: HTTPRouteHandler ) -> None: with create_test_client( route_handlers=[handler], middleware=[LoggingMiddlewareConfig().middleware] ) as client, caplog.at_level(INFO): # Set cookies on the client to avoid warnings about per-request cookies. client.app.get_logger = get_logger client.cookies = {"request-cookie": "abc"} # type: ignore[assignment] response = client.get("/", headers={"request-header": "1"}) assert response.status_code == HTTP_200_OK assert len(caplog.messages) == 2 assert ( caplog.messages[0] == 'HTTP Request: path=/, method=GET, content_type=["",{}], ' 'headers={"host":"testserver.local","accept":"*/*","accept-encoding":"gzip, ' 'deflate, br","connection":"keep-alive","user-agent":"testclient",' '"request-header":"1","cookie":"request-cookie=abc"}, ' 'cookies={"request-cookie":"abc"}, query={}, path_params={}, body=None' ) def test_logging_middleware_struct_logger(handler: HTTPRouteHandler) -> None: with create_test_client( route_handlers=[handler], middleware=[LoggingMiddlewareConfig().middleware], logging_config=StructLoggingConfig(), ) as client, capture_logs() as cap_logs: # Set cookies on the client to avoid warnings about per-request cookies. client.cookies = {"request-cookie": "abc"} # type: ignore[assignment] response = client.get("/", headers={"request-header": "1"}) assert response.status_code == HTTP_200_OK assert len(cap_logs) == 2 assert cap_logs[0] == { "path": "/", "method": "GET", "body": None, "content_type": ("", {}), "headers": { "host": "testserver.local", "accept": "*/*", "accept-encoding": "gzip, deflate, br", "connection": "keep-alive", "user-agent": "testclient", "request-header": "1", "cookie": "request-cookie=abc", }, "cookies": {"request-cookie": "abc"}, "query": {}, "path_params": {}, "event": "HTTP Request", "log_level": "info", } assert cap_logs[1] == { "status_code": 200, "cookies": {"first-cookie": "abc", "Path": "/", "SameSite": "lax", "second-cookie": "xxx"}, "headers": {"token": "123", "regular": "abc", "content-length": "17", "content-type": "application/json"}, "body": '{"hello":"world"}', "event": "HTTP Response", "log_level": "info", } def test_logging_middleware_exclude_pattern( get_logger: "GetLogger", caplog: "LogCaptureFixture", handler: HTTPRouteHandler ) -> None: @get("/exclude") def handler2() -> None: return None config = LoggingMiddlewareConfig(exclude=["^/exclude"]) with create_test_client( route_handlers=[handler, handler2], middleware=[config.middleware] ) as client, caplog.at_level(INFO): # Set cookies on the client to avoid warnings about per-request cookies. client.cookies = {"request-cookie": "abc"} # type: ignore[assignment] client.app.get_logger = get_logger response = client.get("/exclude") assert response.status_code == HTTP_200_OK assert len(caplog.messages) == 0 response = client.get("/") assert response.status_code == HTTP_200_OK assert len(caplog.messages) == 2 def test_logging_middleware_exclude_opt_key( get_logger: "GetLogger", caplog: "LogCaptureFixture", handler: HTTPRouteHandler ) -> None: @get("/exclude", skip_logging=True) def handler2() -> None: return None config = LoggingMiddlewareConfig(exclude_opt_key="skip_logging") with create_test_client( route_handlers=[handler, handler2], middleware=[config.middleware] ) as client, caplog.at_level(INFO): # Set cookies on the client to avoid warnings about per-request cookies. client.cookies = {"request-cookie": "abc"} # type: ignore[assignment] client.app.get_logger = get_logger response = client.get("/exclude") assert response.status_code == HTTP_200_OK assert len(caplog.messages) == 0 response = client.get("/") assert response.status_code == HTTP_200_OK assert len(caplog.messages) == 2 @pytest.mark.parametrize("include", [True, False]) def test_logging_middleware_compressed_response_body( get_logger: "GetLogger", include: bool, caplog: "LogCaptureFixture", handler: HTTPRouteHandler ) -> None: with create_test_client( route_handlers=[handler], compression_config=CompressionConfig(backend="gzip", minimum_size=1), middleware=[LoggingMiddlewareConfig(include_compressed_body=include).middleware], ) as client, caplog.at_level(INFO): # Set cookies on the client to avoid warnings about per-request cookies. client.cookies = {"request-cookie": "abc"} # type: ignore[assignment] client.app.get_logger = get_logger response = client.get("/", headers={"request-header": "1"}) assert response.status_code == HTTP_200_OK assert len(caplog.messages) == 2 if include: assert "body=" in caplog.messages[1] else: assert "body=" not in caplog.messages[1] def test_logging_middleware_post_body() -> None: @post("/") def post_handler(data: Dict[str, str]) -> Dict[str, str]: return data with create_test_client( route_handlers=[post_handler], middleware=[LoggingMiddlewareConfig().middleware], logging_config=LoggingConfig() ) as client: res = client.post("/", json={"foo": "bar"}) assert res.status_code == 201 assert res.json() == {"foo": "bar"} async def test_logging_middleware_post_binary_file_without_structlog(monkeypatch: "MonkeyPatch") -> None: # https://github.com/litestar-org/litestar/issues/2529 @post("/") async def post_handler(data: Annotated[UploadFile, Body(media_type=RequestEncodingType.MULTI_PART)]) -> str: content = await data.read() return f"{len(content)} bytes" # force LoggingConfig to not parse body data monkeypatch.setattr(middleware_logging, "structlog_installed", False) with create_test_client( route_handlers=[post_handler], middleware=[LoggingMiddlewareConfig().middleware], logging_config=LoggingConfig() ) as client: res = client.post("/", files={"foo": b"\xfa\xfb"}) assert res.status_code == 201 assert res.text == "2 bytes" @pytest.mark.parametrize("logger_name", ("litestar", "other")) def test_logging_messages_are_not_doubled( get_logger: "GetLogger", logger_name: str, caplog: "LogCaptureFixture" ) -> None: # https://github.com/litestar-org/litestar/issues/896 @get("/") async def hello_world_handler() -> Dict[str, str]: return {"hello": "world"} logging_middleware_config = LoggingMiddlewareConfig(logger_name=logger_name) with create_test_client( hello_world_handler, logging_config=LoggingConfig(), middleware=[logging_middleware_config.middleware], ) as client, caplog.at_level(INFO): client.app.get_logger = get_logger response = client.get("/") assert response.status_code == HTTP_200_OK assert len(caplog.messages) == 2 def test_logging_middleware_log_fields( get_logger: "GetLogger", caplog: "LogCaptureFixture", handler: HTTPRouteHandler ) -> None: with create_test_client( route_handlers=[handler], middleware=[ LoggingMiddlewareConfig(response_log_fields=["status_code"], request_log_fields=["path"]).middleware ], ) as client, caplog.at_level(INFO): # Set cookies on the client to avoid warnings about per-request cookies. client.app.get_logger = get_logger client.cookies = {"request-cookie": "abc"} # type: ignore[assignment] response = client.get("/", headers={"request-header": "1"}) assert response.status_code == HTTP_200_OK assert len(caplog.messages) == 2 assert caplog.messages[0] == "HTTP Request: path=/" assert caplog.messages[1] == "HTTP Response: status_code=200" def test_logging_middleware_with_session_middleware(session_backend_config_memory: "ServerSideSessionConfig") -> None: # https://github.com/litestar-org/litestar/issues/1228 @post("/") async def set_session(request: Request) -> None: request.set_session({"hello": "world"}) @get("/") async def get_session() -> None: pass logging_middleware_config = LoggingMiddlewareConfig() with create_test_client( [set_session, get_session], logging_config=LoggingConfig(), middleware=[logging_middleware_config.middleware, session_backend_config_memory.middleware], ) as client: response = client.post("/") assert response.status_code == HTTP_201_CREATED assert "session" in client.cookies assert client.cookies["session"] != "*****" session_id = client.cookies["session"] response = client.get("/") assert response.status_code == HTTP_200_OK assert "session" in client.cookies assert client.cookies["session"] == session_id def test_structlog_invalid_request_body_handled() -> None: # https://github.com/litestar-org/litestar/issues/3063 @post("/") async def hello_world(data: Dict[str, Any]) -> Dict[str, Any]: return data with create_test_client( route_handlers=[hello_world], logging_config=StructLoggingConfig(log_exceptions="always"), middleware=[LoggingMiddlewareConfig().middleware], ) as client: assert client.post("/", headers={"Content-Type": "application/json"}, content=b'{"a": "b",}').status_code == 400 litestar-2.16.0/tests/unit/test_middleware/test_middleware_handling.py000066400000000000000000000142241500564371300263250ustar00rootroot00000000000000import logging import sys from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, cast import pytest from _pytest.capture import CaptureFixture from _pytest.logging import LogCaptureFixture from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from litestar import Controller, Request, Response, Router, get, post from litestar.enums import ScopeType from litestar.middleware import DefineMiddleware, MiddlewareProtocol from litestar.testing import create_test_client if TYPE_CHECKING: from typing import Type from litestar.types import ASGIApp, Receive, Scope, Send logger = logging.getLogger(__name__) class MiddlewareProtocolRequestLoggingMiddleware(MiddlewareProtocol): def __init__(self, app: "ASGIApp", kwarg: str = "") -> None: self.app = app self.kwarg = kwarg async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: if scope["type"] == ScopeType.HTTP: request = Request[Any, Any, Any](scope=scope, receive=receive) body = await request.json() logger.info(f"test logging: {request.method}, {request.url}, {body}") await self.app(scope, receive, send) class BaseMiddlewareRequestLoggingMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: # type: ignore[explicit-override, override] logging.getLogger(__name__).info("%s - %s", request.method, request.url) return await call_next(request) # type: ignore[arg-type, return-value] class MiddlewareWithArgsAndKwargs(BaseHTTPMiddleware): def __init__(self, arg: int = 0, *, app: Any, kwarg: str) -> None: super().__init__(app) self.arg = arg self.kwarg = kwarg async def dispatch( # type: ignore[empty-body, explicit-override, override] self, request: Request, call_next: Callable[[Request], Awaitable[Response]] ) -> Response: ... @pytest.mark.parametrize( "middleware", [ BaseMiddlewareRequestLoggingMiddleware, # Middleware(MiddlewareWithArgsAndKwargs, kwarg="123Jeronimo"), # pyright: ignore[reportGeneralTypeIssues] # noqa: ERA001 # Middleware(MiddlewareProtocolRequestLoggingMiddleware, kwarg="123Jeronimo"), # type: ignore[arg-type] # pyright: ignore[reportGeneralTypeIssues] # noqa: ERA001 DefineMiddleware(MiddlewareWithArgsAndKwargs, 1, kwarg="123Jeronimo"), # type: ignore[arg-type] DefineMiddleware(MiddlewareProtocolRequestLoggingMiddleware, kwarg="123Jeronimo"), ], ) def test_custom_middleware_processing(middleware: Any) -> None: @get(path="/") def handler() -> None: ... with create_test_client(route_handlers=[handler], middleware=[middleware]) as client: app = client.app assert app.middleware == [middleware] unpacked_middleware = [] cur = client.app.asgi_router.root_route_map_node.children["/"].asgi_handlers["GET"][0] while hasattr(cur, "app"): unpacked_middleware.append(cur) cur = cast("ASGIApp", cur.app) # pyright: ignore unpacked_middleware.append(cur) middleware_instance, *_ = unpacked_middleware assert isinstance( middleware_instance, ( MiddlewareProtocolRequestLoggingMiddleware, BaseMiddlewareRequestLoggingMiddleware, MiddlewareWithArgsAndKwargs, ), ) if isinstance(middleware_instance, (MiddlewareProtocolRequestLoggingMiddleware, MiddlewareWithArgsAndKwargs)): assert middleware_instance.kwarg == "123Jeronimo" if isinstance(middleware, DefineMiddleware) and isinstance(middleware_instance, MiddlewareWithArgsAndKwargs): assert middleware_instance.arg == 1 def test_request_body_logging_middleware(caplog: LogCaptureFixture, capsys: "CaptureFixture[str]") -> None: @dataclass class JSONRequest: name: str age: int programmer: bool @post(path="/") def post_handler(data: JSONRequest) -> JSONRequest: return data if sys.version_info < (3, 13): with caplog.at_level(logging.INFO): client = create_test_client( route_handlers=[post_handler], middleware=[MiddlewareProtocolRequestLoggingMiddleware] ) response = client.post("/", json={"name": "moishe zuchmir", "age": 40, "programmer": True}) assert response.status_code == 201 assert "test logging" in caplog.text else: client = create_test_client( route_handlers=[post_handler], middleware=[MiddlewareProtocolRequestLoggingMiddleware] ) response = client.post("/", json={"name": "moishe zuchmir", "age": 40, "programmer": True}) assert response.status_code == 201 log = capsys.readouterr() assert "test logging" in log.err def test_middleware_call_order() -> None: """Test that middlewares are called in the order they have been passed.""" results: List[int] = [] def create_test_middleware(middleware_id: int) -> "Type[MiddlewareProtocol]": class TestMiddleware(MiddlewareProtocol): def __init__(self, app: "ASGIApp") -> None: self.app = app async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: results.append(middleware_id) await self.app(scope, receive, send) return TestMiddleware class MyController(Controller): path = "/controller" middleware = [create_test_middleware(4), create_test_middleware(5)] @get("/handler", middleware=[create_test_middleware(6), create_test_middleware(7)]) def my_handler(self) -> None: return None router = Router( path="/router", route_handlers=[MyController], middleware=[create_test_middleware(2), create_test_middleware(3)] ) with create_test_client( route_handlers=[router], middleware=[create_test_middleware(0), create_test_middleware(1)], ) as client: client.get("/router/controller/handler") assert results == [0, 1, 2, 3, 4, 5, 6, 7] litestar-2.16.0/tests/unit/test_middleware/test_rate_limit_middleware.py000066400000000000000000000217741500564371300267020ustar00rootroot00000000000000from datetime import datetime from time import time from typing import TYPE_CHECKING, Any import pytest from time_machine import travel from litestar import Litestar, Request, get from litestar.middleware.rate_limit import ( DURATION_VALUES, CacheObject, DurationUnit, RateLimitConfig, ) from litestar.serialization import decode_json, encode_json from litestar.static_files.config import StaticFilesConfig from litestar.status_codes import HTTP_200_OK, HTTP_429_TOO_MANY_REQUESTS from litestar.stores.base import Store from litestar.testing import TestClient, create_test_client if TYPE_CHECKING: from pathlib import Path @pytest.mark.parametrize("unit", ["minute", "second", "hour", "day"]) async def test_rate_limiting(unit: DurationUnit) -> None: @get("/") def handler() -> None: return None config = RateLimitConfig(rate_limit=(unit, 2)) cache_key = "RateLimitMiddleware::testclient" app = Litestar(route_handlers=[handler], middleware=[config.middleware]) store = app.stores.get("rate_limit") with travel(datetime.utcnow, tick=False) as frozen_time, TestClient(app=app) as client: response = client.get("/") cached_value = await store.get(cache_key) assert cached_value cache_object = CacheObject(**decode_json(value=cached_value)) assert len(cache_object.history) == 1 assert response.status_code == HTTP_200_OK assert response.headers.get(config.rate_limit_policy_header_key) == f"2; w={DURATION_VALUES[unit]}" assert response.headers.get(config.rate_limit_limit_header_key) == "2" assert response.headers.get(config.rate_limit_remaining_header_key) == "1" # Since the time is frozen, no time has passed. # Therefore, the remaining seconds for the current quota window should be the same as the entire window length. assert response.headers.get(config.rate_limit_reset_header_key) == str(DURATION_VALUES[unit]) # Move time one second before the end of the quota window for the next request frozen_time.shift(DURATION_VALUES[unit] - 1) response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers.get(config.rate_limit_policy_header_key) == f"2; w={DURATION_VALUES[unit]}" assert response.headers.get(config.rate_limit_limit_header_key) == "2" assert response.headers.get(config.rate_limit_remaining_header_key) == "0" assert response.headers.get(config.rate_limit_reset_header_key) == "1" response = client.get("/") assert response.status_code == HTTP_429_TOO_MANY_REQUESTS assert response.headers.get(config.rate_limit_policy_header_key) == f"2; w={DURATION_VALUES[unit]}" assert response.headers.get(config.rate_limit_limit_header_key) == "2" assert response.headers.get(config.rate_limit_remaining_header_key) == "0" assert response.headers.get(config.rate_limit_reset_header_key) == "1" # Move time one second so that a new quota window starts frozen_time.shift(1) response = client.get("/") assert response.status_code == HTTP_200_OK async def test_non_default_store(memory_store: Store) -> None: @get("/") def handler() -> None: return None app = Litestar( [handler], middleware=[RateLimitConfig(("second", 10)).middleware], stores={"rate_limit": memory_store} ) with TestClient(app) as client: res = client.get("/") assert res.status_code == 200 assert await memory_store.exists("RateLimitMiddleware::testclient") async def test_set_store_name(memory_store: Store) -> None: @get("/") def handler() -> None: return None app = Litestar( [handler], middleware=[RateLimitConfig(("second", 10), store="some_store").middleware], stores={"some_store": memory_store}, ) with TestClient(app) as client: res = client.get("/") assert res.status_code == 200 assert await memory_store.exists("RateLimitMiddleware::testclient") async def test_reset() -> None: @get("/") def handler() -> None: return None config = RateLimitConfig(rate_limit=("second", 1)) cache_key = "RateLimitMiddleware::testclient" app = Litestar(route_handlers=[handler], middleware=[config.middleware]) store = app.stores.get("rate_limit") with TestClient(app=app) as client: response = client.get("/") assert response.status_code == HTTP_200_OK cached_value = await store.get(cache_key) assert cached_value cache_object = CacheObject(**decode_json(value=cached_value)) assert cache_object.reset == int(time() + 1) cache_object.reset -= 2 await store.set(cache_key, encode_json(cache_object)) response = client.get("/") assert response.status_code == HTTP_200_OK @travel(datetime.utcnow, tick=False) def test_exclude_patterns() -> None: @get("/excluded") def handler() -> None: return None @get("/not-excluded") def handler2() -> None: return None config = RateLimitConfig(rate_limit=("second", 1), exclude=["/excluded"]) with create_test_client(route_handlers=[handler, handler2], middleware=[config.middleware]) as client: response = client.get("/excluded") assert response.status_code == HTTP_200_OK response = client.get("/excluded") assert response.status_code == HTTP_200_OK response = client.get("/not-excluded") assert response.status_code == HTTP_200_OK response = client.get("/not-excluded") assert response.status_code == HTTP_429_TOO_MANY_REQUESTS @travel(datetime.utcnow, tick=False) def test_exclude_opt_key() -> None: @get("/excluded", skip_rate_limiting=True) def handler() -> None: return None @get("/not-excluded") def handler2() -> None: return None config = RateLimitConfig(rate_limit=("second", 1), exclude_opt_key="skip_rate_limiting") with create_test_client(route_handlers=[handler, handler2], middleware=[config.middleware]) as client: response = client.get("/excluded") assert response.status_code == HTTP_200_OK response = client.get("/excluded") assert response.status_code == HTTP_200_OK response = client.get("/not-excluded") assert response.status_code == HTTP_200_OK response = client.get("/not-excluded") assert response.status_code == HTTP_429_TOO_MANY_REQUESTS @travel(datetime.utcnow, tick=False) def test_check_throttle_handler() -> None: @get("/path1") def handler1() -> None: return None @get("/path2") def handler2() -> None: return None def check_throttle_handler(request: Request[Any, Any, Any]) -> bool: return request.url.path == "/path1" config = RateLimitConfig(rate_limit=("minute", 1), check_throttle_handler=check_throttle_handler) with create_test_client(route_handlers=[handler1, handler2], middleware=[config.middleware]) as client: response = client.get("/path1") assert response.status_code == HTTP_200_OK response = client.get("/path1") assert response.status_code == HTTP_429_TOO_MANY_REQUESTS response = client.get("/path2") assert response.status_code == HTTP_200_OK response = client.get("/path2") assert response.status_code == HTTP_200_OK @travel(datetime.utcnow, tick=False) async def test_rate_limiting_works_with_mounted_apps(tmpdir: "Path") -> None: # https://github.com/litestar-org/litestar/issues/781 @get("/not-excluded") def handler() -> None: return None path1 = tmpdir / "test.css" path1.write_text("styles content", "utf-8") static_files_config = StaticFilesConfig(directories=[tmpdir], path="/src/static") # pyright: ignore rate_limit_config = RateLimitConfig(rate_limit=("minute", 1), exclude=[r"^/src.*$"]) with create_test_client( [handler], static_files_config=[static_files_config], middleware=[rate_limit_config.middleware] ) as client: response = client.get("/not-excluded") assert response.status_code == HTTP_200_OK response = client.get("/not-excluded") assert response.status_code == HTTP_429_TOO_MANY_REQUESTS response = client.get("/src/static/test.css") assert response.status_code == HTTP_200_OK assert response.text == "styles content" async def test_rate_limiting_works_with_cache() -> None: @get("/", cache=True) def handler() -> None: return None config = RateLimitConfig(rate_limit=("minute", 2)) app = Litestar(route_handlers=[handler], middleware=[config.middleware]) with TestClient(app=app) as client: response = client.get("/") assert response.headers.get(config.rate_limit_remaining_header_key) == "1" response = client.get("/") assert response.headers.get(config.rate_limit_remaining_header_key) == "0" response = client.get("/") assert response.status_code == HTTP_429_TOO_MANY_REQUESTS litestar-2.16.0/tests/unit/test_middleware/test_session/000077500000000000000000000000001500564371300234525ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_middleware/test_session/__init__.py000066400000000000000000000000001500564371300255510ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_middleware/test_session/conftest.py000066400000000000000000000011671500564371300256560ustar00rootroot00000000000000import secrets from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from litestar.middleware.session.client_side import ClientSideSessionBackend @pytest.fixture() def session_test_cookies(cookie_session_backend: "ClientSideSessionBackend") -> str: # Put random data. If you are also handling session management then use session_middleware fixture and create # session cookies with your own data. _session = {"key": secrets.token_hex(16)} return "; ".join( f"session-{i}={serialize.decode('utf-8')}" for i, serialize in enumerate(cookie_session_backend.dump_data(_session)) ) litestar-2.16.0/tests/unit/test_middleware/test_session/test_client_side_backend.py000066400000000000000000000232331500564371300310170ustar00rootroot00000000000000import os import secrets import time from base64 import b64decode, b64encode from typing import Any, Dict from unittest import mock import pytest from cryptography.exceptions import InvalidTag from litestar import Request, get, post from litestar.datastructures.headers import MutableScopeHeaders from litestar.exceptions import ImproperlyConfiguredException from litestar.middleware.session import SessionMiddleware from litestar.middleware.session.client_side import ( AAD, CHUNK_SIZE, ClientSideSessionBackend, CookieBackendConfig, ) from litestar.serialization import encode_json from litestar.testing import RequestFactory, create_test_client from litestar.types.asgi_types import HTTPResponseStartEvent from tests.helpers import randbytes @pytest.mark.parametrize( "secret, should_raise", [ [randbytes(16), False], [randbytes(24), False], [randbytes(32), False], [randbytes(17), True], [randbytes(4), True], [randbytes(100), True], [b"", True], ], ) def test_secret_validation(secret: bytes, should_raise: bool) -> None: if should_raise: with pytest.raises(ImproperlyConfiguredException): CookieBackendConfig(secret=secret) else: CookieBackendConfig(secret=secret) @pytest.mark.parametrize( "key, should_raise", [ ["", True], ["a", False], ["a" * 256, False], ["a" * 257, True], ], ) def test_key_validation(key: str, should_raise: bool) -> None: if should_raise: with pytest.raises(ImproperlyConfiguredException): CookieBackendConfig(secret=os.urandom(16), key=key) else: CookieBackendConfig(secret=os.urandom(16), key=key) @pytest.mark.parametrize( "max_age, should_raise", [ [0, True], [-1, True], [1, False], [100, False], ], ) def test_max_age_validation(max_age: int, should_raise: bool) -> None: if should_raise: with pytest.raises(ImproperlyConfiguredException): CookieBackendConfig(secret=os.urandom(16), key="a", max_age=max_age) else: CookieBackendConfig(secret=os.urandom(16), key="a", max_age=max_age) def create_session(size: int = 16) -> Dict[str, str]: return {"key": secrets.token_hex(size)} @pytest.mark.parametrize("session", [create_session(), create_session(size=4096)]) def test_dump_and_load_data(session: dict, cookie_session_backend: ClientSideSessionBackend) -> None: ciphertext = cookie_session_backend.dump_data(session) assert isinstance(ciphertext, list) for text in ciphertext: assert len(text) <= CHUNK_SIZE plain_text = cookie_session_backend.load_data(ciphertext) assert plain_text == session @mock.patch("time.time", return_value=round(time.time())) def test_load_data_should_return_empty_if_session_expired( time_mock: mock.MagicMock, cookie_session_backend: ClientSideSessionBackend ) -> None: """Should return empty dict if session is expired.""" ciphertext = cookie_session_backend.dump_data(create_session()) time_mock.return_value = round(time.time()) + cookie_session_backend.config.max_age + 1 plaintext = cookie_session_backend.load_data(data=ciphertext) assert plaintext == {} def test_set_session_cookies(cookie_session_backend_config: "CookieBackendConfig") -> None: """Should set session cookies from session in response.""" chunks_multiplier = 2 @get(path="/test") def handler(request: Request) -> None: # Create large session by keeping it multiple of CHUNK_SIZE. This will split the session into multiple cookies. # Then you only need to check if number of cookies set are more than the multiplying number. request.session.update(create_session(size=CHUNK_SIZE * chunks_multiplier)) @get(path="/test_short_cookie") def handler_short_cookie(request: Request) -> None: # Check the naming of a cookie that's short enough to not get broken into chunks request.session.update(create_session()) with create_test_client( route_handlers=[handler], middleware=[cookie_session_backend_config.middleware], ) as client: response = client.get("/test") assert len(response.cookies) > chunks_multiplier assert "session-0" in response.cookies with create_test_client( route_handlers=[handler_short_cookie], middleware=[cookie_session_backend_config.middleware], ) as client: response = client.get("/test_short_cookie") assert len(response.cookies) == 1 assert "session" in response.cookies def test_session_cookie_name_matching(cookie_session_backend_config: "CookieBackendConfig") -> None: session_data = {"foo": "bar"} @get("/") def handler(request: Request) -> Dict[str, Any]: return request.session @post("/") def set_session_data(request: Request) -> None: request.set_session(session_data) with create_test_client( route_handlers=[handler, set_session_data], middleware=[cookie_session_backend_config.middleware], ) as client: client.post("/") client.cookies[f"thisisnnota{cookie_session_backend_config.key}cookie"] = "foo" response = client.get("/") assert response.json() == session_data @pytest.mark.parametrize("mutate", [False, True]) def test_load_session_cookies_and_expire_previous( mutate: bool, cookie_session_middleware: SessionMiddleware[ClientSideSessionBackend] ) -> None: """Should load session cookies into session from request and overwrite the previously set cookies with the upcoming response. Session cookies from the previous session should not persist because session is mutable. Once the session is loaded from the cookies, those cookies are redundant. The response sets new session cookies overwriting or expiring the previous ones. """ # Test for large session data. If it works for multiple cookies, it works for single also. _session = create_session(size=4096) @get(path="/test") def handler(request: Request) -> dict: nonlocal _session if mutate: # Modify the session, this will overwrite the previously set session cookies. request.session.update(create_session()) _session = request.session return request.session ciphertext = cookie_session_middleware.backend.dump_data(_session) with create_test_client( route_handlers=[handler], middleware=[cookie_session_middleware.backend.config.middleware], ) as client: # Set cookies on the client to avoid warnings about per-request cookies. client.cookies = { # type: ignore[assignment] f"{cookie_session_middleware.backend.config.key}-{i}": text.decode("utf-8") for i, text in enumerate(ciphertext) } response = client.get("/test") assert response.json() == _session # The session cookie names that were in the request will also be present in its response to overwrite or to expire # them. So, the number of cookies in the response will be at least equal to or greater than the number of cookies # that were in the request. assert response.headers["set-cookie"].count("session") >= response.request.headers["Cookie"].count("session") def test_load_data_should_raise_invalid_tag_if_tampered_aad(cookie_session_backend: ClientSideSessionBackend) -> None: """If AAD has been tampered with, the integrity of the data cannot be verified and InvalidTag exception is raised. """ encrypted_session = cookie_session_backend.dump_data(create_session()) # The attacker will tamper with the AAD to increase the expiry time of the cookie. attacker_chosen_time = 300 # In seconds fraudulent_associated_data = encode_json( {"expires_at": round(time.time()) + cookie_session_backend.config.max_age + attacker_chosen_time} ) decoded = b64decode(b"".join(encrypted_session)) aad_starts_from = decoded.find(AAD) # The attacker removes the original AAD and attaches its own. ciphertext = b64encode(decoded[:aad_starts_from] + AAD + fraudulent_associated_data) # The attacker puts the data back to its original form. encoded = [ciphertext[i : i + CHUNK_SIZE] for i in range(0, len(ciphertext), CHUNK_SIZE)] with pytest.raises(InvalidTag): cookie_session_backend.load_data(encoded) async def test_store_in_message_clears_cookies_when_session_grows_gt_chunk_size( cookie_session_backend: ClientSideSessionBackend, ) -> None: """Should clear the cookies when the session grows larger than the chunk size.""" # we have a connection that already contains a cookie header with the "session" key in it connection = RequestFactory().get("/", headers={"Cookie": "session=foo"}) # we want to persist a new session that is larger than the chunk size # by the time the encrypted data, nonce and associated data are b64 encoded, the size of # this session will be > 2x larger than the chunk size session = create_session(size=CHUNK_SIZE) message: HTTPResponseStartEvent = {"type": "http.response.start", "status": 200, "headers": []} await cookie_session_backend.store_in_message(session, message, connection) # due to the large session stored in multiple chunks, we now enumerate the name of the cookies # e.g., session-0, session-1, session-2, etc. This means we need to have a cookie with the name # "session" in the response headers that is set to null to clear the original cookie. headers = MutableScopeHeaders.from_message(message) assert len(headers.headers) > 1 header_name, header_content = headers.headers[-1] assert header_name == b"set-cookie" assert header_content.startswith(b"session=null;") litestar-2.16.0/tests/unit/test_middleware/test_session/test_integration.py000066400000000000000000000036631500564371300274160ustar00rootroot00000000000000from dataclasses import dataclass from typing import Any, Dict, Literal, Optional from uuid import UUID from litestar import Request, get, post from litestar.connection import ASGIConnection from litestar.exceptions import NotAuthorizedException from litestar.middleware.session.server_side import ( ServerSideSessionBackend, ServerSideSessionConfig, ) from litestar.security.session_auth import SessionAuth from litestar.status_codes import HTTP_204_NO_CONTENT from litestar.stores.memory import MemoryStore from litestar.testing import create_test_client @dataclass class User: id: UUID name: str email: str @dataclass class UserLoginPayload: email: str password: str MOCK_DB: Dict[str, User] = {} memory_store = MemoryStore() async def retrieve_user_handler( session: Dict[str, Any], connection: "ASGIConnection[Any, Any, Any, Any]" ) -> Optional[User]: return MOCK_DB.get(user_id) if (user_id := session.get("user_id")) else None @post("/login") async def login(data: UserLoginPayload, request: "Request[Any, Any, Any]") -> User: user_id_bytes = await memory_store.get(data.email) if not user_id_bytes: raise NotAuthorizedException user_id = user_id_bytes.decode("utf-8") request.set_session({"user_id": user_id}) return MOCK_DB[user_id] @get("/user", sync_to_thread=False) def get_user(request: Request[User, Dict[Literal["user_id"], str], Any]) -> Any: return request.user session_auth = SessionAuth[User, ServerSideSessionBackend]( retrieve_user_handler=retrieve_user_handler, session_backend_config=ServerSideSessionConfig(), exclude=["/login", "/schema"], ) def test_options_request_with_session_auth() -> None: with create_test_client( route_handlers=[login, get_user], on_app_init=[session_auth.on_app_init], ) as client: response = client.options(get_user.paths.pop()) assert response.status_code == HTTP_204_NO_CONTENT litestar-2.16.0/tests/unit/test_middleware/test_session/test_middleware.py000066400000000000000000000210261500564371300272010ustar00rootroot00000000000000from typing import TYPE_CHECKING, Dict, Optional, Union from litestar import HttpMethod, Request, Response, get, post, route from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR from litestar.testing import create_test_client from litestar.types import Empty if TYPE_CHECKING: from litestar.middleware.session.base import BaseBackendConfig def test_session_middleware_not_installed_raises() -> None: @get("/test") def handler(request: Request) -> None: if request.session: raise AssertionError("this line should not be hit") with create_test_client(handler, debug=False) as client: response = client.get("/test") assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert response.json()["detail"] == "Internal Server Error" def test_integration(session_backend_config: "BaseBackendConfig") -> None: @route("/session", http_method=[HttpMethod.GET, HttpMethod.POST, HttpMethod.DELETE]) def session_handler(request: Request) -> Optional[Dict[str, bool]]: if request.method == HttpMethod.GET: return {"has_session": request.session != {}} if request.method == HttpMethod.DELETE: request.clear_session() else: request.session["username"] = "moishezuchmir" return None with create_test_client(route_handlers=[session_handler], middleware=[session_backend_config.middleware]) as client: response = client.get("/session") assert response.json() == {"has_session": False} first_session_id = client.cookies.get("session") client.post("/session") response = client.get("/session") assert response.json() == {"has_session": True} client.delete("/session") response = client.get("/session") assert response.json() == {"has_session": False} client.post("/session") response = client.get("/session") assert response.json() == {"has_session": True} second_session_id = client.cookies.get("session") assert first_session_id != second_session_id def test_session_id_correctness(session_backend_config: "BaseBackendConfig") -> None: # Test that `request.get_session_id()` is the same as in the cookies @route("/session", http_method=[HttpMethod.POST]) def session_handler(request: Request) -> Optional[Dict[str, Union[str, None]]]: request.set_session({"foo": "bar"}) return {"session_id": request.get_session_id()} with create_test_client(route_handlers=[session_handler], middleware=[session_backend_config.middleware]) as client: if isinstance(session_backend_config, ServerSideSessionConfig): # Generic verification that a session id is set before entering the route handler scope response = client.post("/session") request_session_id = response.json()["session_id"] cookie_session_id = client.cookies.get("session") assert request_session_id == cookie_session_id else: # Client side config does not have a session id in cookies response = client.post("/session") assert response.json()["session_id"] is None assert client.cookies.get("session") is not None response = client.post("/session") assert response.json()["session_id"] is None assert client.cookies.get("session") is not None def test_keep_session_id(session_backend_config: "BaseBackendConfig") -> None: # Test that session is only created if not already exists @route("/session", http_method=[HttpMethod.POST]) def session_handler(request: Request) -> Optional[Dict[str, Union[str, None]]]: request.set_session({"foo": "bar"}) return {"session_id": request.get_session_id()} with create_test_client(route_handlers=[session_handler], middleware=[session_backend_config.middleware]) as client: if isinstance(session_backend_config, ServerSideSessionConfig): # Generic verification that a session id is set before entering the route handler scope response = client.post("/session") first_call_id = response.json()["session_id"] response = client.post("/session") second_call_id = response.json()["session_id"] assert first_call_id == second_call_id == client.cookies.get("session") else: # Client side config does not have a session id in cookies response = client.post("/session") assert response.json()["session_id"] is None assert client.cookies.get("session") is not None response = client.post("/session") assert response.json()["session_id"] is None assert client.cookies.get("session") is not None def test_set_empty(session_backend_config: "BaseBackendConfig") -> None: @post("/create-session") def create_session_handler(request: Request) -> None: request.set_session({"foo": "bar"}) @post("/empty-session") def empty_session_handler(request: Request) -> None: request.set_session(Empty) with create_test_client( route_handlers=[create_session_handler, empty_session_handler], middleware=[session_backend_config.middleware], session_config=session_backend_config, ) as client: client.post("/create-session") client.post("/empty-session") assert not client.get_session_data() def get_session_installed(request: Request) -> Dict[str, bool]: return {"has_session": "session" in request.scope} def test_middleware_exclude_pattern(session_backend_config_memory: "ServerSideSessionConfig") -> None: session_backend_config_memory.exclude = ["north", "south"] @get("/north") def north_handler(request: Request) -> Dict[str, bool]: return get_session_installed(request) @get("/south") def south_handler(request: Request) -> Dict[str, bool]: return get_session_installed(request) @get("/west") def west_handler(request: Request) -> Dict[str, bool]: return get_session_installed(request) with create_test_client( route_handlers=[north_handler, south_handler, west_handler], middleware=[session_backend_config_memory.middleware], ) as client: response = client.get("/north") assert response.json() == {"has_session": False} response = client.get("/south") assert response.json() == {"has_session": False} response = client.get("/west") assert response.json() == {"has_session": True} def test_middleware_exclude_flag(session_backend_config_memory: "ServerSideSessionConfig") -> None: @get("/north") def north_handler(request: Request) -> Dict[str, bool]: return get_session_installed(request) @get("/south", skip_session=True) def south_handler(request: Request) -> Dict[str, bool]: return get_session_installed(request) with create_test_client( route_handlers=[north_handler, south_handler], middleware=[session_backend_config_memory.middleware], ) as client: response = client.get("/north") assert response.json() == {"has_session": True} response = client.get("/south") assert response.json() == {"has_session": False} def test_middleware_exclude_custom_key(session_backend_config_memory: "ServerSideSessionConfig") -> None: session_backend_config_memory.exclude_opt_key = "my_exclude_key" @get("/north") def north_handler(request: Request) -> Dict[str, bool]: return get_session_installed(request) @get("/south", my_exclude_key=True) def south_handler(request: Request) -> Dict[str, bool]: return get_session_installed(request) with create_test_client( route_handlers=[north_handler, south_handler], middleware=[session_backend_config_memory.middleware], ) as client: response = client.get("/north") assert response.json() == {"has_session": True} response = client.get("/south") assert response.json() == {"has_session": False} def test_does_not_override_cookies(session_backend_config_memory: "ServerSideSessionConfig") -> None: # https://github.com/litestar-org/litestar/issues/2033 @get("/") async def index() -> Response[str]: return Response(cookies={"foo": "bar"}, content="hello") with create_test_client(index, middleware=[session_backend_config_memory.middleware]) as client: res = client.get("/") assert res.cookies.get("foo") == "bar" litestar-2.16.0/tests/unit/test_middleware/test_session/test_server_side_backend.py000066400000000000000000000125041500564371300310460ustar00rootroot00000000000000from secrets import token_hex from typing import TYPE_CHECKING import pytest from litestar import Litestar, Request, get from litestar.exceptions import ImproperlyConfiguredException from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.serialization import encode_json from litestar.stores.memory import MemoryStore from litestar.testing import TestClient if TYPE_CHECKING: from time_machine import Coordinates from litestar.middleware.session.server_side import ServerSideSessionBackend def generate_session_data() -> bytes: return encode_json({token_hex(): token_hex()}) @pytest.fixture def session_data() -> bytes: return generate_session_data() async def test_non_default_store(memory_store: MemoryStore) -> None: @get("/") def handler(request: Request) -> None: request.set_session({"foo": "bar"}) return app = Litestar([handler], middleware=[ServerSideSessionConfig().middleware], stores={"sessions": memory_store}) with TestClient(app) as client: res = client.get("/") assert res.status_code == 200 assert await memory_store.exists(res.cookies["session"]) async def test_set_store_name(memory_store: MemoryStore) -> None: @get("/") def handler(request: Request) -> None: request.set_session({"foo": "bar"}) return app = Litestar( [handler], middleware=[ServerSideSessionConfig(store="some_store").middleware], stores={"some_store": memory_store}, ) with TestClient(app) as client: res = client.get("/") assert res.status_code == 200 assert await memory_store.exists(res.cookies["session"]) async def test_get_set( server_side_session_backend: "ServerSideSessionBackend", session_data: bytes, memory_store: MemoryStore ) -> None: await server_side_session_backend.set("foo", session_data, memory_store) loaded = await server_side_session_backend.get("foo", memory_store) assert loaded == session_data async def test_get_renew_on_access( server_side_session_backend: "ServerSideSessionBackend", session_data: bytes, memory_store: MemoryStore, frozen_datetime: "Coordinates", ) -> None: server_side_session_backend.config.max_age = 1 server_side_session_backend.config.renew_on_access = True await server_side_session_backend.set("foo", session_data, memory_store) server_side_session_backend.config.max_age = 10 await server_side_session_backend.get("foo", memory_store) frozen_datetime.shift(1.01) assert await server_side_session_backend.get("foo", memory_store) is not None async def test_get_set_multiple_returns_correct_identity( server_side_session_backend: "ServerSideSessionBackend", memory_store: MemoryStore ) -> None: foo_data = generate_session_data() bar_data = generate_session_data() await server_side_session_backend.set("foo", foo_data, memory_store) await server_side_session_backend.set("bar", bar_data, memory_store) loaded = await server_side_session_backend.get("foo", memory_store) assert loaded == foo_data async def test_delete(server_side_session_backend: "ServerSideSessionBackend", memory_store: MemoryStore) -> None: await server_side_session_backend.set("foo", generate_session_data(), memory_store) await server_side_session_backend.set("bar", generate_session_data(), memory_store) await server_side_session_backend.delete("foo", memory_store) assert not await server_side_session_backend.get("foo", memory_store) assert await server_side_session_backend.get("bar", memory_store) async def test_delete_idempotence( server_side_session_backend: "ServerSideSessionBackend", session_data: bytes, memory_store: MemoryStore ) -> None: await server_side_session_backend.set("foo", session_data, memory_store) await server_side_session_backend.delete("foo", memory_store) await server_side_session_backend.delete("foo", memory_store) # ensure this doesn't raise an error async def test_max_age_expires( server_side_session_backend: "ServerSideSessionBackend", session_data: bytes, memory_store: MemoryStore, frozen_datetime: "Coordinates", ) -> None: server_side_session_backend.config.max_age = 1 await server_side_session_backend.set("foo", session_data, memory_store) frozen_datetime.shift(1) assert not await server_side_session_backend.get("foo", memory_store) @pytest.mark.parametrize( "key, should_raise", [ ["", True], ["a", False], ["a" * 256, False], ["a" * 257, True], ], ) def test_key_validation(server_side_session_backend: "ServerSideSessionBackend", key: str, should_raise: bool) -> None: if should_raise: with pytest.raises(ImproperlyConfiguredException): ServerSideSessionConfig(key=key) else: ServerSideSessionConfig(key=key) @pytest.mark.parametrize( "max_age, should_raise", [ [0, True], [-1, True], [1, False], [100, False], ], ) def test_max_age_validation( server_side_session_backend: "ServerSideSessionBackend", max_age: int, should_raise: bool ) -> None: if should_raise: with pytest.raises(ImproperlyConfiguredException): ServerSideSessionConfig(key="a", max_age=max_age) else: ServerSideSessionConfig(key="a", max_age=max_age) litestar-2.16.0/tests/unit/test_openapi/000077500000000000000000000000001500564371300202465ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_openapi/__init__.py000066400000000000000000000000001500564371300223450ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_openapi/conftest.py000066400000000000000000000132251500564371300224500ustar00rootroot00000000000000from datetime import date, datetime from typing import Any, Dict, List, Optional, Type, Union import pytest from litestar import Controller, MediaType, delete, get, patch, post, put from litestar.datastructures import ResponseHeader, State from litestar.dto import DataclassDTO, DTOConfig, DTOData from litestar.openapi.controller import OpenAPIController from litestar.openapi.spec.example import Example from litestar.params import Parameter from tests.models import DataclassPerson, DataclassPersonFactory, DataclassPet from tests.unit.test_openapi.utils import Gender, LuckyNumber, PetException class PartialDataclassPersonDTO(DataclassDTO[DataclassPerson]): config = DTOConfig(partial=True) def create_person_controller() -> Type[Controller]: class PersonController(Controller): path = "/{service_id:int}/person" @get(sync_to_thread=False) def get_persons( self, # expected to be ignored headers: Any, request: Any, state: State, query: Dict[str, Any], cookies: Dict[str, Any], # required query parameters below page: int, name: Optional[Union[str, List[str]]], # intentionally without default service_id: int, page_size: int = Parameter( query="pageSize", description="Page Size Description", title="Page Size Title", examples=[Example(description="example value", value=1)], ), # path parameter # non-required query parameters below from_date: Optional[Union[int, datetime, date]] = None, to_date: Optional[Union[int, datetime, date]] = None, gender: Optional[Union[Gender, List[Gender]]] = Parameter( examples=[Example(value=Gender.MALE), Example(value=[Gender.MALE, Gender.OTHER])] ), lucky_number: Optional[LuckyNumber] = Parameter(examples=[Example(value=LuckyNumber.SEVEN)]), # header parameter secret_header: str = Parameter(header="secret"), # cookie parameter cookie_value: int = Parameter(cookie="value"), ) -> List[DataclassPerson]: return [] @post(media_type=MediaType.TEXT, sync_to_thread=False) def create_person( self, data: DataclassPerson, secret_header: str = Parameter(header="secret") ) -> DataclassPerson: return data @post(path="/bulk", dto=PartialDataclassPersonDTO, sync_to_thread=False) def bulk_create_person( self, data: DTOData[List[DataclassPerson]], secret_header: str = Parameter(header="secret") ) -> List[DataclassPerson]: return [] @put(path="/bulk", sync_to_thread=False) def bulk_update_person( self, data: List[DataclassPerson], secret_header: str = Parameter(header="secret") ) -> List[DataclassPerson]: return [] @patch(path="/bulk", dto=PartialDataclassPersonDTO, sync_to_thread=False) def bulk_partial_update_person( self, data: DTOData[List[DataclassPerson]], secret_header: str = Parameter(header="secret") ) -> List[DataclassPerson]: return [] @get(path="/{person_id:str}", sync_to_thread=False) def get_person_by_id(self, person_id: str) -> DataclassPerson: """Description in docstring.""" return DataclassPersonFactory.build(id=person_id) @patch( path="/{person_id:str}", description="Description in decorator", dto=PartialDataclassPersonDTO, sync_to_thread=False, ) def partial_update_person(self, person_id: str, data: DTOData[DataclassPerson]) -> DataclassPerson: """Description in docstring.""" return DataclassPersonFactory.build(id=person_id) @put(path="/{person_id:str}", sync_to_thread=False) def update_person(self, person_id: str, data: DataclassPerson) -> DataclassPerson: """Multiline docstring example. Line 3. """ return data @delete(path="/{person_id:str}", sync_to_thread=False) def delete_person(self, person_id: str) -> None: return None @get(path="/dataclass", sync_to_thread=False) def get_person_dataclass(self) -> DataclassPerson: return DataclassPerson( first_name="Moishe", last_name="zuchmir", id="1", optional=None, complex={}, pets=None ) return PersonController def create_pet_controller() -> Type[Controller]: class PetController(Controller): path = "/pet" @get(sync_to_thread=False) def pets(self) -> List[DataclassPet]: return [] @get( path="/owner-or-pet", response_headers=[ResponseHeader(name="x-my-tag", value="123")], raises=[PetException], sync_to_thread=False, ) def get_pets_or_owners(self) -> List[Union[DataclassPerson, DataclassPet]]: return [] return PetController @pytest.fixture def person_controller(disable_warn_implicit_sync_to_thread: None) -> Type[Controller]: """Fixture without a top-level mark.""" return create_person_controller() @pytest.fixture def pet_controller(disable_warn_implicit_sync_to_thread: None) -> Type[Controller]: """Fixture without a top-level mark.""" return create_pet_controller() @pytest.fixture(params=[OpenAPIController, None]) def openapi_controller(request: pytest.FixtureRequest) -> Optional[Type[OpenAPIController]]: return request.param # type: ignore[no-any-return] litestar-2.16.0/tests/unit/test_openapi/test_config.py000066400000000000000000000077711500564371300231400ustar00rootroot00000000000000from typing import TYPE_CHECKING, Any, List, Type import pytest from litestar import Litestar, get from litestar.exceptions import ImproperlyConfiguredException from litestar.openapi.config import OpenAPIConfig from litestar.openapi.controller import OpenAPIController from litestar.openapi.plugins import RedocRenderPlugin, SwaggerRenderPlugin from litestar.openapi.spec import Components, Example, OpenAPIHeader, OpenAPIType, Schema if TYPE_CHECKING: from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.openapi.plugins import OpenAPIRenderPlugin def test_merged_components_correct() -> None: components_one = Components(headers={"one": OpenAPIHeader()}, schemas={"test": Schema(type=OpenAPIType.STRING)}) components_two = Components(headers={"two": OpenAPIHeader()}) components_three = Components(examples={"example-one": Example(summary="an example")}) config = OpenAPIConfig( title="my title", version="1.0.0", components=[components_one, components_two, components_three] ) openapi = config.to_openapi_schema() assert openapi.components assert openapi.components.to_schema() == { "schemas": {"test": {"type": "string"}}, "examples": {"example-one": {"summary": "an example"}}, "headers": { "one": { "required": False, "deprecated": False, }, "two": { "required": False, "deprecated": False, }, }, } def test_allows_customization_of_operation_id_creator() -> None: def operation_id_creator(handler: "HTTPRouteHandler", _: Any, __: Any) -> str: return handler.name or "" @get(path="/1", name="x") def handler_1() -> None: return @get(path="/2", name="y") def handler_2() -> None: return app = Litestar( route_handlers=[handler_1, handler_2], openapi_config=OpenAPIConfig(title="my title", version="1.0.0", operation_id_creator=operation_id_creator), ) assert app.openapi_schema.to_schema()["paths"] == { "/1": { "get": { "deprecated": False, "operationId": "x", "responses": {"200": {"description": "Request fulfilled, document follows", "headers": {}}}, "summary": "Handler1", } }, "/2": { "get": { "deprecated": False, "operationId": "y", "responses": {"200": {"description": "Request fulfilled, document follows", "headers": {}}}, "summary": "Handler2", } }, } def test_allows_customization_of_path() -> None: app = Litestar( openapi_config=OpenAPIConfig( title="my title", version="1.0.0", openapi_controller=OpenAPIController, path="/custom_schema_path" ), ) assert app.openapi_config assert app.openapi_config.path == "/custom_schema_path" assert app.openapi_config.openapi_controller is not None assert app.openapi_config.openapi_controller.path == "/custom_schema_path" def test_raises_exception_when_no_config_in_place() -> None: with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[], openapi_config=None).update_openapi_schema() @pytest.mark.parametrize( ("plugins", "exp"), [ ((), RedocRenderPlugin), ([RedocRenderPlugin()], RedocRenderPlugin), ([SwaggerRenderPlugin(), RedocRenderPlugin()], SwaggerRenderPlugin), ([RedocRenderPlugin(), SwaggerRenderPlugin(path="/")], SwaggerRenderPlugin), ], ) def test_default_plugin(plugins: "List[OpenAPIRenderPlugin]", exp: "Type[OpenAPIRenderPlugin]") -> None: config = OpenAPIConfig(title="my title", version="1.0.0", render_plugins=plugins) assert isinstance(config.default_plugin, exp) def test_default_plugin_legacy() -> None: config = OpenAPIConfig(title="my title", version="1.0.0", openapi_controller=OpenAPIController) assert config.default_plugin is None litestar-2.16.0/tests/unit/test_openapi/test_constrained_fields.py000066400000000000000000000000001500564371300255040ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_openapi/test_datastructures.py000066400000000000000000000121751500564371300247420ustar00rootroot00000000000000from __future__ import annotations from typing import Dict, Generic, List, TypeVar import msgspec import pytest from litestar._openapi.datastructures import SchemaRegistry, _get_normalized_schema_key from litestar.exceptions import ImproperlyConfiguredException from litestar.openapi.spec import Reference, Schema from litestar.params import KwargDefinition from litestar.typing import FieldDefinition from tests.models import DataclassPerson @pytest.fixture() def schema_registry() -> SchemaRegistry: return SchemaRegistry() def test_get_schema_for_field_definition(schema_registry: SchemaRegistry) -> None: assert not schema_registry._schema_key_map assert not schema_registry._schema_reference_map assert not schema_registry._model_name_groups field = FieldDefinition.from_annotation(str) schema = schema_registry.get_schema_for_field_definition(field) key = _get_normalized_schema_key(field) assert isinstance(schema, Schema) assert key in schema_registry._schema_key_map assert not schema_registry._schema_reference_map assert len(schema_registry._model_name_groups[key[-1]]) == 1 assert schema_registry._model_name_groups[key[-1]][0].schema is schema assert schema_registry.get_schema_for_field_definition(field) is schema def test_get_reference_for_field_definition(schema_registry: SchemaRegistry) -> None: assert not schema_registry._schema_key_map assert not schema_registry._schema_reference_map assert not schema_registry._model_name_groups field = FieldDefinition.from_annotation(str) key = _get_normalized_schema_key(field) assert schema_registry.get_reference_for_field_definition(field) is None schema_registry.get_schema_for_field_definition(field) reference = schema_registry.get_reference_for_field_definition(field) assert isinstance(reference, Reference) assert id(reference) in schema_registry._schema_reference_map assert reference in schema_registry._schema_key_map[key].references def test_get_normalized_schema_key() -> None: class LocalClass(msgspec.Struct): id: str T = TypeVar("T") # replace each of the long strings with underscores with a tuple of strings split at each underscore assert _get_normalized_schema_key(FieldDefinition.from_annotation(LocalClass)) == ( "tests", "unit", "test_openapi", "test_datastructures", "test_get_normalized_schema_key.LocalClass", ) assert _get_normalized_schema_key(FieldDefinition.from_annotation(DataclassPerson)) == ( "tests", "models", "DataclassPerson", ) builtin_dict = Dict[str, List[int]] assert _get_normalized_schema_key(FieldDefinition.from_annotation(builtin_dict)) == ( "typing", "Dict_str_typing.List_int_", ) builtin_with_custom = Dict[str, DataclassPerson] assert _get_normalized_schema_key(FieldDefinition.from_annotation(builtin_with_custom)) == ( "typing", "Dict_str_tests.models.DataclassPerson_", ) class LocalGeneric(Generic[T]): pass assert _get_normalized_schema_key(FieldDefinition.from_annotation(LocalGeneric)) == ( "tests", "unit", "test_openapi", "test_datastructures", "test_get_normalized_schema_key.LocalGeneric", ) generic_int = LocalGeneric[int] generic_str = LocalGeneric[str] assert _get_normalized_schema_key(FieldDefinition.from_annotation(generic_int)) == ( "tests", "unit", "test_openapi", "test_datastructures", "test_get_normalized_schema_key.LocalGeneric_int_", ) assert _get_normalized_schema_key(FieldDefinition.from_annotation(generic_str)) == ( "tests", "unit", "test_openapi", "test_datastructures", "test_get_normalized_schema_key.LocalGeneric_str_", ) assert _get_normalized_schema_key(FieldDefinition.from_annotation(generic_int)) != _get_normalized_schema_key( FieldDefinition.from_annotation(generic_str) ) def test_raise_on_override_for_same_field_definition() -> None: registry = SchemaRegistry() schema = registry.get_schema_for_field_definition( FieldDefinition.from_annotation(str, kwarg_definition=KwargDefinition(schema_component_key="foo")) ) # registering the same thing again with the same name should work assert ( registry.get_schema_for_field_definition( FieldDefinition.from_annotation(str, kwarg_definition=KwargDefinition(schema_component_key="foo")) ) is schema ) # registering the same *type* with a different name should result in a different schema assert ( registry.get_schema_for_field_definition( FieldDefinition.from_annotation(str, kwarg_definition=KwargDefinition(schema_component_key="bar")) ) is not schema ) # registering a different type with a previously used name should raise an exception with pytest.raises(ImproperlyConfiguredException): registry.get_schema_for_field_definition( FieldDefinition.from_annotation(int, kwarg_definition=KwargDefinition(schema_component_key="foo")) ) litestar-2.16.0/tests/unit/test_openapi/test_endpoints.py000066400000000000000000000473661500564371300237020ustar00rootroot00000000000000from typing import List, Optional, Type import pytest from litestar import Controller from litestar.enums import MediaType from litestar.openapi.config import OpenAPIConfig from litestar.openapi.controller import OpenAPIController from litestar.openapi.plugins import ( JsonRenderPlugin, OpenAPIRenderPlugin, RapidocRenderPlugin, RedocRenderPlugin, ScalarRenderPlugin, StoplightRenderPlugin, SwaggerRenderPlugin, ) from litestar.status_codes import HTTP_200_OK, HTTP_404_NOT_FOUND from litestar.testing import create_test_client root_paths: List[str] = ["", "/part1", "/part1/part2"] @pytest.fixture() def config(openapi_controller: Optional[Type[OpenAPIController]]) -> OpenAPIConfig: return OpenAPIConfig(title="Litestar API", version="1.0.0", openapi_controller=openapi_controller) def test_default_redoc_cdn_urls( person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig ) -> None: default_redoc_version = "next" default_redoc_js_bundle = f"https://cdn.jsdelivr.net/npm/redoc@{default_redoc_version}/bundles/redoc.standalone.js" with create_test_client([person_controller, pet_controller], openapi_config=config) as client: response = client.get("/schema/redoc") assert default_redoc_js_bundle in response.text def test_default_swagger_ui_cdn_urls( person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig ) -> None: default_swagger_ui_version = "5.18.2" default_swagger_bundles = [ f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{default_swagger_ui_version}/swagger-ui.css", f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{default_swagger_ui_version}/swagger-ui-bundle.js", f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{default_swagger_ui_version}/swagger-ui-standalone-preset.js", ] with create_test_client([person_controller, pet_controller], openapi_config=config) as client: response = client.get("/schema/swagger") assert all(cdn_url in response.text for cdn_url in default_swagger_bundles) def test_default_stoplight_elements_cdn_urls( person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig ) -> None: default_stoplight_elements_version = "7.7.18" default_stoplight_elements_bundles = [ f"https://unpkg.com/@stoplight/elements@{default_stoplight_elements_version}/styles.min.css", f"https://unpkg.com/@stoplight/elements@{default_stoplight_elements_version}/web-components.min.js", ] with create_test_client([person_controller, pet_controller], openapi_config=config) as client: response = client.get("/schema/elements") assert all(cdn_url in response.text for cdn_url in default_stoplight_elements_bundles) def test_default_rapidoc_elements_cdn_urls( person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig ) -> None: default_rapidoc_version = "9.3.4" default_rapidoc_bundles = [f"https://unpkg.com/rapidoc@{default_rapidoc_version}/dist/rapidoc-min.js"] with create_test_client([person_controller, pet_controller], openapi_config=config) as client: response = client.get("/schema/rapidoc") assert all(cdn_url in response.text for cdn_url in default_rapidoc_bundles) def test_redoc_with_google_fonts( person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig ) -> None: google_font_cdn = "https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" with create_test_client([person_controller, pet_controller], openapi_config=config) as client: response = client.get("/schema/redoc") assert google_font_cdn in response.text @pytest.mark.parametrize( ("openapi_controller", "render_plugins"), [ (type("OfflineOpenAPIController", (OpenAPIController,), {"redoc_google_fonts": False}), []), (None, [RedocRenderPlugin(google_fonts=False)]), ], ) def test_redoc_without_google_fonts( person_controller: Type[Controller], pet_controller: Type[Controller], openapi_controller: Optional[Type[OpenAPIController]], render_plugins: List[OpenAPIRenderPlugin], ) -> None: offline_config = OpenAPIConfig( title="Litestar API", version="1.0.0", openapi_controller=openapi_controller, render_plugins=render_plugins ) with create_test_client([person_controller, pet_controller], openapi_config=offline_config) as client: response = client.get("/schema/redoc") assert "fonts.googleapis.com" not in response.text OFFLINE_LOCATION_JS_URL = "https://offline_location/bundle.js" OFFLINE_LOCATION_CSS_URL = "https://offline_location/bundle.css" OFFLINE_LOCATION_OTHER_URL = "https://offline_location/bundle.other" @pytest.mark.parametrize( ("openapi_controller", "render_plugins"), [ (type("OfflineOpenAPIController", (OpenAPIController,), {"redoc_js_url": OFFLINE_LOCATION_JS_URL}), []), (None, [RedocRenderPlugin(js_url=OFFLINE_LOCATION_JS_URL)]), ], ) def test_openapi_redoc_offline( person_controller: Type[Controller], pet_controller: Type[Controller], openapi_controller: Optional[Type[OpenAPIController]], render_plugins: List[OpenAPIRenderPlugin], ) -> None: offline_config = OpenAPIConfig( title="Litestar API", version="1.0.0", openapi_controller=openapi_controller, render_plugins=render_plugins ) with create_test_client([person_controller, pet_controller], openapi_config=offline_config) as client: response = client.get("/schema/redoc") assert OFFLINE_LOCATION_JS_URL in response.text @pytest.mark.parametrize( ("openapi_controller", "render_plugins"), [ ( type( "OfflineOpenAPIController", (OpenAPIController,), { "swagger_ui_bundle_js_url": OFFLINE_LOCATION_JS_URL, "swagger_css_url": OFFLINE_LOCATION_CSS_URL, "swagger_ui_standalone_preset_js_url": OFFLINE_LOCATION_OTHER_URL, }, ), [], ), ( None, [ SwaggerRenderPlugin( js_url=OFFLINE_LOCATION_JS_URL, css_url=OFFLINE_LOCATION_CSS_URL, standalone_preset_js_url=OFFLINE_LOCATION_OTHER_URL, ) ], ), ], ) def test_openapi_swagger_offline( person_controller: Type[Controller], pet_controller: Type[Controller], openapi_controller: Optional[Type[OpenAPIController]], render_plugins: List[OpenAPIRenderPlugin], ) -> None: offline_config = OpenAPIConfig( title="Litestar API", version="1.0.0", openapi_controller=openapi_controller, render_plugins=render_plugins ) with create_test_client([person_controller, pet_controller], openapi_config=offline_config) as client: response = client.get("/schema/swagger") assert all( offline_url in response.text for offline_url in [OFFLINE_LOCATION_JS_URL, OFFLINE_LOCATION_CSS_URL, OFFLINE_LOCATION_OTHER_URL] ) @pytest.mark.parametrize( ("openapi_controller", "render_plugins"), [ ( type( "OfflineOpenAPIController", (OpenAPIController,), { "stoplight_elements_css_url": OFFLINE_LOCATION_CSS_URL, "stoplight_elements_js_url": OFFLINE_LOCATION_JS_URL, }, ), [], ), ( None, [ StoplightRenderPlugin( js_url=OFFLINE_LOCATION_JS_URL, css_url=OFFLINE_LOCATION_CSS_URL, ) ], ), ], ) def test_openapi_stoplight_elements_offline( person_controller: Type[Controller], pet_controller: Type[Controller], openapi_controller: Optional[Type[OpenAPIController]], render_plugins: List[OpenAPIRenderPlugin], ) -> None: offline_config = OpenAPIConfig( title="Litestar API", version="1.0.0", openapi_controller=openapi_controller, render_plugins=render_plugins ) with create_test_client([person_controller, pet_controller], openapi_config=offline_config) as client: response = client.get("/schema/elements") assert all(offline_url in response.text for offline_url in [OFFLINE_LOCATION_JS_URL, OFFLINE_LOCATION_CSS_URL]) @pytest.mark.parametrize( ("openapi_controller", "render_plugins"), [ ( None, [ ScalarRenderPlugin( js_url=OFFLINE_LOCATION_JS_URL, css_url=OFFLINE_LOCATION_CSS_URL, ) ], ), ], ) def test_openapi_scalar_offline( person_controller: Type[Controller], pet_controller: Type[Controller], openapi_controller: Optional[Type[OpenAPIController]], render_plugins: List[OpenAPIRenderPlugin], ) -> None: offline_config = OpenAPIConfig( title="Litestar API", version="1.0.0", openapi_controller=openapi_controller, render_plugins=render_plugins ) with create_test_client([person_controller, pet_controller], openapi_config=offline_config) as client: response = client.get("/schema/scalar") assert all(offline_url in response.text for offline_url in [OFFLINE_LOCATION_JS_URL, OFFLINE_LOCATION_CSS_URL]) @pytest.mark.parametrize( ("openapi_controller", "render_plugins"), [ (type("OfflineOpenAPIController", (OpenAPIController,), {"rapidoc_js_url": OFFLINE_LOCATION_JS_URL}), []), (None, [RapidocRenderPlugin(js_url=OFFLINE_LOCATION_JS_URL)]), ], ) def test_openapi_rapidoc_offline( person_controller: Type[Controller], pet_controller: Type[Controller], openapi_controller: Optional[Type[OpenAPIController]], render_plugins: List[OpenAPIRenderPlugin], ) -> None: offline_config = OpenAPIConfig( title="Litestar API", version="1.0.0", openapi_controller=openapi_controller, render_plugins=render_plugins ) with create_test_client([person_controller, pet_controller], openapi_config=offline_config) as client: response = client.get("/schema/rapidoc") assert OFFLINE_LOCATION_JS_URL in response.text @pytest.mark.parametrize("root_path", root_paths) def test_openapi_root( root_path: str, person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig ) -> None: with create_test_client([person_controller, pet_controller], root_path=root_path, openapi_config=config) as client: response = client.get("/schema") assert response.status_code == HTTP_200_OK assert response.headers["content-type"].startswith(MediaType.HTML.value) @pytest.mark.parametrize("root_path", root_paths) def test_openapi_redoc( root_path: str, person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig ) -> None: with create_test_client([person_controller, pet_controller], root_path=root_path, openapi_config=config) as client: response = client.get("/schema/redoc") assert response.status_code == HTTP_200_OK assert response.headers["content-type"].startswith(MediaType.HTML.value) @pytest.mark.parametrize("root_path", root_paths) def test_openapi_swagger( root_path: str, person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig ) -> None: with create_test_client([person_controller, pet_controller], root_path=root_path, openapi_config=config) as client: response = client.get("/schema/swagger") assert response.status_code == HTTP_200_OK assert response.headers["content-type"].startswith(MediaType.HTML.value) @pytest.mark.parametrize("root_path", root_paths) def test_openapi_swagger_caching_schema( root_path: str, person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig ) -> None: with create_test_client([person_controller, pet_controller], root_path=root_path, openapi_config=config) as client: # Make sure that the schema is tweaked for swagger as the openapi version is changed. # Because schema can get cached, make sure that getting a different schema type before works. client.get("/schema/redoc") # Cache the schema response = client.get("/schema/swagger") # Request swagger, should use a different cache assert "3.1.0" in response.text # Make sure the injected version is still there assert response.status_code == HTTP_200_OK assert response.headers["content-type"].startswith(MediaType.HTML.value) @pytest.mark.parametrize("root_path", root_paths) def test_openapi_stoplight_elements( root_path: str, person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig ) -> None: with create_test_client([person_controller, pet_controller], root_path=root_path, openapi_config=config) as client: response = client.get("/schema/elements/") assert response.status_code == HTTP_200_OK assert response.headers["content-type"].startswith(MediaType.HTML.value) @pytest.mark.parametrize("root_path", root_paths) def test_openapi_rapidoc( root_path: str, person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig ) -> None: with create_test_client([person_controller, pet_controller], root_path=root_path, openapi_config=config) as client: response = client.get("/schema/rapidoc") assert response.status_code == HTTP_200_OK assert response.headers["content-type"].startswith(MediaType.HTML.value) def test_openapi_root_not_allowed( person_controller: Type[Controller], pet_controller: Type[Controller], openapi_controller: Optional[Type[OpenAPIController]], ) -> None: with create_test_client( [person_controller, pet_controller], openapi_config=OpenAPIConfig( title="Litestar API", version="1.0.0", enabled_endpoints={"swagger", "elements", "openapi.json", "openapi.yaml", "openapi.yml"}, openapi_controller=openapi_controller, ), ) as client: response = client.get("/schema") assert response.status_code == HTTP_404_NOT_FOUND assert response.headers["content-type"].startswith(MediaType.HTML.value) def test_openapi_redoc_not_allowed( person_controller: Type[Controller], pet_controller: Type[Controller], openapi_controller: Optional[Type[OpenAPIController]], ) -> None: with create_test_client( [person_controller, pet_controller], openapi_config=OpenAPIConfig( title="Litestar API", version="1.0.0", enabled_endpoints={"swagger", "elements", "openapi.json", "openapi.yaml", "openapi.yml"}, openapi_controller=openapi_controller, ), ) as client: response = client.get("/schema/redoc") assert response.status_code == HTTP_404_NOT_FOUND assert response.headers["content-type"].startswith(MediaType.HTML.value) def test_openapi_swagger_not_allowed( person_controller: Type[Controller], pet_controller: Type[Controller], openapi_controller: Optional[Type[OpenAPIController]], ) -> None: with create_test_client( [person_controller, pet_controller], openapi_config=OpenAPIConfig( title="Litestar API", version="1.0.0", enabled_endpoints={"redoc", "elements", "openapi.json", "openapi.yaml", "openapi.yml"}, openapi_controller=openapi_controller, ), ) as client: response = client.get("/schema/swagger") assert response.status_code == HTTP_404_NOT_FOUND assert response.headers["content-type"].startswith(MediaType.HTML.value) def test_openapi_stoplight_elements_not_allowed( person_controller: Type[Controller], pet_controller: Type[Controller], openapi_controller: Optional[Type[OpenAPIController]], ) -> None: with create_test_client( [person_controller, pet_controller], openapi_config=OpenAPIConfig( title="Litestar API", version="1.0.0", enabled_endpoints={"redoc", "swagger", "openapi.json", "openapi.yaml", "openapi.yml"}, openapi_controller=openapi_controller, ), ) as client: response = client.get("/schema/elements/") assert response.status_code == HTTP_404_NOT_FOUND assert response.headers["content-type"].startswith(MediaType.HTML.value) def test_openapi_rapidoc_not_allowed( person_controller: Type[Controller], pet_controller: Type[Controller], openapi_controller: Optional[Type[OpenAPIController]], ) -> None: with create_test_client( [person_controller, pet_controller], openapi_config=OpenAPIConfig( title="Litestar API", version="1.0.0", enabled_endpoints={"swagger", "elements", "openapi.json", "openapi.yaml", "openapi.yml"}, openapi_controller=openapi_controller, ), ) as client: response = client.get("/schema/rapidoc") assert response.status_code == HTTP_404_NOT_FOUND assert response.headers["content-type"].startswith(MediaType.HTML.value) @pytest.mark.parametrize( ("render_plugins",), [ ([],), ([RedocRenderPlugin()],), ([RedocRenderPlugin(), JsonRenderPlugin()],), ([JsonRenderPlugin(path="/custom_path")],), ([JsonRenderPlugin(path=["/openapi.json", "/custom_path"])],), ], ) def test_json_plugin_always_enabled(render_plugins: List["OpenAPIRenderPlugin"]) -> None: """We assume that an '/openapi.json' path is available in many of the openapi render plugins. This test ensures that the json plugin is always enabled, even if the user has not explicitly included it in the render_plugins list. """ openapi_config = OpenAPIConfig(title="my title", version="1.0.0", render_plugins=render_plugins) with create_test_client([], openapi_config=openapi_config) as client: response = client.get("/schema/openapi.json") assert response.status_code == HTTP_200_OK def test_default_plugin_explicit_path() -> None: config = OpenAPIConfig(title="my title", version="1.0.0", render_plugins=[SwaggerRenderPlugin(path="/")]) with create_test_client([], openapi_config=config) as client: response = client.get("/schema/") assert response.status_code == HTTP_200_OK response = client.get("/schema/swagger") assert response.status_code == HTTP_404_NOT_FOUND def test_default_plugin_backward_compatibility() -> None: config = OpenAPIConfig(title="my title", version="1.0.0") with create_test_client([], openapi_config=config) as client: response = client.get("/schema/") assert response.status_code == HTTP_200_OK response = client.get("/schema/redoc") assert response.status_code == HTTP_200_OK def test_default_plugin_backward_compatibility_not_found() -> None: config = OpenAPIConfig(title="my title", version="1.0.0", enabled_endpoints={"redoc"}, root_schema_site="swagger") with create_test_client([], openapi_config=config) as client: response = client.get("/schema/") assert response.status_code == HTTP_404_NOT_FOUND response = client.get("/schema/swagger") assert response.status_code == HTTP_404_NOT_FOUND response = client.get("/schema/redoc") assert response.status_code == HTTP_200_OK def test_default_plugin_future_compatibility() -> None: config = OpenAPIConfig(title="my title", version="1.0.0", render_plugins=[SwaggerRenderPlugin()]) with create_test_client([], openapi_config=config) as client: response = client.get("/schema/") assert response.status_code == HTTP_200_OK response = client.get("/schema/swagger") assert response.status_code == HTTP_200_OK litestar-2.16.0/tests/unit/test_openapi/test_integration.py000066400000000000000000000513371500564371300242130ustar00rootroot00000000000000from __future__ import annotations import dataclasses from dataclasses import dataclass from types import ModuleType from typing import Callable, Generic, Optional, TypeVar, cast import msgspec import pytest import yaml from typing_extensions import Annotated from litestar import Controller, Litestar, Router, delete, get, patch, post from litestar._openapi.plugin import OpenAPIPlugin from litestar.enums import MediaType, OpenAPIMediaType, ParamType from litestar.openapi import OpenAPIConfig, OpenAPIController from litestar.openapi.spec import Parameter as OpenAPIParameter from litestar.params import Parameter from litestar.serialization.msgspec_hooks import decode_json, encode_json, get_serializer from litestar.status_codes import HTTP_200_OK, HTTP_404_NOT_FOUND from litestar.testing import create_test_client CREATE_EXAMPLES_VALUES = (True, False) @pytest.fixture(params=[True, False]) def create_examples(request: pytest.FixtureRequest) -> bool: return request.param # type: ignore[no-any-return] @pytest.mark.parametrize("schema_path", ["/schema/openapi.yaml", "/schema/openapi.yml"]) def test_openapi( person_controller: type[Controller], pet_controller: type[Controller], create_examples: bool, schema_path: str, openapi_controller: type[OpenAPIController] | None, ) -> None: openapi_config = OpenAPIConfig( "Example API", "1.0.0", create_examples=create_examples, openapi_controller=openapi_controller ) with create_test_client([person_controller, pet_controller], openapi_config=openapi_config) as client: assert client.app.openapi_schema openapi_schema = client.app.openapi_schema assert openapi_schema.paths response = client.get(schema_path) assert response.status_code == HTTP_200_OK assert response.headers["content-type"] == OpenAPIMediaType.OPENAPI_YAML.value assert client.app.openapi_schema serializer = get_serializer(client.app.type_encoders) schema_json = decode_json(encode_json(openapi_schema.to_schema(), serializer)) assert response.content.decode("utf-8") == yaml.dump(schema_json) def test_openapi_json( person_controller: type[Controller], pet_controller: type[Controller], create_examples: bool, openapi_controller: type[OpenAPIController] | None, ) -> None: openapi_config = OpenAPIConfig( "Example API", "1.0.0", create_examples=create_examples, openapi_controller=openapi_controller ) with create_test_client([person_controller, pet_controller], openapi_config=openapi_config) as client: assert client.app.openapi_schema openapi_schema = client.app.openapi_schema assert openapi_schema.paths response = client.get("/schema/openapi.json") assert response.status_code == HTTP_200_OK assert response.headers["content-type"] == OpenAPIMediaType.OPENAPI_JSON.value assert client.app.openapi_schema serializer = get_serializer(client.app.type_encoders) assert response.content == encode_json(openapi_schema.to_schema(), serializer) @pytest.mark.parametrize( "endpoint, schema_path", [("openapi.yaml", "/schema/openapi.yaml"), ("openapi.yml", "/schema/openapi.yml")] ) def test_openapi_yaml_not_allowed( endpoint: str, schema_path: str, person_controller: type[Controller], pet_controller: type[Controller], openapi_controller: type[OpenAPIController] | None, ) -> None: openapi_config = OpenAPIConfig( "Example API", "1.0.0", enabled_endpoints=set(), openapi_controller=openapi_controller ) with create_test_client([person_controller, pet_controller], openapi_config=openapi_config) as client: assert client.app.openapi_schema openapi_schema = client.app.openapi_schema assert openapi_schema.paths response = client.get(schema_path) assert response.status_code == HTTP_404_NOT_FOUND def test_openapi_json_not_allowed(person_controller: type[Controller], pet_controller: type[Controller]) -> None: # only tested with the OpenAPIController, b/c new router based approach always serves `openapi.json`. openapi_config = OpenAPIConfig( "Example API", "1.0.0", enabled_endpoints=set(), openapi_controller=OpenAPIController, ) with create_test_client([person_controller, pet_controller], openapi_config=openapi_config) as client: assert client.app.openapi_schema openapi_schema = client.app.openapi_schema assert openapi_schema.paths response = client.get("/schema/openapi.json") assert response.status_code == HTTP_404_NOT_FOUND @pytest.mark.parametrize( "schema_paths", [ ("/schema/openapi.json", "/schema/openapi.yaml"), ("/schema/openapi.yaml", "/schema/openapi.json"), ], ) def test_openapi_controller_internal_schema_conversion(schema_paths: list[str]) -> None: openapi_config = OpenAPIConfig("Example API", "1.0.0", openapi_controller=OpenAPIController) with create_test_client([], openapi_config=openapi_config) as client: for schema_path in schema_paths: response = client.get(schema_path) assert response.status_code == HTTP_200_OK assert "Example API" in response.text def test_openapi_custom_path(openapi_controller: type[OpenAPIController] | None) -> None: openapi_config = OpenAPIConfig( title="my title", version="1.0.0", path="/custom_schema_path", openapi_controller=openapi_controller ) with create_test_client([], openapi_config=openapi_config) as client: response = client.get("/schema") assert response.status_code == HTTP_404_NOT_FOUND response = client.get("/custom_schema_path") assert response.status_code == HTTP_200_OK response = client.get("/custom_schema_path/openapi.json") assert response.status_code == HTTP_200_OK def test_openapi_normalizes_custom_path(openapi_controller: type[OpenAPIController] | None) -> None: openapi_config = OpenAPIConfig( title="my title", version="1.0.0", path="custom_schema_path", openapi_controller=openapi_controller ) with create_test_client([], openapi_config=openapi_config) as client: response = client.get("/custom_schema_path/openapi.json") assert response.status_code == HTTP_200_OK response = client.get("/custom_schema_path/openapi.json") assert response.status_code == HTTP_200_OK def test_openapi_custom_path_avoids_override() -> None: class CustomOpenAPIController(OpenAPIController): path = "/custom_docs" openapi_config = OpenAPIConfig(title="my title", version="1.0.0", openapi_controller=CustomOpenAPIController) with create_test_client([], openapi_config=openapi_config) as client: response = client.get("/schema") assert response.status_code == HTTP_404_NOT_FOUND response = client.get("/custom_docs/openapi.json") assert response.status_code == HTTP_200_OK response = client.get("/custom_docs/openapi.json") assert response.status_code == HTTP_200_OK def test_openapi_custom_path_overrides_custom_controller_path() -> None: class CustomOpenAPIController(OpenAPIController): path = "/custom_docs" openapi_config = OpenAPIConfig( title="my title", version="1.0.0", openapi_controller=CustomOpenAPIController, path="/override_docs_path" ) with create_test_client([], openapi_config=openapi_config) as client: response = client.get("/custom_docs") assert response.status_code == HTTP_404_NOT_FOUND response = client.get("/override_docs_path/openapi.json") assert response.status_code == HTTP_200_OK response = client.get("/override_docs_path/openapi.json") assert response.status_code == HTTP_200_OK def test_msgspec_schema_generation(create_examples: bool, openapi_controller: type[OpenAPIController] | None) -> None: class Lookup(msgspec.Struct): id: Annotated[ str, msgspec.Meta( min_length=12, max_length=16, description="A unique identifier", examples=["e4eaaaf2-d142-11e1-b3e4-080027620cdd"], ), ] @post("/example") async def example_route() -> Lookup: return Lookup(id="1234567812345678") with create_test_client( route_handlers=[example_route], openapi_config=OpenAPIConfig( title="Example API", version="1.0.0", create_examples=create_examples, openapi_controller=openapi_controller, ), signature_types=[Lookup], ) as client: response = client.get("/schema/openapi.json") assert response.status_code == HTTP_200_OK assert response.json()["components"]["schemas"]["test_msgspec_schema_generation.Lookup"]["properties"][ "id" ] == { "description": "A unique identifier", "examples": ["e4eaaaf2-d142-11e1-b3e4-080027620cdd"], "maxLength": 16, "minLength": 12, "type": "string", } def test_dataclass_field_default() -> None: # https://github.com/litestar-org/litestar/issues/3201 @dataclass class SomeModel: field_a: str = "default_a" field_b: str = dataclasses.field(default="default_b") field_c: str = dataclasses.field(default_factory=lambda: "default_c") @get("/") async def handler() -> SomeModel: return SomeModel() app = Litestar(route_handlers=[handler], signature_types=[SomeModel]) schema = app.openapi_schema.components.schemas["test_dataclass_field_default.SomeModel"] assert schema assert schema.properties["field_a"].default == "default_a" # type: ignore[union-attr, index] assert schema.properties["field_b"].default == "default_b" # type: ignore[union-attr, index] assert schema.properties["field_c"].default is None # type: ignore[union-attr, index] def test_struct_field_default() -> None: # https://github.com/litestar-org/litestar/issues/3201 class SomeModel(msgspec.Struct, kw_only=True): field_a: str = "default_a" field_b: str = msgspec.field(default="default_b") field_c: str = msgspec.field(default_factory=lambda: "default_c") @get("/") async def handler() -> SomeModel: return SomeModel() app = Litestar(route_handlers=[handler], signature_types=[SomeModel]) schema = app.openapi_schema.components.schemas["test_struct_field_default.SomeModel"] assert schema assert schema.properties["field_a"].default == "default_a" # type: ignore[union-attr, index] assert schema.properties["field_b"].default == "default_b" # type: ignore[union-attr, index] assert schema.properties["field_c"].default is None # type: ignore[union-attr, index] def test_schema_for_optional_path_parameter(openapi_controller: type[OpenAPIController] | None) -> None: @get(path=["/", "/{test_message:str}"], media_type=MediaType.TEXT, sync_to_thread=False) def handler(test_message: Optional[str]) -> str: # noqa: UP007 return test_message or "no message" with create_test_client( route_handlers=[handler], openapi_config=OpenAPIConfig( title="Example API", version="1.0.0", create_examples=True, openapi_controller=openapi_controller, ), ) as client: response = client.get("/schema/openapi.json") assert response.status_code == HTTP_200_OK assert "parameters" not in response.json()["paths"]["/"]["get"] # type[ignore] parameter = response.json()["paths"]["/{test_message}"]["get"]["parameters"][0] # type[ignore] assert parameter assert parameter["in"] == ParamType.PATH assert parameter["name"] == "test_message" T = TypeVar("T") @dataclass class Foo(Generic[T]): foo: T def test_with_generic_class(openapi_controller: type[OpenAPIController] | None) -> None: @get("/foo-str", sync_to_thread=False) def handler_foo_str() -> Foo[str]: return Foo("") @get("/foo-int", sync_to_thread=False) def handler_foo_int() -> Foo[int]: return Foo(1) with create_test_client( route_handlers=[handler_foo_str, handler_foo_int], openapi_config=OpenAPIConfig( title="Example API", version="1.0.0", openapi_controller=openapi_controller, ), ) as client: response = client.get("/schema/openapi.json") assert response.status_code == HTTP_200_OK assert response.json() == { "info": {"title": "Example API", "version": "1.0.0"}, "openapi": "3.1.0", "servers": [{"url": "/"}], "paths": { "/foo-str": { "get": { "summary": "HandlerFooStr", "operationId": "FooStrHandlerFooStr", "responses": { "200": { "description": "Request fulfilled, document follows", "headers": {}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Foo_str_"}}}, } }, "deprecated": False, } }, "/foo-int": { "get": { "summary": "HandlerFooInt", "operationId": "FooIntHandlerFooInt", "responses": { "200": { "description": "Request fulfilled, document follows", "headers": {}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Foo_int_"}}}, } }, "deprecated": False, } }, }, "components": { "schemas": { "Foo_str_": { "properties": {"foo": {"type": "string"}}, "type": "object", "required": ["foo"], "title": "Foo[str]", }, "Foo_int_": { "properties": {"foo": {"type": "integer"}}, "type": "object", "required": ["foo"], "title": "Foo[int]", }, } }, } def test_allow_multiple_parameters_with_same_name_but_different_location() -> None: """Test that we can support params with the same name if they are in different locations, e.g., cookie and header. https://github.com/litestar-org/litestar/issues/2662 """ @post("/test") async def route( name: Annotated[Optional[str], Parameter(cookie="name")] = None, # noqa: UP007 name_header: Annotated[Optional[str], Parameter(header="name")] = None, # noqa: UP007 ) -> str: return name or name_header or "" app = Litestar(route_handlers=[route], debug=True) assert app.openapi_schema.paths is not None schema = app.openapi_schema paths = schema.paths assert paths is not None path = paths["/test"] assert path.post is not None parameters = path.post.parameters assert parameters is not None assert len(parameters) == 2 assert all(isinstance(param, OpenAPIParameter) for param in parameters) params = cast("list[OpenAPIParameter]", parameters) assert all(param.name == "name" for param in params) assert tuple(param.param_in for param in params) == ("cookie", "header") def test_schema_name_collisions(create_module: Callable[[str], ModuleType]) -> None: module_a = create_module( """ from dataclasses import dataclass @dataclass class Model: a: str """ ) module_b = create_module( """ from dataclasses import dataclass @dataclass class Model: b: str """ ) @get("/foo", sync_to_thread=False, signature_namespace={"module_a": module_a}) def handler_a() -> module_a.Model: # type: ignore[name-defined] return module_a.Model(a="") @get("/bar", sync_to_thread=False, signature_namespace={"module_b": module_b}) def handler_b() -> module_b.Model: # type: ignore[name-defined] return module_b.Model(b="") app = Litestar(route_handlers=[handler_a, handler_b], debug=True) openapi_plugin = app.plugins.get(OpenAPIPlugin) assert openapi_plugin.provide_openapi().components.schemas.keys() == { f"{module_a.__name__}_Model", f"{module_b.__name__}_Model", } # TODO: expand this test to cover more cases def test_multiple_handlers_for_same_route() -> None: @post("/", sync_to_thread=False) def post_handler() -> None: ... @get("/", sync_to_thread=False) def get_handler() -> None: ... app = Litestar([get_handler, post_handler]) openapi_plugin = app.plugins.get(OpenAPIPlugin) openapi = openapi_plugin.provide_openapi() assert openapi.paths is not None path_item = openapi.paths["/"] assert path_item.get is not None assert path_item.post is not None @pytest.mark.parametrize(("random_seed_one", "random_seed_two", "should_be_equal"), [(10, 10, True), (10, 20, False)]) def test_seeding(random_seed_one: int, random_seed_two: int, should_be_equal: bool) -> None: @post("/", sync_to_thread=False) def post_handler(q: str) -> None: ... @get("/", sync_to_thread=False) def get_handler(q: str) -> None: ... app = Litestar( [get_handler, post_handler], openapi_config=OpenAPIConfig("Litestar", "v0.0.1", True, random_seed_one) ) openapi_plugin = app.plugins.get(OpenAPIPlugin) openapi_one = openapi_plugin.provide_openapi() app = Litestar( [get_handler, post_handler], openapi_config=OpenAPIConfig("Litestar", "v0.0.1", True, random_seed_two) ) openapi_plugin = app.plugins.get(OpenAPIPlugin) openapi_two = openapi_plugin.provide_openapi() if should_be_equal: assert openapi_one == openapi_two else: assert openapi_one != openapi_two def test_components_schemas_in_alphabetical_order() -> None: # https://github.com/litestar-org/litestar/issues/3059 @dataclass class A: ... @dataclass class B: ... @dataclass class C: ... class TestController(Controller): @post("/", sync_to_thread=False) def post_handler(self, data: B) -> None: ... @get("/", sync_to_thread=False) def get_handler(self) -> A: # type: ignore[empty-body] ... @patch("/", sync_to_thread=False) def patch_handler(self, data: C) -> A: # type: ignore[empty-body] ... @delete("/", sync_to_thread=False) def delete_handler(self, data: B) -> None: ... app = Litestar([TestController], signature_types=[A, B, C]) openapi_plugin = app.plugins.get(OpenAPIPlugin) openapi = openapi_plugin.provide_openapi() expected_keys = [ "test_components_schemas_in_alphabetical_order.A", "test_components_schemas_in_alphabetical_order.B", "test_components_schemas_in_alphabetical_order.C", ] assert list(openapi.components.schemas.keys()) == expected_keys def test_openapi_controller_and_openapi_router_on_same_app() -> None: """Test that OpenAPIController and OpenAPIRouter can coexist on the same app. As part of backward compatibility with new plugin-based OpenAPI router approach, we did not consider the case where an OpenAPIController is registered on the application by means other than via the OpenAPIConfig object. This is an approach that has been used to serve the openapi both under the `/schema` and `/some-prefix/schema` paths. This test ensures that the OpenAPIController and OpenAPIRouter can coexist on the same app. See: https://github.com/litestar-org/litestar/issues/3337 """ router = Router(path="/abc", route_handlers=[OpenAPIController]) openapi_config = OpenAPIConfig("Litestar", "v0.0.1") # no openapi_controller specified means we use the router app = Litestar([router], openapi_config=openapi_config) assert sorted(r.path for r in app.routes) == [ "/abc/schema", "/abc/schema/elements", "/abc/schema/oauth2-redirect.html", "/abc/schema/openapi.json", "/abc/schema/openapi.yaml", "/abc/schema/openapi.yml", "/abc/schema/rapidoc", "/abc/schema/redoc", "/abc/schema/swagger", "/schema", "/schema/elements", "/schema/oauth2-redirect.html", "/schema/openapi.json", "/schema/openapi.yaml", "/schema/openapi.yml", "/schema/rapidoc", "/schema/redoc", "/schema/swagger", "/schema/{path:str}", ] litestar-2.16.0/tests/unit/test_openapi/test_parameters.py000066400000000000000000000420231500564371300240230ustar00rootroot00000000000000import dataclasses from typing import TYPE_CHECKING, Any, List, Optional, Type, cast from uuid import UUID import pytest from typing_extensions import Annotated, NewType from litestar import Controller, Litestar, Router, get from litestar._openapi.datastructures import OpenAPIContext from litestar._openapi.parameters import ParameterFactory from litestar._openapi.schema_generation.examples import ExampleFactory from litestar._openapi.typescript_converter.schema_parsing import is_schema_value from litestar.di import Provide from litestar.enums import ParamType from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers import HTTPRouteHandler from litestar.openapi import OpenAPIConfig from litestar.openapi.spec import Example, OpenAPI, Reference, Schema from litestar.openapi.spec.enums import OpenAPIType from litestar.params import Dependency, Parameter from litestar.routes import BaseRoute from litestar.testing import create_test_client from litestar.utils import find_index from tests.unit.test_openapi.utils import Gender, LuckyNumber if TYPE_CHECKING: from litestar.openapi.spec.parameter import Parameter as OpenAPIParameter def create_factory(route: BaseRoute, handler: HTTPRouteHandler) -> ParameterFactory: return ParameterFactory( OpenAPIContext( openapi_config=OpenAPIConfig(title="Test API", version="1.0.0", create_examples=True), plugins=[] ), route_handler=handler, path_parameters=route.path_parameters, ) def _create_parameters(app: Litestar, path: str) -> List["OpenAPIParameter"]: index = find_index(app.routes, lambda x: x.path_format == path) route = app.routes[index] route_handler = route.route_handler_map["GET"][0] # type: ignore[union-attr] handler = route_handler.fn assert callable(handler) return create_factory(route, route_handler).create_parameters_for_handler() def test_create_parameters(person_controller: Type[Controller]) -> None: ExampleFactory.seed_random(10) parameters = _create_parameters(app=Litestar(route_handlers=[person_controller]), path="/{service_id}/person") assert len(parameters) == 10 page, name, service_id, page_size, from_date, to_date, gender, lucky_number, secret_header, cookie_value = tuple( parameters ) assert service_id.name == "service_id" assert service_id.param_in == ParamType.PATH assert is_schema_value(service_id.schema) assert service_id.schema.type == OpenAPIType.INTEGER assert service_id.required assert service_id.schema.examples assert page.param_in == ParamType.QUERY assert page.name == "page" assert is_schema_value(page.schema) assert page.schema.type == OpenAPIType.INTEGER assert page.required assert page.schema.examples assert page_size.param_in == ParamType.QUERY assert page_size.name == "pageSize" assert is_schema_value(page_size.schema) assert page_size.schema.type == OpenAPIType.INTEGER assert page_size.required assert page_size.description == "Page Size Description" assert page_size.examples assert page_size.schema.examples == [1] assert name.param_in == ParamType.QUERY assert name.name == "name" assert is_schema_value(name.schema) assert name.schema.one_of assert len(name.schema.one_of) == 3 assert not name.required assert name.schema.examples assert from_date.param_in == ParamType.QUERY assert from_date.name == "from_date" assert is_schema_value(from_date.schema) assert from_date.schema.one_of assert len(from_date.schema.one_of) == 4 assert not from_date.required assert from_date.schema.examples assert to_date.param_in == ParamType.QUERY assert to_date.name == "to_date" assert is_schema_value(to_date.schema) assert to_date.schema.one_of assert len(to_date.schema.one_of) == 4 assert not to_date.required assert to_date.schema.examples assert gender.param_in == ParamType.QUERY assert gender.name == "gender" assert is_schema_value(gender.schema) assert gender.schema == Schema( one_of=[ Reference(ref="#/components/schemas/tests_unit_test_openapi_utils_Gender"), Schema( type=OpenAPIType.ARRAY, items=Reference(ref="#/components/schemas/tests_unit_test_openapi_utils_Gender"), examples=[[Gender.MALE]], ), Schema(type=OpenAPIType.NULL), ], examples=[Gender.MALE, [Gender.MALE, Gender.OTHER]], ) assert not gender.required assert secret_header.param_in == ParamType.HEADER assert is_schema_value(secret_header.schema) assert secret_header.schema.type == OpenAPIType.STRING assert secret_header.required assert secret_header.schema.examples assert cookie_value.param_in == ParamType.COOKIE assert is_schema_value(cookie_value.schema) assert cookie_value.schema.type == OpenAPIType.INTEGER assert cookie_value.required assert cookie_value.schema.examples assert lucky_number.param_in == ParamType.QUERY assert lucky_number.name == "lucky_number" assert is_schema_value(lucky_number.schema) assert lucky_number.schema == Schema( one_of=[ Reference(ref="#/components/schemas/tests_unit_test_openapi_utils_LuckyNumber"), Schema(type=OpenAPIType.NULL), ], examples=[LuckyNumber.SEVEN], ) assert not lucky_number.required def test_deduplication_for_param_where_key_and_type_are_equal() -> None: class BaseDep: def __init__(self, query_param: str) -> None: ... class ADep(BaseDep): ... class BDep(BaseDep): ... async def c_dep(other_param: float) -> float: return other_param async def d_dep(other_param: float) -> float: return other_param @get( "/test", dependencies={ "a": Provide(ADep, sync_to_thread=False), "b": Provide(BDep, sync_to_thread=False), "c": Provide(c_dep), "d": Provide(d_dep), }, ) def handler(a: ADep, b: BDep, c: float, d: float) -> str: return "OK" app = Litestar(route_handlers=[handler]) assert isinstance(app.openapi_schema, OpenAPI) open_api_path_item = app.openapi_schema.paths["/test"] # type: ignore[index] open_api_parameters = open_api_path_item.get.parameters # type: ignore[union-attr] assert len(open_api_parameters) == 2 # type: ignore[arg-type] assert {p.name for p in open_api_parameters} == {"query_param", "other_param"} # type: ignore[union-attr] def test_raise_for_multiple_parameters_of_same_name_and_differing_types() -> None: async def a_dep(query_param: int) -> int: return query_param async def b_dep(query_param: str) -> int: return 1 @get("/test", dependencies={"a": Provide(a_dep), "b": Provide(b_dep)}) def handler(a: int, b: int) -> str: return "OK" app = Litestar(route_handlers=[handler]) with pytest.raises(ImproperlyConfiguredException): app.openapi_schema def test_dependency_params_in_docs_if_dependency_provided() -> None: async def produce_dep(param: str) -> int: return 13 @get(dependencies={"dep": Provide(produce_dep)}) def handler(dep: Optional[int] = Dependency()) -> None: return None app = Litestar(route_handlers=[handler]) param_name_set = {p.name for p in cast("OpenAPI", app.openapi_schema).paths["/"].get.parameters} # type: ignore[index, redundant-cast, union-attr] assert "dep" not in param_name_set assert "param" in param_name_set def test_dependency_not_in_doc_params_if_not_provided() -> None: @get() def handler(dep: Optional[int] = Dependency()) -> None: return None app = Litestar(route_handlers=[handler]) assert cast("OpenAPI", app.openapi_schema).paths["/"].get.parameters is None # type: ignore[index, redundant-cast, union-attr] def test_non_dependency_in_doc_params_if_not_provided() -> None: @get() def handler(param: Optional[int]) -> None: return None app = Litestar(route_handlers=[handler]) param_name_set = {p.name for p in cast("OpenAPI", app.openapi_schema).paths["/"].get.parameters} # type: ignore[index, redundant-cast, union-attr] assert "param" in param_name_set def test_layered_parameters() -> None: class MyController(Controller): path = "/controller" parameters = { "controller1": Parameter(lt=100), "controller2": Parameter(str, query="controller3"), } @get("/{local:int}") def my_handler( self, local: int, controller1: int, router1: str, router2: float, app1: str, app2: List[str], controller2: float = Parameter(float, ge=5.0), ) -> dict: return {} router = Router( path="/router", route_handlers=[MyController], parameters={ "router1": Parameter(str, pattern="^[a-zA-Z]$"), "router2": Parameter(float, multiple_of=5.0, header="router3"), }, ) parameters = _create_parameters( app=Litestar( route_handlers=[router], parameters={ "app1": Parameter(str, cookie="app4"), "app2": Parameter(List[str], min_items=2), "app3": Parameter(bool, required=False), }, ), path="/router/controller/{local}", ) local, app3, controller1, router1, router3, app4, app2, controller3 = tuple(parameters) assert app4.param_in == ParamType.COOKIE assert app4.schema.type == OpenAPIType.STRING # type: ignore[union-attr] assert app4.required assert app4.schema.examples # type: ignore[union-attr] assert app2.param_in == ParamType.QUERY assert app2.schema.type == OpenAPIType.ARRAY # type: ignore[union-attr] assert app2.required assert app2.schema.examples # type: ignore[union-attr] assert app3.param_in == ParamType.QUERY assert app3.schema.type == OpenAPIType.BOOLEAN # type: ignore[union-attr] assert not app3.required assert app3.schema.examples # type: ignore[union-attr] assert router1.param_in == ParamType.QUERY assert router1.schema.type == OpenAPIType.STRING # type: ignore[union-attr] assert router1.required assert router1.schema.pattern == "^[a-zA-Z]$" # type: ignore[union-attr] assert router1.schema.examples # type: ignore[union-attr] assert router3.param_in == ParamType.HEADER assert router3.schema.type == OpenAPIType.NUMBER # type: ignore[union-attr] assert router3.required assert router3.schema.multiple_of == 5.0 # type: ignore[union-attr] assert router3.schema.examples # type: ignore[union-attr] assert controller1.param_in == ParamType.QUERY assert controller1.schema.type == OpenAPIType.INTEGER # type: ignore[union-attr] assert controller1.required assert controller1.schema.exclusive_maximum == 100.0 # type: ignore[union-attr] assert controller1.schema.examples # type: ignore[union-attr] assert controller3.param_in == ParamType.QUERY assert controller3.schema.type == OpenAPIType.NUMBER # type: ignore[union-attr] assert controller3.required assert controller3.schema.minimum == 5.0 # type: ignore[union-attr] assert controller3.schema.examples # type: ignore[union-attr] assert local.param_in == ParamType.PATH assert local.schema.type == OpenAPIType.INTEGER # type: ignore[union-attr] assert local.required assert local.schema.examples # type: ignore[union-attr] def test_parameter_examples() -> None: @get(path="/") async def index( text: Annotated[str, Parameter(examples=[Example(value="example value", summary="example summary")])], ) -> str: return text with create_test_client( route_handlers=[index], openapi_config=OpenAPIConfig(title="Test API", version="1.0.0") ) as client: response = client.get("/schema/openapi.json") assert response.json()["paths"]["/"]["get"]["parameters"][0]["examples"] == { "text-example-1": {"summary": "example summary", "value": "example value"} } def test_parameter_schema_extra() -> None: @get() async def handler( query1: Annotated[ str, Parameter( schema_extra={ "schema_not": Schema( any_of=[ Schema(type=OpenAPIType.STRING, pattern=r"^somePrefix:.*$"), Schema(type=OpenAPIType.STRING, enum=["denied", "values"]), ] ), } ), ], ) -> Any: return query1 @get() async def error_handler(query1: Annotated[str, Parameter(schema_extra={"invalid": "dummy"})]) -> Any: return query1 # Success app = Litestar([handler]) schema = app.openapi_schema.to_schema() assert schema["paths"]["/"]["get"]["parameters"][0]["schema"]["not"] == { "anyOf": [ {"type": "string", "pattern": r"^somePrefix:.*$"}, {"type": "string", "enum": ["denied", "values"]}, ] } # Attempt to pass invalid key app = Litestar([error_handler]) with pytest.raises(ValueError) as e: app.openapi_schema assert str(e.value).startswith("`schema_extra` declares key") def test_uuid_path_description_generation() -> None: # https://github.com/litestar-org/litestar/issues/2967 @get("str/{id:str}") async def str_path(id: Annotated[str, Parameter(description="String ID")]) -> str: return id @get("uuid/{id:uuid}") async def uuid_path(id: Annotated[UUID, Parameter(description="UUID ID")]) -> UUID: return id with create_test_client( [str_path, uuid_path], openapi_config=OpenAPIConfig(title="Test API", version="1.0.0") ) as client: response = client.get("/schema/openapi.json") assert response.json()["paths"]["/str/{id}"]["get"]["parameters"][0]["description"] == "String ID" assert response.json()["paths"]["/uuid/{id}"]["get"]["parameters"][0]["description"] == "UUID ID" def test_unwrap_new_type() -> None: FancyString = NewType("FancyString", str) @get("/{path_param:str}") async def handler( param: FancyString, optional_param: Optional[FancyString], path_param: FancyString, ) -> FancyString: return FancyString("") app = Litestar([handler]) assert app.openapi_schema.paths["/{path_param}"].get.parameters[0].schema.type == OpenAPIType.STRING # type: ignore[index, union-attr] assert app.openapi_schema.paths["/{path_param}"].get.parameters[1].schema.one_of == [ # type: ignore[index, union-attr] Schema(type=OpenAPIType.STRING), Schema(type=OpenAPIType.NULL), ] assert app.openapi_schema.paths["/{path_param}"].get.parameters[2].schema.type == OpenAPIType.STRING # type: ignore[index, union-attr] assert ( app.openapi_schema.paths["/{path_param}"].get.responses["200"].content["application/json"].schema.type # type: ignore[index, union-attr] == OpenAPIType.STRING ) def test_unwrap_nested_new_type() -> None: FancyString = NewType("FancyString", str) FancierString = NewType("FancierString", FancyString) # pyright: ignore @get("/") async def handler( param: FancierString, ) -> None: return None app = Litestar([handler]) assert app.openapi_schema.paths["/"].get.parameters[0].schema.type == OpenAPIType.STRING # type: ignore[index, union-attr] def test_unwrap_annotated_new_type() -> None: FancyString = NewType("FancyString", str) @dataclasses.dataclass class TestModel: param: Annotated[FancyString, "foo"] @get("/") async def handler( param: TestModel, ) -> None: return None app = Litestar([handler]) testmodel_schema_name = app.openapi_schema.paths["/"].get.parameters[0].schema.value # type: ignore[index, union-attr] assert app.openapi_schema.components.schemas[testmodel_schema_name].properties["param"].type == OpenAPIType.STRING # type: ignore[index, union-attr] def test_query_param_only_properties() -> None: # https://github.com/litestar-org/litestar/issues/3908 @get("/{path_param:str}") def handler( path_param: str, query_param: str, header_param: Annotated[str, Parameter(header="header_param")], cookie_param: Annotated[str, Parameter(cookie="cookie_param")], ) -> None: pass app = Litestar([handler]) params = {p.name: p for p in app.openapi_schema.paths["/{path_param}"].get.parameters} # type: ignore[union-attr, index] for key in ["path_param", "header_param", "cookie_param"]: schema = params[key].to_schema() assert "allowEmptyValue" not in schema assert "allowReserved" not in schema assert params["query_param"].to_schema() == { "name": "query_param", "in": "query", "schema": {"type": "string"}, "required": True, "deprecated": False, "allowEmptyValue": False, "allowReserved": False, } litestar-2.16.0/tests/unit/test_openapi/test_path_item.py000066400000000000000000000233161500564371300236360ustar00rootroot00000000000000from __future__ import annotations import dataclasses from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable, cast from unittest.mock import MagicMock import pytest from typing_extensions import TypeAlias from litestar import Controller, HttpMethod, Litestar, Request, Router, delete, get from litestar._openapi.datastructures import OpenAPIContext from litestar._openapi.path_item import PathItemFactory, merge_path_item_operations from litestar._openapi.utils import default_operation_id_creator from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.openapi.config import OpenAPIConfig from litestar.openapi.spec import Operation, PathItem from litestar.utils import find_index if TYPE_CHECKING: from litestar.routes import HTTPRoute @pytest.fixture() def route(person_controller: type[Controller]) -> HTTPRoute: app = Litestar(route_handlers=[person_controller], openapi_config=None) index = find_index(app.routes, lambda x: x.path_format == "/{service_id}/person/{person_id}") return cast("HTTPRoute", app.routes[index]) @pytest.fixture() def routes_with_router(person_controller: type[Controller]) -> tuple[HTTPRoute, HTTPRoute]: class PersonControllerV2(person_controller): # type: ignore[misc, valid-type] pass router_v1 = Router(path="/v1", route_handlers=[person_controller]) router_v2 = Router(path="/v2", route_handlers=[PersonControllerV2]) app = Litestar(route_handlers=[router_v1, router_v2], openapi_config=None) index_v1 = find_index(app.routes, lambda x: x.path_format == "/v1/{service_id}/person/{person_id}") index_v2 = find_index(app.routes, lambda x: x.path_format == "/v2/{service_id}/person/{person_id}") return cast("HTTPRoute", app.routes[index_v1]), cast("HTTPRoute", app.routes[index_v2]) CreateFactoryFixture: TypeAlias = "Callable[[HTTPRoute], PathItemFactory]" @pytest.fixture() def create_factory() -> CreateFactoryFixture: def factory(route: HTTPRoute) -> PathItemFactory: return PathItemFactory( OpenAPIContext( openapi_config=OpenAPIConfig(title="Test", version="1.0.0", description="Test", create_examples=True), plugins=[], ), route, ) return factory def test_create_path_item(route: HTTPRoute, create_factory: CreateFactoryFixture) -> None: schema = create_factory(route).create_path_item() assert schema.delete assert schema.delete.operation_id == "ServiceIdPersonPersonIdDeletePerson" assert schema.delete.summary == "DeletePerson" assert schema.get assert schema.get.operation_id == "ServiceIdPersonPersonIdGetPersonById" assert schema.get.summary == "GetPersonById" assert schema.patch assert schema.patch.operation_id == "ServiceIdPersonPersonIdPartialUpdatePerson" assert schema.patch.summary == "PartialUpdatePerson" assert schema.put assert schema.put.operation_id == "ServiceIdPersonPersonIdUpdatePerson" assert schema.put.summary == "UpdatePerson" def test_unique_operation_ids_for_multiple_http_methods(create_factory: CreateFactoryFixture) -> None: class MultipleMethodsRouteController(Controller): path = "/" @HTTPRouteHandler("/", http_method=["GET", "HEAD"]) async def root(self, *, request: Request[str, str, Any]) -> None: pass app = Litestar(route_handlers=[MultipleMethodsRouteController], openapi_config=None) index = find_index(app.routes, lambda x: x.path_format == "/") route_with_multiple_methods = cast("HTTPRoute", app.routes[index]) schema = create_factory(route_with_multiple_methods).create_path_item() assert schema.get assert schema.get.operation_id assert schema.head assert schema.head.operation_id assert schema.get.operation_id != schema.head.operation_id def test_unique_operation_ids_for_multiple_http_methods_with_handler_level_operation_creator( create_factory: CreateFactoryFixture, ) -> None: class MultipleMethodsRouteController(Controller): path = "/" @HTTPRouteHandler("/", http_method=["GET", "HEAD"], operation_id=default_operation_id_creator) async def root(self, *, request: Request[str, str, Any]) -> None: pass app = Litestar(route_handlers=[MultipleMethodsRouteController], openapi_config=None) index = find_index(app.routes, lambda x: x.path_format == "/") route_with_multiple_methods = cast("HTTPRoute", app.routes[index]) factory = create_factory(route_with_multiple_methods) factory.context.openapi_config.operation_id_creator = lambda x: "abc" # type: ignore[assignment, misc] schema = create_factory(route_with_multiple_methods).create_path_item() assert schema.get assert schema.get.operation_id assert schema.head assert schema.head.operation_id assert schema.get.operation_id != schema.head.operation_id def test_routes_with_different_paths_should_generate_unique_operation_ids( routes_with_router: tuple[HTTPRoute, HTTPRoute], create_factory: CreateFactoryFixture ) -> None: route_v1, route_v2 = routes_with_router schema_v1 = create_factory(route_v1).create_path_item() schema_v2 = create_factory(route_v2).create_path_item() assert schema_v1.get assert schema_v2.get assert schema_v1.get.operation_id != schema_v2.get.operation_id def test_create_path_item_use_handler_docstring_false(route: HTTPRoute, create_factory: CreateFactoryFixture) -> None: factory = create_factory(route) assert not factory.context.openapi_config.use_handler_docstrings schema = factory.create_path_item() assert schema.get assert schema.get.description is None assert schema.patch assert schema.patch.description == "Description in decorator" def test_create_path_item_use_handler_docstring_true(route: HTTPRoute, create_factory: CreateFactoryFixture) -> None: factory = create_factory(route) factory.context.openapi_config.use_handler_docstrings = True schema = factory.create_path_item() assert schema.get assert schema.get.description == "Description in docstring." assert schema.patch assert schema.patch.description == "Description in decorator" assert schema.put assert schema.put.description # make sure multiline docstring is fully included assert "Line 3." in schema.put.description # make sure internal docstring indentation used to line up with the code # is removed from description assert " " not in schema.put.description def test_operation_id_validation() -> None: @get(path="/1", operation_id="handler") def handler_1() -> None: ... @get(path="/2", operation_id="handler") def handler_2() -> None: ... app = Litestar(route_handlers=[handler_1, handler_2]) with pytest.raises(ImproperlyConfiguredException): app.openapi_schema def test_operation_override() -> None: @dataclass class CustomOperation(Operation): x_code_samples: list[dict[str, str]] | None = field(default=None, metadata={"alias": "x-codeSamples"}) def __post_init__(self) -> None: self.tags = ["test"] self.description = "test" self.x_code_samples = [ {"lang": "Python", "source": "import requests; requests.get('localhost/example')", "label": "Python"}, {"lang": "cURL", "source": "curl -XGET localhost/example", "label": "curl"}, ] @get(path="/1") def handler_1() -> None: ... @get(path="/2", operation_class=CustomOperation) def handler_2() -> None: ... app = Litestar(route_handlers=[handler_1, handler_2]) assert app.openapi_schema.paths assert app.openapi_schema.paths["/1"] assert app.openapi_schema.paths["/1"].get assert isinstance(app.openapi_schema.paths["/1"].get, Operation) assert app.openapi_schema.paths["/2"] assert app.openapi_schema.paths["/2"].get assert isinstance(app.openapi_schema.paths["/2"].get, CustomOperation) assert app.openapi_schema.paths["/2"].get.tags == ["test"] assert app.openapi_schema.paths["/2"].get.description == "test" operation_schema = CustomOperation().to_schema() assert "x-codeSamples" in operation_schema def test_handler_excluded_from_schema(create_factory: CreateFactoryFixture) -> None: @get("/", sync_to_thread=False) def handler_1() -> None: ... @delete("/", include_in_schema=False, sync_to_thread=False) def handler_2() -> None: ... app = Litestar(route_handlers=[handler_1, handler_2]) index = find_index(app.routes, lambda x: x.path_format == "/") route_with_multiple_methods = cast("HTTPRoute", app.routes[index]) factory = create_factory(route_with_multiple_methods) schema = factory.create_path_item() assert schema.get assert schema.delete is None @pytest.mark.parametrize("method", HttpMethod) def test_merge_path_item_operations_operation_set_on_both_raises(method: HttpMethod) -> None: with pytest.raises(ValueError, match="Cannot merge operation"): merge_path_item_operations( PathItem(**{method.value.lower(): MagicMock()}), PathItem(**{method.value.lower(): MagicMock()}), for_path="/", ) @pytest.mark.parametrize( "attr", [ f.name for f in dataclasses.fields(PathItem) if f.name.upper() not in [ *HttpMethod, "TRACE", # remove once https://github.com/litestar-org/litestar/pull/3294 is merged ] ], ) def test_merge_path_item_operation_differing_values_raises(attr: str) -> None: with pytest.raises(ImproperlyConfiguredException, match="Conflicting OpenAPI path configuration for '/'"): merge_path_item_operations(PathItem(), PathItem(**{attr: MagicMock()}), for_path="/") litestar-2.16.0/tests/unit/test_openapi/test_plugins.py000066400000000000000000000053751500564371300233520ustar00rootroot00000000000000import pytest from litestar import Litestar from litestar.config.csrf import CSRFConfig from litestar.openapi.config import OpenAPIConfig from litestar.openapi.plugins import RapidocRenderPlugin, ScalarRenderPlugin, SwaggerRenderPlugin from litestar.testing import TestClient rapidoc_fragment = ".addEventListener('before-try'," swagger_fragment = "requestInterceptor:" def test_rapidoc_csrf() -> None: app = Litestar( csrf_config=CSRFConfig(secret="litestar"), openapi_config=OpenAPIConfig( title="Litestar Example", version="0.0.1", render_plugins=[RapidocRenderPlugin()], ), ) with TestClient(app=app) as client: resp = client.get("/schema/rapidoc") assert resp.status_code == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" assert rapidoc_fragment in resp.text def test_swagger_ui_csrf() -> None: app = Litestar( csrf_config=CSRFConfig(secret="litestar"), openapi_config=OpenAPIConfig( title="Litestar Example", version="0.0.1", render_plugins=[SwaggerRenderPlugin()], ), ) with TestClient(app=app) as client: resp = client.get("/schema/swagger") assert resp.status_code == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" assert swagger_fragment in resp.text def test_plugins_csrf_httponly() -> None: app = Litestar( csrf_config=CSRFConfig(secret="litestar", cookie_httponly=True), openapi_config=OpenAPIConfig( title="Litestar Example", version="0.0.1", render_plugins=[RapidocRenderPlugin(), SwaggerRenderPlugin()], ), ) with TestClient(app=app) as client: resp = client.get("/schema/rapidoc") assert resp.status_code == 200 assert rapidoc_fragment not in resp.text resp = client.get("/schema/swagger") assert resp.status_code == 200 assert swagger_fragment not in resp.text @pytest.mark.parametrize( "scalar_config", [ {"showSidebar": False}, ], ) @pytest.mark.parametrize( "expected_config_render", [ "document.getElementById('api-reference').dataset.configuration = '{\"showSidebar\":false}'", ], ) def test_openapi_scalar_options(scalar_config: dict, expected_config_render: str) -> None: app = Litestar( openapi_config=OpenAPIConfig( title="Litestar Example", version="0.0.1", render_plugins=[ScalarRenderPlugin(options=scalar_config)], ) ) with TestClient(app=app) as client: resp = client.get("/schema/scalar") assert resp.status_code == 200 assert expected_config_render in resp.text litestar-2.16.0/tests/unit/test_openapi/test_render_plugins.py000066400000000000000000000005021500564371300246740ustar00rootroot00000000000000from __future__ import annotations from litestar.openapi.plugins import OpenAPIRenderPlugin from litestar.testing import RequestFactory def test_render_plugin_get_openapi_json_route() -> None: request = RequestFactory().get() assert OpenAPIRenderPlugin.get_openapi_json_route(request) == "/schema/openapi.json" litestar-2.16.0/tests/unit/test_openapi/test_request_body.py000066400000000000000000000141311500564371300243640ustar00rootroot00000000000000from dataclasses import dataclass from typing import Any, Callable, Dict, List, Type from unittest.mock import ANY, MagicMock import pytest from typing_extensions import Annotated from litestar import Controller, Litestar, get, post from litestar._openapi.datastructures import OpenAPIContext from litestar._openapi.request_body import create_request_body from litestar.datastructures.upload_file import UploadFile from litestar.dto import AbstractDTO from litestar.enums import RequestEncodingType from litestar.handlers import BaseRouteHandler from litestar.openapi.config import OpenAPIConfig from litestar.openapi.spec import RequestBody from litestar.params import Body from litestar.typing import FieldDefinition @dataclass class FormData: cv: UploadFile image: UploadFile RequestBodyFactory = Callable[[BaseRouteHandler, FieldDefinition], RequestBody] @pytest.fixture() def openapi_context() -> OpenAPIContext: return OpenAPIContext( openapi_config=OpenAPIConfig(title="test", version="1.0.0", create_examples=True), plugins=[], ) @pytest.fixture() def create_request(openapi_context: OpenAPIContext) -> RequestBodyFactory: def _factory(route_handler: BaseRouteHandler, data_field: FieldDefinition) -> RequestBody: return create_request_body( context=openapi_context, handler_id=route_handler.handler_id, resolved_data_dto=route_handler.resolve_data_dto(), data_field=data_field, ) return _factory def test_create_request_body(person_controller: Type[Controller], create_request: RequestBodyFactory) -> None: for route in Litestar(route_handlers=[person_controller]).routes: for route_handler, _ in route.route_handler_map.values(): # type: ignore[union-attr] handler_fields = route_handler.parsed_fn_signature.parameters if "data" in handler_fields: request_body = create_request(route_handler, handler_fields["data"]) assert request_body def test_request_body_schema_extra() -> None: @dataclass class RequestBody: foo: str @get() async def handler( body1: Annotated[ RequestBody, Body( title="Default title", schema_extra={ "title": "Overridden title", }, ), ], ) -> Any: return body1 app = Litestar([handler]) schema = app.openapi_schema.to_schema() resp = next(iter(schema["components"]["schemas"].values())) assert resp["title"] == "Overridden title" def test_upload_single_file_schema_generation() -> None: @post(path="/file-upload") async def handle_file_upload( data: UploadFile = Body(media_type=RequestEncodingType.MULTI_PART), ) -> None: return None app = Litestar([handle_file_upload]) schema = app.openapi_schema.to_schema() assert schema["paths"]["/file-upload"]["post"]["requestBody"]["content"]["multipart/form-data"]["schema"] == { "properties": {"file": {"type": "string", "format": "binary", "contentMediaType": "application/octet-stream"}}, "type": "object", } def test_upload_list_of_files_schema_generation() -> None: @post(path="/file-list-upload") async def handle_file_list_upload( data: List[UploadFile] = Body(media_type=RequestEncodingType.MULTI_PART), ) -> None: return None app = Litestar([handle_file_list_upload]) schema = app.openapi_schema.to_schema() assert schema["paths"]["/file-list-upload"]["post"]["requestBody"]["content"]["multipart/form-data"]["schema"] == { "type": "object", "properties": { "files": { "items": {"type": "string", "contentMediaType": "application/octet-stream", "format": "binary"}, "type": "array", } }, } def test_upload_file_dict_schema_generation() -> None: @post(path="/file-dict-upload") async def handle_file_list_upload( data: Dict[str, UploadFile] = Body(media_type=RequestEncodingType.MULTI_PART), ) -> None: return None app = Litestar([handle_file_list_upload]) schema = app.openapi_schema.to_schema() assert schema["paths"]["/file-dict-upload"]["post"]["requestBody"]["content"]["multipart/form-data"]["schema"] == { "type": "object", "properties": { "files": { "items": {"type": "string", "contentMediaType": "application/octet-stream", "format": "binary"}, "type": "array", } }, } def test_upload_file_model_schema_generation() -> None: @post(path="/form-upload") async def handle_form_upload( data: FormData = Body(media_type=RequestEncodingType.MULTI_PART), ) -> None: return None app = Litestar([handle_form_upload]) schema = app.openapi_schema.to_schema() assert schema["paths"]["/form-upload"]["post"]["requestBody"]["content"]["multipart/form-data"] == { "schema": {"$ref": "#/components/schemas/FormData"} } assert schema["components"] == { "schemas": { "FormData": { "properties": { "cv": {"type": "string", "contentMediaType": "application/octet-stream", "format": "binary"}, "image": {"type": "string", "contentMediaType": "application/octet-stream", "format": "binary"}, }, "type": "object", "required": ["cv", "image"], "title": "FormData", } } } def test_request_body_generation_with_dto(create_request: RequestBodyFactory) -> None: mock_dto = MagicMock(spec=AbstractDTO) @post(path="/form-upload", dto=mock_dto) # pyright: ignore async def handler(data: Dict[str, Any]) -> None: return None Litestar(route_handlers=[handler]) field_definition = FieldDefinition.from_annotation(Dict[str, Any]) create_request(handler, field_definition) mock_dto.create_openapi_schema.assert_called_once_with( field_definition=field_definition, handler_id=handler.handler_id, schema_creator=ANY ) litestar-2.16.0/tests/unit/test_openapi/test_responses.py000066400000000000000000000556611500564371300237150ustar00rootroot00000000000000# ruff: noqa: UP006 from __future__ import annotations from dataclasses import dataclass from http import HTTPStatus from pathlib import Path from types import ModuleType from typing import Any, Callable, Dict, TypedDict, TypeVar from unittest.mock import MagicMock import pytest from typing_extensions import TypeAlias from litestar import Controller, Litestar, MediaType, Response, delete, get, post from litestar._openapi.datastructures import OpenAPIContext from litestar._openapi.responses import ( ResponseFactory, create_error_responses, ) from litestar._openapi.schema_generation.plugins import openapi_schema_plugins from litestar.datastructures import Cookie, ResponseHeader from litestar.dto import AbstractDTO from litestar.exceptions import ( HTTPException, PermissionDeniedException, ValidationException, ) from litestar.handlers import HTTPRouteHandler from litestar.openapi.config import OpenAPIConfig from litestar.openapi.datastructures import ResponseSpec from litestar.openapi.spec import Example, OpenAPIHeader, OpenAPIMediaType, Reference, Schema from litestar.openapi.spec.enums import OpenAPIType from litestar.response import File, Redirect, Stream, Template from litestar.routes import HTTPRoute from litestar.status_codes import ( HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_307_TEMPORARY_REDIRECT, HTTP_400_BAD_REQUEST, HTTP_406_NOT_ACCEPTABLE, ) from litestar.typing import FieldDefinition from tests.models import DataclassPerson, DataclassPersonFactory from tests.unit.test_openapi.utils import PetException T = TypeVar("T") CreateFactoryFixture: TypeAlias = "Callable[..., ResponseFactory]" @pytest.fixture() def create_factory() -> CreateFactoryFixture: def _create_factory(route_handler: HTTPRouteHandler, generate_examples: bool = False) -> ResponseFactory: return ResponseFactory( context=OpenAPIContext( openapi_config=OpenAPIConfig(title="test", version="1.0.0", create_examples=generate_examples), plugins=openapi_schema_plugins, ), route_handler=route_handler, ) return _create_factory def get_registered_route_handler(handler: HTTPRouteHandler | type[Controller], name: str) -> HTTPRouteHandler: app = Litestar(route_handlers=[handler]) return app.asgi_router.route_handler_index[name] # type: ignore[return-value] def test_create_responses( person_controller: type[Controller], pet_controller: type[Controller], create_factory: CreateFactoryFixture ) -> None: for route in Litestar(route_handlers=[person_controller]).routes: assert isinstance(route, HTTPRoute) for route_handler, _ in route.route_handler_map.values(): if route_handler.resolve_include_in_schema(): responses = create_factory(route_handler).create_responses(True) assert responses assert str(route_handler.status_code) in responses assert str(HTTP_400_BAD_REQUEST) in responses handler = get_registered_route_handler( pet_controller, "tests.unit.test_openapi.conftest.create_pet_controller..PetController.get_pets_or_owners", ) responses = create_factory(handler).create_responses(raises_validation_error=False) assert responses assert str(HTTP_400_BAD_REQUEST) not in responses assert str(HTTP_406_NOT_ACCEPTABLE) in responses assert str(HTTP_200_OK) in responses def test_create_error_responses() -> None: class AlternativePetException(HTTPException): status_code = ValidationException.status_code pet_exc_response, permission_denied_exc_response, validation_exc_response = create_error_responses( exceptions=[ PetException, PermissionDeniedException, AlternativePetException, ValidationException, ] ) assert pet_exc_response[0] == str(PetException.status_code) assert pet_exc_response[1].description == HTTPStatus(PetException.status_code).description assert pet_exc_response[1].content assert pet_exc_response[1].content[MediaType.JSON] pet_exc_response_schema = pet_exc_response[1].content[MediaType.JSON].schema assert isinstance(pet_exc_response_schema, Schema) assert pet_exc_response_schema.examples assert pet_exc_response_schema.properties assert pet_exc_response_schema.description assert pet_exc_response_schema.required assert pet_exc_response_schema.type assert not pet_exc_response_schema.one_of assert permission_denied_exc_response[0] == str(PermissionDeniedException.status_code) assert ( permission_denied_exc_response[1].description == HTTPStatus(PermissionDeniedException.status_code).description ) assert permission_denied_exc_response[1].content assert permission_denied_exc_response[1].content[MediaType.JSON] schema = permission_denied_exc_response[1].content[MediaType.JSON].schema assert isinstance(schema, Schema) assert schema.examples assert schema.properties assert schema.description assert schema.required assert schema.type assert not schema.one_of assert validation_exc_response[0] == str(ValidationException.status_code) assert validation_exc_response[1].description == HTTPStatus(ValidationException.status_code).description assert validation_exc_response[1].content assert validation_exc_response[1].content[MediaType.JSON] schema = validation_exc_response[1].content[MediaType.JSON].schema assert isinstance(schema, Schema) assert schema.one_of assert len(schema.one_of) == 2 for schema in schema.one_of: assert isinstance(schema, Schema) assert schema.examples assert schema.description assert schema.properties assert schema.required assert schema.type def test_create_error_responses_with_non_http_status_code() -> None: class HouseNotFoundError(HTTPException): status_code: int = 420 detail: str = "House not found." house_not_found_exc_response = next(create_error_responses(exceptions=[HouseNotFoundError])) assert house_not_found_exc_response[0] == str(HouseNotFoundError.status_code) assert house_not_found_exc_response[1].description == HouseNotFoundError.detail def test_create_success_response_with_headers(create_factory: CreateFactoryFixture) -> None: @get( path="/test", response_headers=[ResponseHeader(name="special-header", value="100", description="super-duper special")], response_description="test", content_encoding="base64", content_media_type="image/png", name="test", ) def handler() -> list: return [] handler = get_registered_route_handler(handler, "test") response = create_factory(handler, True).create_success_response() assert response.description == "test" assert response.content assert isinstance(handler.media_type, MediaType) schema = response.content[handler.media_type.value].schema assert isinstance(schema, Schema) assert schema.content_encoding == "base64" assert schema.content_media_type == "image/png" assert isinstance(response.headers, dict) assert isinstance(response.headers["special-header"], OpenAPIHeader) assert response.headers["special-header"].to_schema() == { "schema": {"type": "string"}, "description": "super-duper special", "required": False, "deprecated": False, } def test_create_success_response_with_cookies(create_factory: CreateFactoryFixture) -> None: @get( path="/test", response_cookies=[ Cookie(key="first-cookie", httponly=True, samesite="strict", description="the first cookie", secure=True), Cookie(key="second-cookie", max_age=500, description="the second cookie"), ], name="test", ) def handler() -> list: return [] handler = get_registered_route_handler(handler, "test") response = create_factory(handler, True).create_success_response() assert isinstance(response.headers, dict) assert isinstance(response.headers["Set-Cookie"], OpenAPIHeader) schema = response.headers["Set-Cookie"].schema assert isinstance(schema, Schema) assert schema.to_schema() == { "allOf": [ { "description": "the first cookie", "example": 'first-cookie=""; HttpOnly; Path=/; SameSite=strict; Secure', }, { "description": "the second cookie", "example": 'second-cookie=""; Max-Age=500; Path=/; SameSite=lax', }, ] } def test_create_success_response_with_response_class(create_factory: CreateFactoryFixture) -> None: @get(path="/test", name="test") def handler() -> Response[DataclassPerson]: return Response(content=DataclassPersonFactory.build()) handler = get_registered_route_handler(handler, "test") factory = create_factory(handler, True) response = factory.create_success_response() assert response.content reference = response.content["application/json"].schema assert isinstance(reference, Reference) assert isinstance(factory.context.schema_registry.from_reference(reference).schema, Schema) def test_create_success_response_with_stream(create_factory: CreateFactoryFixture) -> None: @get(path="/test", name="test") def handler() -> Stream: return Stream(iter([])) handler = get_registered_route_handler(handler, "test") response = create_factory(handler, True).create_success_response() assert response.description == "Stream Response" def test_create_success_response_redirect(create_factory: CreateFactoryFixture) -> None: @get(path="/test", name="test") def redirect_handler() -> Redirect: return Redirect(path="/target") handler = get_registered_route_handler(redirect_handler, "test") response = create_factory(handler, True).create_success_response() assert response.description == "Redirect Response" assert response.headers location = response.headers["location"] assert isinstance(location, OpenAPIHeader) assert isinstance(location.schema, Schema) assert location.schema.type == OpenAPIType.STRING assert location.description def test_create_success_response_redirect_override(create_factory: CreateFactoryFixture) -> None: @get(path="/test", status_code=HTTP_307_TEMPORARY_REDIRECT, name="test") def redirect_handler() -> Redirect: return Redirect(path="/target") handler = get_registered_route_handler(redirect_handler, "test") response = create_factory(handler, True).create_success_response() assert response.description == "Redirect Response" assert response.headers location = response.headers["location"] assert isinstance(location, OpenAPIHeader) assert isinstance(location.schema, Schema) assert location.schema.type == OpenAPIType.STRING assert location.description def test_create_success_response_no_content_explicit_responsespec( create_factory: CreateFactoryFixture, ) -> None: @delete( path="/test", responses={HTTP_204_NO_CONTENT: ResponseSpec(None, description="Custom description")}, name="test", ) def handler() -> None: return None handler = get_registered_route_handler(handler, "test") factory = create_factory(handler) responses = factory.create_additional_responses() status, response = next(responses) assert status == "204" assert response.description == "Custom description" assert not response.content with pytest.raises(StopIteration): next(responses) def test_create_success_response_file_data(create_factory: CreateFactoryFixture) -> None: @get(path="/test", name="test") def file_handler() -> File: return File(path=Path("test_responses.py")) handler = get_registered_route_handler(file_handler, "test") response = create_factory(handler, True).create_success_response() assert response.description == "File Download" assert response.headers assert isinstance(response.headers["content-length"], OpenAPIHeader) assert isinstance(response.headers["content-length"].schema, Schema) assert response.headers["content-length"].schema.type == OpenAPIType.STRING assert response.headers["content-length"].description assert isinstance(response.headers["last-modified"], OpenAPIHeader) assert isinstance(response.headers["last-modified"].schema, Schema) assert response.headers["last-modified"].schema.type == OpenAPIType.STRING assert response.headers["last-modified"].description assert isinstance(response.headers["etag"], OpenAPIHeader) assert isinstance(response.headers["etag"].schema, Schema) assert response.headers["etag"].schema.type == OpenAPIType.STRING assert response.headers["etag"].description def test_create_success_response_template(create_factory: CreateFactoryFixture) -> None: @get(path="/template", name="test") def template_handler() -> Template: return Template(template_name="none") handler = get_registered_route_handler(template_handler, "test") response = create_factory(handler, True).create_success_response() assert response.description == "Request fulfilled, document follows" assert response.content assert response.content[MediaType.HTML.value] def test_create_additional_responses(create_factory: CreateFactoryFixture) -> None: @dataclass class ServerError: message: str class AuthenticationError(TypedDict): message: str class UnknownError(TypedDict): message: str @get( responses={ 401: ResponseSpec(data_container=AuthenticationError, description="Authentication error"), 500: ResponseSpec(data_container=ServerError, generate_examples=False, media_type=MediaType.TEXT), 505: ResponseSpec(data_container=UnknownError), 900: ResponseSpec(data_container=UnknownError, media_type="application/vnd.custom"), } ) def handler() -> DataclassPerson: return DataclassPersonFactory.build() factory = create_factory(handler) responses = factory.create_additional_responses() first_response = next(responses) assert first_response[0] == "401" assert first_response[1].description == "Authentication error" assert first_response[1].content assert isinstance(first_response[1].content["application/json"], OpenAPIMediaType) reference = first_response[1].content["application/json"].schema assert isinstance(reference, Reference) schema = factory.context.schema_registry.from_reference(reference).schema assert isinstance(schema, Schema) assert schema.title == "AuthenticationError" second_response = next(responses) assert second_response[0] == "500" assert second_response[1].description == "Additional response" assert second_response[1].content assert isinstance(second_response[1].content["text/plain"], OpenAPIMediaType) reference = second_response[1].content["text/plain"].schema assert isinstance(reference, Reference) schema = factory.context.schema_registry.from_reference(reference).schema assert isinstance(schema, Schema) assert schema.title == "ServerError" assert not schema.examples third_response = next(responses) assert third_response[0] == "505" assert third_response[1].description == "Additional response" fourth_response = next(responses) assert fourth_response[0] == "900" assert fourth_response[1].description == "Additional response" custom_media_type_content = fourth_response[1].content.get("application/vnd.custom") # type: ignore[union-attr] assert custom_media_type_content assert isinstance(custom_media_type_content, OpenAPIMediaType) with pytest.raises(StopIteration): next(responses) def test_additional_responses_overlap_with_other_responses(create_factory: CreateFactoryFixture) -> None: @dataclass class OkResponse: message: str @get(responses={200: ResponseSpec(data_container=OkResponse, description="Overwritten response")}, name="test") def handler() -> DataclassPerson: return DataclassPersonFactory.build() handler = get_registered_route_handler(handler, "test") responses = create_factory(handler).create_responses(True) assert responses is not None assert responses["200"] is not None assert responses["200"].description == "Overwritten response" def test_additional_responses_overlap_with_raises(create_factory: CreateFactoryFixture) -> None: @dataclass class ErrorResponse: message: str @get( raises=[ValidationException], responses={400: ResponseSpec(data_container=ErrorResponse, description="Overwritten response")}, name="test", ) def handler() -> DataclassPerson: raise ValidationException() handler = get_registered_route_handler(handler, "test") responses = create_factory(handler).create_responses(True) assert responses is not None assert responses["400"] is not None assert responses["400"].description == "Overwritten response" def test_additional_responses_with_custom_examples(create_factory: CreateFactoryFixture) -> None: @get(responses={200: ResponseSpec(DataclassPerson, examples=[Example(value={"string": "example", "number": 1})])}) def handler() -> DataclassPerson: return DataclassPersonFactory.build() factory = create_factory(handler) responses = factory.create_additional_responses() status_code, response = next(responses) assert response.content assert response.content["application/json"].examples == { "dataclassperson-example-1": Example( value={ "string": "example", "number": 1, } ), } with pytest.raises(StopIteration): next(responses) def test_additional_responses_with_custom_example_ids(create_factory: CreateFactoryFixture) -> None: """Test that custom example IDs are used when provided in the Example object.""" @get( responses={ 200: ResponseSpec( DataclassPerson, examples=[ Example(id="custom-id-1", summary="First example", value={"string": "example1", "number": 1}), Example(id="custom-id-2", summary="Second example", value={"string": "example2", "number": 2}), Example(summary="Third example", value={"string": "example3", "number": 3}), ], ) } ) def handler() -> DataclassPerson: return DataclassPersonFactory.build() factory = create_factory(handler) responses = factory.create_additional_responses() status_code, response = next(responses) assert response.content assert isinstance(response.content["application/json"], OpenAPIMediaType) assert response.content["application/json"].examples is not None assert "custom-id-1" in response.content["application/json"].examples assert "custom-id-2" in response.content["application/json"].examples assert "dataclassperson-example-3" in response.content["application/json"].examples assert response.content["application/json"].examples["custom-id-1"].summary == "First example" assert response.content["application/json"].examples["custom-id-2"].summary == "Second example" assert response.content["application/json"].examples["dataclassperson-example-3"].summary == "Third example" with pytest.raises(StopIteration): next(responses) def test_create_response_for_response_subclass(create_factory: CreateFactoryFixture) -> None: class CustomResponse(Response[T]): pass @get(path="/test", name="test", signature_types=[CustomResponse]) def handler() -> CustomResponse[DataclassPerson]: return CustomResponse(content=DataclassPersonFactory.build()) handler = get_registered_route_handler(handler, "test") factory = create_factory(handler, True) response = factory.create_success_response() assert response.content assert isinstance(response.content["application/json"], OpenAPIMediaType) reference = response.content["application/json"].schema assert isinstance(reference, Reference) schema = factory.context.schema_registry.from_reference(reference).schema assert schema.title == "DataclassPerson" def test_success_response_with_future_annotations( create_module: Callable[[str], ModuleType], create_factory: CreateFactoryFixture ) -> None: module = create_module( """ from __future__ import annotations from litestar import get @get(path="/test", name="test") def handler() -> int: ... """ ) handler = get_registered_route_handler(module.handler, "test") response = create_factory(handler, True).create_success_response() assert next(iter(response.content.values())).schema.type == OpenAPIType.INTEGER # type: ignore[union-attr] def test_response_generation_with_dto(create_factory: CreateFactoryFixture) -> None: mock_dto = MagicMock(spec=AbstractDTO) mock_dto.create_openapi_schema.return_value = Schema() @post(path="/form-upload", return_dto=mock_dto) # pyright: ignore async def handler(data: Dict[str, Any]) -> Dict[str, Any]: return data Litestar(route_handlers=[handler]) factory = create_factory(handler) field_definition = FieldDefinition.from_annotation(Dict[str, Any]) factory.create_success_response() mock_dto.create_openapi_schema.assert_called_once_with( field_definition=field_definition, handler_id=handler.handler_id, schema_creator=factory.schema_creator ) @pytest.mark.parametrize( "content_media_type, expected", ((MediaType.TEXT, MediaType.TEXT), (None, "application/octet-stream")) ) def test_file_response_media_type(content_media_type: Any, expected: Any, create_factory: CreateFactoryFixture) -> None: @get("/", content_media_type=content_media_type) def handler() -> File: return File("test.txt") response = create_factory(handler).create_success_response() assert next(iter(response.content.values())).schema.content_media_type == expected # type: ignore[union-attr] def test_response_header_deprecated_properties() -> None: assert ResponseHeader(name="foo", value="bar").allow_empty_value is False assert ResponseHeader(name="foo", value="bar").allow_reserved is False with pytest.warns(DeprecationWarning, match="property is invalid for headers"): ResponseHeader(name="foo", value="bar", allow_empty_value=True) with pytest.warns(DeprecationWarning, match="property is invalid for headers"): ResponseHeader(name="foo", value="bar", allow_reserved=True) def test_header_deprecated_properties() -> None: assert OpenAPIHeader().allow_empty_value is False assert OpenAPIHeader().allow_reserved is False with pytest.warns(DeprecationWarning, match="property is invalid for headers"): OpenAPIHeader(allow_empty_value=True) with pytest.warns(DeprecationWarning, match="property is invalid for headers"): OpenAPIHeader(allow_reserved=True) litestar-2.16.0/tests/unit/test_openapi/test_schema.py000066400000000000000000000656201500564371300231300ustar00rootroot00000000000000import sys from dataclasses import dataclass from datetime import date, datetime, timezone from enum import Enum, auto from typing import ( TYPE_CHECKING, Any, Dict, Generic, List, Literal, Optional, Tuple, TypedDict, TypeVar, Union, # pyright: ignore ) import annotated_types import msgspec import pytest from msgspec import Struct from typing_extensions import Annotated, TypeAlias, TypeAliasType from litestar import Controller, MediaType, get, post from litestar._openapi.schema_generation.plugins import openapi_schema_plugins from litestar._openapi.schema_generation.schema import ( KWARG_DEFINITION_ATTRIBUTE_TO_OPENAPI_PROPERTY_MAP, SchemaCreator, ) from litestar.app import DEFAULT_OPENAPI_CONFIG, Litestar from litestar.di import Provide from litestar.enums import ParamType from litestar.exceptions import ImproperlyConfiguredException from litestar.openapi.spec import ExternalDocumentation, OpenAPIType, Reference from litestar.openapi.spec.example import Example from litestar.openapi.spec.parameter import Parameter as OpenAPIParameter from litestar.openapi.spec.schema import Schema from litestar.pagination import ClassicPagination, CursorPagination, OffsetPagination from litestar.params import KwargDefinition, Parameter, ParameterKwarg from litestar.testing import create_test_client from litestar.typing import FieldDefinition from litestar.utils.helpers import get_name from tests.helpers import get_schema_for_field_definition from tests.models import DataclassPerson, DataclassPet if TYPE_CHECKING: from types import ModuleType from typing import Callable T = TypeVar("T") def test_process_schema_result() -> None: test_str = "abc" kwarg_definition = ParameterKwarg( examples=[Example(value=1)], external_docs=ExternalDocumentation(url="https://example.com/docs"), content_encoding="utf-8", default=test_str, title=test_str, description=test_str, const=True, gt=1, ge=1, lt=1, le=1, multiple_of=1, min_items=1, max_items=1, min_length=1, max_length=1, pattern="^[a-z]$", ) schema = get_schema_for_field_definition( FieldDefinition.from_annotation(annotation=str, kwarg_definition=kwarg_definition) ) assert schema.title assert schema.const == test_str assert kwarg_definition.examples for signature_key, schema_key in KWARG_DEFINITION_ATTRIBUTE_TO_OPENAPI_PROPERTY_MAP.items(): if schema_key == "examples": assert schema.examples == [kwarg_definition.examples[0].value] else: assert getattr(schema, schema_key) == getattr(kwarg_definition, signature_key) def test_override_schema_component_key() -> None: @dataclass class Data: pass @post("/") def handler( data: Data, ) -> Annotated[Data, Parameter(schema_component_key="not_data")]: return Data() @get("/") def handler_2() -> Annotated[Data, Parameter(schema_component_key="not_data")]: return Data() app = Litestar([handler, handler_2]) schema = app.openapi_schema.to_schema() # we expect the annotated / non-annotated to generate independent components assert schema["paths"]["/"]["post"]["requestBody"]["content"]["application/json"] == { "schema": {"$ref": "#/components/schemas/test_override_schema_component_key.Data"} } assert schema["paths"]["/"]["post"]["responses"]["201"]["content"] == { "application/json": {"schema": {"$ref": "#/components/schemas/not_data"}} } # a response with the same type and the same name should reference the same component assert schema["paths"]["/"]["get"]["responses"]["200"]["content"] == { "application/json": {"schema": {"$ref": "#/components/schemas/not_data"}} } assert app.openapi_schema.to_schema()["components"] == { "schemas": { "not_data": {"properties": {}, "type": "object", "required": [], "title": "Data"}, "test_override_schema_component_key.Data": { "properties": {}, "type": "object", "required": [], "title": "Data", }, } } def test_override_schema_component_key_raise_if_keys_are_not_unique() -> None: @dataclass class Data: pass @dataclass class Data2: pass @post("/") def handler( data: Data, ) -> Annotated[Data, Parameter(schema_component_key="not_data")]: return Data() @get("/") def handler_2() -> Annotated[Data2, Parameter(schema_component_key="not_data")]: return Data2() with pytest.raises(ImproperlyConfiguredException, match="Schema component keys must be unique"): Litestar([handler, handler_2]).openapi_schema.to_schema() def test_dependency_schema_generation() -> None: async def top_dependency(query_param: int) -> int: return query_param async def mid_level_dependency(header_param: str = Parameter(header="header_param", required=False)) -> int: return 5 async def local_dependency(path_param: int, mid_level: int, top_level: int) -> int: return path_param + mid_level + top_level class MyController(Controller): path = "/test" dependencies = {"mid_level": Provide(mid_level_dependency)} @get( path="/{path_param:int}", dependencies={ "summed": Provide(local_dependency), }, media_type=MediaType.TEXT, ) def test_function(self, summed: int, handler_param: int) -> str: return str(summed) with create_test_client( MyController, dependencies={"top_level": Provide(top_dependency)}, openapi_config=DEFAULT_OPENAPI_CONFIG, ) as client: handler = client.app.openapi_schema.paths["/test/{path_param}"] data = {param.name: {"in": param.param_in, "required": param.required} for param in handler.get.parameters} assert data == { "path_param": {"in": ParamType.PATH, "required": True}, "header_param": {"in": ParamType.HEADER, "required": False}, "query_param": {"in": ParamType.QUERY, "required": True}, "handler_param": {"in": ParamType.QUERY, "required": True}, } def test_get_schema_for_annotation_enum() -> None: class Opts(str, Enum): opt1 = "opt1" opt2 = "opt2" schema = get_schema_for_field_definition(FieldDefinition.from_annotation(Opts)) assert schema.enum == ["opt1", "opt2"] ValueType: TypeAlias = Literal["a", "b", "c"] ConstType: TypeAlias = Literal[1] def test_handling_of_literals() -> None: @dataclass class DataclassWithLiteral: value: ValueType const: ConstType composite: Literal[ValueType, ConstType] schema = get_schema_for_field_definition(FieldDefinition.from_kwarg(name="", annotation=DataclassWithLiteral)) assert isinstance(schema, Schema) assert schema.properties value = schema.properties["value"] assert isinstance(value, Schema) assert value.enum == ["a", "b", "c"] const = schema.properties["const"] assert isinstance(const, Schema) assert const.const == 1 composite = schema.properties["composite"] assert isinstance(composite, Schema) assert composite.enum == ["a", "b", "c", 1] def test_schema_hashing() -> None: schema = Schema( one_of=[ Schema(type=OpenAPIType.STRING), Schema(type=OpenAPIType.NUMBER), Schema(type=OpenAPIType.OBJECT, properties={"key": Schema(type=OpenAPIType.STRING)}), ], examples=[None, [1, 2, 3]], ) assert hash(schema) def test_title_validation() -> None: # TODO: what is this actually testing? creator = SchemaCreator(plugins=openapi_schema_plugins) person_ref = creator.for_field_definition(FieldDefinition.from_kwarg(name="Person", annotation=DataclassPerson)) pet_ref = creator.for_field_definition(FieldDefinition.from_kwarg(name="Pet", annotation=DataclassPet)) assert isinstance(person_ref, Reference) assert isinstance(pet_ref, Reference) assert isinstance(creator.schema_registry.from_reference(person_ref).schema, Schema) assert isinstance(creator.schema_registry.from_reference(pet_ref).schema, Schema) @pytest.mark.parametrize("with_future_annotations", [True, False]) def test_create_schema_for_dataclass_with_annotated_model_attribute( with_future_annotations: bool, create_module: "Callable[[str], ModuleType]" ) -> None: """Test that a model with an annotated attribute is correctly handled.""" module = create_module( f""" {'from __future__ import annotations' if with_future_annotations else ''} from typing_extensions import Annotated from dataclasses import dataclass @dataclass class Foo: foo: Annotated[int, "Foo description"] """ ) schema = get_schema_for_field_definition(FieldDefinition.from_annotation(module.Foo)) assert schema.properties and "foo" in schema.properties @pytest.mark.parametrize("with_future_annotations", [True, False]) def test_create_schema_for_typedict_with_annotated_required_and_not_required_model_attributes( with_future_annotations: bool, create_module: "Callable[[str], ModuleType]" ) -> None: """Test that a model with an annotated attribute is correctly handled.""" module = create_module( f""" {'from __future__ import annotations' if with_future_annotations else ''} from typing_extensions import Annotated, Required, NotRequired from typing import TypedDict class Foo(TypedDict): foo: Annotated[int, "Foo description"] bar: Annotated[Required[int], "Bar description"] baz: Annotated[NotRequired[int], "Baz description"] """ ) schema = get_schema_for_field_definition(FieldDefinition.from_annotation(module.Foo)) assert schema.properties and all(key in schema.properties for key in ("foo", "bar", "baz")) def test_create_schema_from_msgspec_annotated_type() -> None: class Lookup(msgspec.Struct): str_field: Annotated[ str, msgspec.Meta(max_length=16, examples=["example"], description="description", title="title", pattern=r"\w+"), ] bytes_field: Annotated[bytes, msgspec.Meta(max_length=2, min_length=1)] default_field: Annotated[str, msgspec.Meta(min_length=1)] = "a" schema = get_schema_for_field_definition(FieldDefinition.from_kwarg(name="Lookup", annotation=Lookup)) assert schema.properties["str_field"].type == OpenAPIType.STRING # type: ignore[index, union-attr] assert schema.properties["str_field"].examples == ["example"] # type: ignore[index, union-attr] assert schema.properties["str_field"].description == "description" # type: ignore[index] assert schema.properties["str_field"].title == "title" # type: ignore[index, union-attr] assert schema.properties["str_field"].max_length == 16 # type: ignore[index, union-attr] assert sorted(schema.required) == sorted(["str_field", "bytes_field"]) # type: ignore[arg-type] assert schema.properties["bytes_field"].to_schema() == { # type: ignore[index] "contentEncoding": "utf-8", "maxLength": 2, "minLength": 1, "type": "string", } def test_annotated_types() -> None: historical_date = date(year=1980, day=1, month=1) today = date.today() @dataclass class MyDataclass: constrained_int: Annotated[int, annotated_types.Gt(1), annotated_types.Lt(10)] constrained_float: Annotated[float, annotated_types.Ge(1), annotated_types.Le(10)] constrained_date: Annotated[date, annotated_types.Interval(gt=historical_date, lt=today)] constrained_lower_case: annotated_types.LowerCase[str] constrained_upper_case: annotated_types.UpperCase[str] constrained_is_ascii: annotated_types.IsAscii[str] constrained_is_digit: annotated_types.IsDigit[str] schema = get_schema_for_field_definition(FieldDefinition.from_kwarg(name="MyDataclass", annotation=MyDataclass)) assert schema.properties["constrained_int"].exclusive_minimum == 1 # type: ignore[index, union-attr] assert schema.properties["constrained_int"].exclusive_maximum == 10 # type: ignore[index, union-attr] assert schema.properties["constrained_float"].minimum == 1 # type: ignore[index, union-attr] assert schema.properties["constrained_float"].maximum == 10 # type: ignore[index, union-attr] assert datetime.fromtimestamp( schema.properties["constrained_date"].exclusive_minimum, # type: ignore[arg-type, index, union-attr] tz=timezone.utc, ) == datetime.fromordinal(historical_date.toordinal()).replace(tzinfo=timezone.utc) assert datetime.fromtimestamp( schema.properties["constrained_date"].exclusive_maximum, # type: ignore[arg-type, index, union-attr] tz=timezone.utc, ) == datetime.fromordinal(today.toordinal()).replace(tzinfo=timezone.utc) assert schema.properties["constrained_lower_case"].description == "must be in lower case" # type: ignore[index] assert schema.properties["constrained_upper_case"].description == "must be in upper case" # type: ignore[index] assert schema.properties["constrained_is_ascii"].pattern == "[[:ascii:]]" # type: ignore[index, union-attr] assert schema.properties["constrained_is_digit"].pattern == "[[:digit:]]" # type: ignore[index, union-attr] def test_literal_enums() -> None: class Foo(Enum): A = auto() B = auto() schema = get_schema_for_field_definition(FieldDefinition.from_annotation(List[Literal[Foo.A]])) assert isinstance(schema.items, Schema) assert schema.items.const == 1 @dataclass class DataclassGeneric(Generic[T]): foo: T optional_foo: Optional[T] annotated_foo: Annotated[T, object()] class MsgspecGeneric(Struct, Generic[T]): foo: T optional_foo: Optional[T] annotated_foo: Annotated[T, object()] annotations: List[type] = [DataclassGeneric[int], MsgspecGeneric[int]] # Generic TypedDict was only supported from 3.11 onwards if sys.version_info >= (3, 11): class TypedDictGeneric(TypedDict, Generic[T]): foo: T optional_foo: Optional[T] annotated_foo: Annotated[T, object()] annotations.append(TypedDictGeneric[int]) @pytest.mark.parametrize("cls", annotations) def test_schema_generation_with_generic_classes(cls: Any) -> None: expected_foo_schema = Schema(type=OpenAPIType.INTEGER) expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.NULL)]) properties = get_schema_for_field_definition( FieldDefinition.from_kwarg(name=get_name(cls), annotation=cls) ).properties assert properties assert properties["foo"] == expected_foo_schema assert properties["annotated_foo"] == expected_foo_schema assert properties["optional_foo"] == expected_optional_foo_schema B = TypeVar("B", bound=int) C = TypeVar("C", int, str) @dataclass class ConstrainedGenericDataclass(Generic[T, B, C]): bound: B constrained: C union: Union[T, bool] union_constrained: Union[C, bool] union_bound: Union[B, bool] def test_schema_generation_with_generic_classes_constrained() -> None: cls = ConstrainedGenericDataclass properties = get_schema_for_field_definition( FieldDefinition.from_kwarg(name=cls.__name__, annotation=cls) ).properties assert properties assert properties["bound"] == Schema(type=OpenAPIType.INTEGER) assert properties["constrained"] == Schema( one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.STRING)] ) assert properties["union"] == Schema(one_of=[Schema(type=OpenAPIType.OBJECT), Schema(type=OpenAPIType.BOOLEAN)]) assert properties["union_constrained"] == Schema( one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.STRING), Schema(type=OpenAPIType.BOOLEAN)] ) assert properties["union_bound"] == Schema( one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.BOOLEAN)] ) @pytest.mark.parametrize( "annotation", ( ClassicPagination[DataclassGeneric[int]], OffsetPagination[DataclassGeneric[int]], CursorPagination[int, DataclassGeneric[int]], ), ) def test_schema_generation_with_pagination(annotation: Any) -> None: expected_foo_schema = Schema(type=OpenAPIType.INTEGER) expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.NULL)]) properties = get_schema_for_field_definition(FieldDefinition.from_annotation(annotation).inner_types[-1]).properties assert properties assert properties["foo"] == expected_foo_schema assert properties["annotated_foo"] == expected_foo_schema assert properties["optional_foo"] == expected_optional_foo_schema def test_schema_generation_with_ellipsis() -> None: schema = get_schema_for_field_definition(FieldDefinition.from_annotation(Tuple[int, ...])) assert isinstance(schema.items, Schema) assert schema.items.type == OpenAPIType.INTEGER def test_schema_tuple_with_union() -> None: schema = get_schema_for_field_definition(FieldDefinition.from_annotation(Tuple[int, Union[int, str]])) assert schema.prefix_items == [ Schema(type=OpenAPIType.INTEGER), Schema(one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.STRING)]), ] def test_schema_tuple() -> None: schema = get_schema_for_field_definition(FieldDefinition.from_annotation(Tuple[int, str])) assert schema == Schema( prefix_items=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.STRING)], type=OpenAPIType.ARRAY, ) def test_schema_optional_tuple() -> None: schema = get_schema_for_field_definition(FieldDefinition.from_annotation(Optional[Tuple[int, str]])) assert schema == Schema( one_of=[ Schema( prefix_items=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.STRING)], type=OpenAPIType.ARRAY, ), Schema(type=OpenAPIType.NULL), ], ) def test_optional_enum() -> None: class Foo(Enum): A = 1 B = "b" creator = SchemaCreator(plugins=openapi_schema_plugins) schema = creator.for_field_definition(FieldDefinition.from_annotation(Optional[Foo])) assert isinstance(schema, Schema) assert schema.type is None assert schema.one_of is not None null_schema = schema.one_of[1] assert isinstance(null_schema, Schema) assert null_schema.type is not None assert null_schema.type is OpenAPIType.NULL enum_ref = schema.one_of[0] assert isinstance(enum_ref, Reference) assert enum_ref.ref == "#/components/schemas/tests_unit_test_openapi_test_schema_test_optional_enum.Foo" enum_schema = creator.schema_registry.from_reference(enum_ref).schema assert enum_schema.type assert set(enum_schema.type) == {OpenAPIType.INTEGER, OpenAPIType.STRING} assert enum_schema.enum assert enum_schema.enum[0] == 1 assert enum_schema.enum[1] == "b" def test_optional_str_specified_enum() -> None: class StringEnum(str, Enum): A = "a" B = "b" creator = SchemaCreator(plugins=openapi_schema_plugins) schema = creator.for_field_definition(FieldDefinition.from_annotation(Optional[StringEnum])) assert isinstance(schema, Schema) assert schema.type is None assert schema.one_of is not None enum_ref = schema.one_of[0] assert isinstance(enum_ref, Reference) assert ( enum_ref.ref == "#/components/schemas/tests_unit_test_openapi_test_schema_test_optional_str_specified_enum.StringEnum" ) enum_schema = creator.schema_registry.from_reference(enum_ref).schema assert enum_schema.type assert enum_schema.type == OpenAPIType.STRING assert enum_schema.enum assert enum_schema.enum[0] == "a" assert enum_schema.enum[1] == "b" null_schema = schema.one_of[1] assert isinstance(null_schema, Schema) assert null_schema.type is not None assert null_schema.type is OpenAPIType.NULL def test_optional_int_specified_enum() -> None: class IntEnum(int, Enum): A = 1 B = 2 creator = SchemaCreator(plugins=openapi_schema_plugins) schema = creator.for_field_definition(FieldDefinition.from_annotation(Optional[IntEnum])) assert isinstance(schema, Schema) assert schema.type is None assert schema.one_of is not None enum_ref = schema.one_of[0] assert isinstance(enum_ref, Reference) assert ( enum_ref.ref == "#/components/schemas/tests_unit_test_openapi_test_schema_test_optional_int_specified_enum.IntEnum" ) enum_schema = creator.schema_registry.from_reference(enum_ref).schema assert enum_schema.type assert enum_schema.type == OpenAPIType.INTEGER assert enum_schema.enum assert enum_schema.enum[0] == 1 assert enum_schema.enum[1] == 2 null_schema = schema.one_of[1] assert isinstance(null_schema, Schema) assert null_schema.type is not None assert null_schema.type is OpenAPIType.NULL def test_optional_literal() -> None: schema = get_schema_for_field_definition(FieldDefinition.from_annotation(Optional[Literal[1]])) assert schema.type is not None assert set(schema.type) == {OpenAPIType.INTEGER, OpenAPIType.NULL} assert schema.enum == [1, None] def test_not_generating_examples_property() -> None: with_examples = SchemaCreator(generate_examples=True) without_examples = with_examples.not_generating_examples assert without_examples.generate_examples is False def test_process_schema_result_with_unregistered_object_schema() -> None: """This test ensures that if a schema is created for an object and not registered in the schema registry, the schema is returned as-is, and not referenced. """ schema = Schema(title="has title", type=OpenAPIType.OBJECT) field_definition = FieldDefinition.from_annotation(dict) assert SchemaCreator().process_schema_result(field_definition, schema) is schema @pytest.mark.parametrize("base_type", [msgspec.Struct, TypedDict, dataclass]) def test_type_union(base_type: type) -> None: if base_type is dataclass: @dataclass class ModelA: # pyright: ignore pass @dataclass class ModelB: # pyright: ignore pass else: class ModelA(base_type): # type: ignore[no-redef, misc] pass class ModelB(base_type): # type: ignore[no-redef, misc] pass schema = get_schema_for_field_definition( FieldDefinition.from_kwarg(name="Lookup", annotation=Union[ModelA, ModelB]) ) assert schema.one_of == [ Reference(ref="#/components/schemas/tests_unit_test_openapi_test_schema_test_type_union.ModelA"), Reference(ref="#/components/schemas/tests_unit_test_openapi_test_schema_test_type_union.ModelB"), ] @pytest.mark.parametrize("base_type", [msgspec.Struct, TypedDict, dataclass]) def test_type_union_with_none(base_type: type) -> None: # https://github.com/litestar-org/litestar/issues/2971 if base_type is dataclass: @dataclass class ModelA: # pyright: ignore pass @dataclass class ModelB: # pyright: ignore pass else: class ModelA(base_type): # type: ignore[no-redef, misc] pass class ModelB(base_type): # type: ignore[no-redef, misc] pass schema = get_schema_for_field_definition( FieldDefinition.from_kwarg(name="Lookup", annotation=Union[ModelA, ModelB, None]) ) assert schema.one_of == [ Reference(ref="#/components/schemas/tests_unit_test_openapi_test_schema_test_type_union_with_none.ModelA"), Reference("#/components/schemas/tests_unit_test_openapi_test_schema_test_type_union_with_none.ModelB"), Schema(type=OpenAPIType.NULL), ] def test_default_only_on_field_definition() -> None: field_definition = FieldDefinition.from_annotation(int, default=10) assert field_definition.kwarg_definition is None schema = get_schema_for_field_definition(field_definition) assert schema.default == 10 def test_default_not_provided_for_kwarg_but_for_field() -> None: field_definition = FieldDefinition.from_annotation(int, default=10, kwarg_definition=KwargDefinition()) schema = get_schema_for_field_definition(field_definition) assert schema.default == 10 def test_routes_with_different_path_param_types_get_merged() -> None: # https://github.com/litestar-org/litestar/issues/2700 @get("/{param:int}") async def get_handler(param: int) -> None: pass @post("/{param:str}") async def post_handler(param: str) -> None: pass app = Litestar([get_handler, post_handler]) assert app.openapi_schema.paths paths = app.openapi_schema.paths["/{param}"] assert paths.get is not None assert paths.post is not None def test_unconsumed_path_parameters_are_documented() -> None: # https://github.com/litestar-org/litestar/issues/3290 # https://github.com/litestar-org/litestar/issues/3369 async def dd(param3: Annotated[str, Parameter(description="123")]) -> str: return param3 async def d(dep_dep: str, param2: Annotated[str, Parameter(description="abc")]) -> str: return f"{dep_dep}_{param2}" @get("/{param1:str}/{param2:str}/{param3:str}", dependencies={"dep": d, "dep_dep": dd}) async def handler(dep: str) -> None: pass app = Litestar([handler]) params = app.openapi_schema.paths["/{param1}/{param2}/{param3}"].get.parameters # type: ignore[index, union-attr] assert params assert len(params) == 3 for i, param in enumerate(sorted(params, key=lambda p: p.name), 1): # pyright: ignore assert isinstance(param, OpenAPIParameter) assert param.name == f"param{i}" assert param.required is True assert param.param_in is ParamType.PATH def test_type_alias_type() -> None: @get("/") def handler(query_param: Annotated[TypeAliasType("IntAlias", int), Parameter(description="foo")]) -> None: # type: ignore[valid-type] pass app = Litestar([handler]) param = app.openapi_schema.paths["/"].get.parameters[0] # type: ignore[index, union-attr] assert param.schema.type is OpenAPIType.INTEGER # type: ignore[union-attr] # ensure other attributes than the plain type are carried over correctly assert param.description == "foo" @pytest.mark.skipif(sys.version_info < (3, 12), reason="type keyword not available before 3.12") def test_type_alias_type_keyword() -> None: ctx: Dict[str, Any] = {} exec("type IntAlias = int", ctx, None) annotation = ctx["IntAlias"] @get("/") def handler(query_param: Annotated[annotation, Parameter(description="foo")]) -> None: # type: ignore[valid-type] pass app = Litestar([handler]) param = app.openapi_schema.paths["/"].get.parameters[0] # type: ignore[union-attr, index] assert param.schema.type is OpenAPIType.INTEGER # type: ignore[union-attr] # ensure other attributes than the plain type are carried over correctly assert param.description == "foo" litestar-2.16.0/tests/unit/test_openapi/test_security_schemes.py000066400000000000000000000107211500564371300252360ustar00rootroot00000000000000from typing import TYPE_CHECKING, Any import pytest from litestar import Controller, Litestar, Router, get from litestar.openapi.config import OpenAPIConfig from litestar.openapi.spec import Components from litestar.openapi.spec.security_scheme import SecurityScheme if TYPE_CHECKING: from litestar.handlers.http_handlers import HTTPRouteHandler @pytest.fixture() def public_route() -> "HTTPRouteHandler": @get("/handler") def _handler() -> Any: ... return _handler @pytest.fixture() def protected_route() -> "HTTPRouteHandler": @get("/protected", security=[{"BearerToken": []}]) def _handler() -> Any: ... return _handler def test_schema_without_security_property(public_route: "HTTPRouteHandler") -> None: app = Litestar(route_handlers=[public_route]) schema = app.openapi_schema assert schema assert schema.components assert not schema.components.security_schemes def test_schema_with_security_scheme_defined(public_route: "HTTPRouteHandler") -> None: app = Litestar( route_handlers=[public_route], openapi_config=OpenAPIConfig( title="test app", version="0.0.1", components=Components( security_schemes={ "BearerToken": SecurityScheme( type="http", scheme="bearer", ) }, ), security=[{"BearerToken": []}], ), ) schema = app.openapi_schema assert schema schema_dict = schema.to_schema() schema_components = schema_dict.get("components", {}) assert "securitySchemes" in schema_components assert schema_components.get("securitySchemes", {}) == { "BearerToken": { "type": "http", "scheme": "bearer", } } assert schema_dict.get("security", []) == [{"BearerToken": []}] def test_schema_with_route_security_overridden(protected_route: "HTTPRouteHandler") -> None: app = Litestar( route_handlers=[protected_route], openapi_config=OpenAPIConfig( title="test app", version="0.0.1", components=Components( security_schemes={ "BearerToken": SecurityScheme( type="http", scheme="bearer", ) }, ), ), ) schema = app.openapi_schema assert schema schema_dict = schema.to_schema() route = schema_dict["paths"]["/protected"]["get"] assert route.get("security", None) == [{"BearerToken": []}] def test_layered_security_declaration() -> None: class MyController(Controller): path = "/controller" security = [{"controllerToken": []}] # pyright: ignore @get("", security=[{"handlerToken": []}]) def my_handler(self) -> None: ... router = Router("/router", route_handlers=[MyController], security=[{"routerToken": []}]) app = Litestar( route_handlers=[router], security=[{"appToken": []}], openapi_config=OpenAPIConfig( title="test app", version="0.0.1", components=Components( security_schemes={ "handlerToken": SecurityScheme( type="http", scheme="bearer", ), "controllerToken": SecurityScheme( type="http", scheme="bearer", ), "routerToken": SecurityScheme( type="http", scheme="bearer", ), "appToken": SecurityScheme( type="http", scheme="bearer", ), }, ), ), ) assert app.openapi_schema assert app.openapi_schema.components security_schemes = app.openapi_schema.components.security_schemes assert security_schemes assert list(security_schemes.keys()) == [ "handlerToken", "controllerToken", "routerToken", "appToken", ] assert app.openapi_schema paths = app.openapi_schema.paths assert paths assert paths["/router/controller"].get assert paths["/router/controller"].get.security == [ {"appToken": []}, {"routerToken": []}, {"controllerToken": []}, {"handlerToken": []}, ] litestar-2.16.0/tests/unit/test_openapi/test_spec_generation.py000066400000000000000000000144421500564371300250310ustar00rootroot00000000000000import sys from types import ModuleType from typing import Any, Callable import pytest from msgspec import Struct from litestar import delete, post from litestar.openapi import ResponseSpec from litestar.openapi.spec import OpenAPI from litestar.status_codes import HTTP_204_NO_CONTENT from litestar.testing import create_test_client from tests.models import DataclassPerson, MsgSpecStructPerson, TypedDictPerson @pytest.mark.parametrize("cls", (DataclassPerson, TypedDictPerson, MsgSpecStructPerson)) def test_spec_generation(cls: Any) -> None: @post("/") def handler(data: cls) -> cls: return data with create_test_client(handler) as client: schema = client.app.openapi_schema assert schema assert schema.to_schema()["components"]["schemas"][cls.__name__] == { "properties": { "first_name": {"type": "string"}, "last_name": {"type": "string"}, "id": {"type": "string"}, "optional": {"oneOf": [{"type": "string"}, {"type": "null"}]}, "complex": { "type": "object", "additionalProperties": { "type": "array", "items": {"type": "object", "additionalProperties": {"type": "string"}}, }, }, "pets": { "oneOf": [ { "items": {"$ref": "#/components/schemas/DataclassPet"}, "type": "array", }, {"type": "null"}, ] }, }, "type": "object", "required": ["complex", "first_name", "id", "last_name"], "title": f"{cls.__name__}", } def test_spec_generation_no_content() -> None: @delete( "/", status_code=HTTP_204_NO_CONTENT, responses={204: ResponseSpec(None, description="Custom response")}, ) def handler() -> None: return None with create_test_client(handler) as client: schema: OpenAPI = client.app.openapi_schema assert schema.to_schema()["paths"] == { "/": { "delete": { "summary": "Handler", "deprecated": False, "operationId": "Handler", "responses": { "204": { "description": "Custom response", } }, }, }, } def test_msgspec_schema() -> None: class CamelizedStruct(Struct, rename="camel"): field_one: int field_two: float @post("/") def handler(data: CamelizedStruct) -> CamelizedStruct: return data with create_test_client(handler) as client: schema = client.app.openapi_schema assert schema assert schema.to_schema()["components"]["schemas"]["test_msgspec_schema.CamelizedStruct"] == { "properties": {"fieldOne": {"type": "integer"}, "fieldTwo": {"type": "number"}}, "required": ["fieldOne", "fieldTwo"], "title": "CamelizedStruct", "type": "object", } @pytest.fixture() def py_38_module_content() -> str: return """ from __future__ import annotations from typing import List, Optional from msgspec import Struct from litestar import Litestar, get class A(Struct): a: A b: B opt_a: Optional[A] = None opt_b: Optional[B] = None list_a: List[A] = [] list_b: List[B] = [] class B(Struct): a: A b: B opt_a: Optional[A] = None opt_b: Optional[B] = None list_a: List[A] = [] list_b: List[B] = [] @get("/") async def test() -> A: return A() """ @pytest.fixture() def py_310_module_content() -> str: return """ from __future__ import annotations from msgspec import Struct from litestar import Litestar, get class A(Struct): a: A b: B opt_a: A | None = None opt_b: B | None = None list_a: list[A] = [] list_b: list[B] = [] class B(Struct): a: A b: B opt_a: A | None = None opt_b: B | None = None list_a: list[A] = [] list_b: list[B] = [] @get("/") async def test() -> A: return A() """ @pytest.mark.parametrize( ("fixture_name",), [ ("py_38_module_content",), pytest.param( "py_310_module_content", marks=pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python 3.10", ), ), ], ) def test_recursive_schema_generation( fixture_name: str, create_module: Callable[[str], ModuleType], request: pytest.FixtureRequest ) -> None: module_content = request.getfixturevalue(fixture_name) module = create_module(module_content) with create_test_client(module.test, debug=True) as client: schema = client.app.openapi_schema assert schema assert schema.to_schema()["components"]["schemas"]["A"] == { "required": ["a", "b"], "properties": { "a": {"$ref": "#/components/schemas/A"}, "b": {"$ref": "#/components/schemas/B"}, "opt_a": {"oneOf": [{"$ref": "#/components/schemas/A"}, {"type": "null"}]}, "opt_b": {"oneOf": [{"$ref": "#/components/schemas/B"}, {"type": "null"}]}, "list_a": {"items": {"$ref": "#/components/schemas/A"}, "type": "array"}, "list_b": {"items": {"$ref": "#/components/schemas/B"}, "type": "array"}, }, "type": "object", "title": "A", } assert schema.to_schema()["components"]["schemas"]["B"] == { "required": ["a", "b"], "properties": { "a": {"$ref": "#/components/schemas/A"}, "b": {"$ref": "#/components/schemas/B"}, "opt_a": {"oneOf": [{"$ref": "#/components/schemas/A"}, {"type": "null"}]}, "opt_b": {"oneOf": [{"$ref": "#/components/schemas/B"}, {"type": "null"}]}, "list_a": {"items": {"$ref": "#/components/schemas/A"}, "type": "array"}, "list_b": {"items": {"$ref": "#/components/schemas/B"}, "type": "array"}, }, "type": "object", "title": "B", } litestar-2.16.0/tests/unit/test_openapi/test_tags.py000066400000000000000000000032131500564371300226140ustar00rootroot00000000000000from typing import TYPE_CHECKING, Any, Type import pytest from litestar import Controller, Litestar, Router, get from litestar.handlers.http_handlers import HTTPRouteHandler if TYPE_CHECKING: from litestar.openapi.spec.open_api import OpenAPI @pytest.fixture() def handler() -> HTTPRouteHandler: @get("/handler", tags=["handler"]) def _handler() -> Any: ... return _handler @pytest.fixture() def controller() -> Type[Controller]: class _Controller(Controller): path = "/controller" tags = ["controller"] @get(tags=["handler", "a"]) def _handler(self) -> Any: ... return _Controller @pytest.fixture() def router(controller: Type[Controller]) -> Router: return Router(path="/router", route_handlers=[controller], tags=["router"]) @pytest.fixture() def app(handler: HTTPRouteHandler, controller: Type[Controller], router: Router) -> Litestar: return Litestar(route_handlers=[handler, controller, router]) @pytest.fixture() def openapi_schema(app: Litestar) -> "OpenAPI": return app.openapi_schema def test_openapi_schema_handler_tags(openapi_schema: "OpenAPI") -> None: assert openapi_schema.paths["/handler"].get.tags == ["handler"] # type: ignore[index, union-attr] def test_openapi_schema_controller_tags(openapi_schema: "OpenAPI") -> None: assert openapi_schema.paths["/controller"].get.tags == ["a", "controller", "handler"] # type: ignore[index, union-attr] def test_openapi_schema_router_tags(openapi_schema: "OpenAPI") -> None: assert openapi_schema.paths["/router/controller"].get.tags == ["a", "controller", "handler", "router"] # type: ignore[index, union-attr] litestar-2.16.0/tests/unit/test_openapi/test_typescript_converter/000077500000000000000000000000001500564371300256025ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_openapi/test_typescript_converter/__init__.py000066400000000000000000000000001500564371300277010ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_openapi/test_typescript_converter/test_converter.py000066400000000000000000000207171500564371300312310ustar00rootroot00000000000000from typing import Type from polyfactory import BaseFactory from litestar import Controller, Litestar from litestar._openapi.typescript_converter.converter import ( convert_openapi_to_typescript, ) def test_openapi_to_typescript_converter(person_controller: Type[Controller], pet_controller: Type[Controller]) -> None: BaseFactory.seed_random(1) app = Litestar(route_handlers=[person_controller, pet_controller]) assert app.openapi_schema result = convert_openapi_to_typescript(openapi_schema=app.openapi_schema) assert ( result.write() == """export namespace API { export namespace PetOwnerOrPetGetPetsOrOwners { export namespace Http200 { export type ResponseBody = ({ age: number; name: string; species?: "Cat" | "Dog" | "Monkey" | "Pig"; } | { complex: { }; first_name: string; id: string; last_name: string; optional?: null | string; pets?: null | { age: number; name: string; species?: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; })[]; export interface ResponseHeaders { "x-my-tag"?: string; }; }; export namespace Http406 { export type ResponseBody = { detail: string; extra?: Record | null | unknown[]; status_code: number; }; }; }; export namespace PetPets { export namespace Http200 { export type ResponseBody = { age: number; name: string; species?: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }; }; export namespace ServiceIdPersonBulkBulkCreatePerson { export interface HeaderParameters { secret: string; }; export namespace Http201 { export type ResponseBody = { complex: { }; first_name: string; id: string; last_name: string; optional: null | string; pets: null | { age: number; name: string; species: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }[]; }; export namespace Http400 { export type ResponseBody = { detail: string; extra?: Record | null | unknown[]; status_code: number; }; }; export interface PathParameters { service_id: number; }; export type RequestBody = { complex: { }; first_name: string; id: string; last_name: string; optional: null | string; pets: null | { age: number; name: string; species: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }[]; }; export namespace ServiceIdPersonBulkBulkPartialUpdatePerson { export interface HeaderParameters { secret: string; }; export namespace Http200 { export type ResponseBody = { complex: { }; first_name: string; id: string; last_name: string; optional: null | string; pets: null | { age: number; name: string; species: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }[]; }; export namespace Http400 { export type ResponseBody = { detail: string; extra?: Record | null | unknown[]; status_code: number; }; }; export interface PathParameters { service_id: number; }; export type RequestBody = { complex: { }; first_name: string; id: string; last_name: string; optional: null | string; pets: null | { age: number; name: string; species: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }[]; }; export namespace ServiceIdPersonBulkBulkUpdatePerson { export interface HeaderParameters { secret: string; }; export namespace Http200 { export type ResponseBody = { complex: { }; first_name: string; id: string; last_name: string; optional?: null | string; pets?: null | { age: number; name: string; species?: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }[]; }; export namespace Http400 { export type ResponseBody = { detail: string; extra?: Record | null | unknown[]; status_code: number; }; }; export interface PathParameters { service_id: number; }; export type RequestBody = { complex: { }; first_name: string; id: string; last_name: string; optional?: null | string; pets?: null | { age: number; name: string; species?: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }[]; }; export namespace ServiceIdPersonCreatePerson { export interface HeaderParameters { secret: string; }; export namespace Http201 { export type ResponseBody = { complex: { }; first_name: string; id: string; last_name: string; optional?: null | string; pets?: null | { age: number; name: string; species?: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }; }; export namespace Http400 { export type ResponseBody = { detail: string; extra?: Record | null | unknown[]; status_code: number; }; }; export interface PathParameters { service_id: number; }; export type RequestBody = { complex: { }; first_name: string; id: string; last_name: string; optional?: null | string; pets?: null | { age: number; name: string; species?: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }; }; export namespace ServiceIdPersonDataclassGetPersonDataclass { export namespace Http200 { export type ResponseBody = { complex: { }; first_name: string; id: string; last_name: string; optional?: null | string; pets?: null | { age: number; name: string; species?: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }; }; export namespace Http400 { export type ResponseBody = { detail: string; extra?: Record | null | unknown[]; status_code: number; }; }; export interface PathParameters { service_id: number; }; }; export namespace ServiceIdPersonGetPersons { export interface CookieParameters { value: number; }; export interface HeaderParameters { secret: string; }; export namespace Http200 { export type ResponseBody = { complex: { }; first_name: string; id: string; last_name: string; optional?: null | string; pets?: null | { age: number; name: string; species?: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }[]; }; export namespace Http400 { export type ResponseBody = { detail: string; extra?: Record | null | unknown[]; status_code: number; }; }; export interface PathParameters { service_id: number; }; export interface QueryParameters { from_date?: null | number | string | string; gender?: "A" | "F" | "M" | "O" | ("A" | "F" | "M" | "O")[] | null; lucky_number?: 2 | 7 | null; name?: null | string | string[]; page: number; pageSize: number; to_date?: null | number | string | string; }; }; export namespace ServiceIdPersonPersonIdDeletePerson { export namespace Http204 { export type ResponseBody = undefined; }; export namespace Http400 { export type ResponseBody = { detail: string; extra?: Record | null | unknown[]; status_code: number; }; }; export interface PathParameters { person_id: string; service_id: number; }; }; export namespace ServiceIdPersonPersonIdGetPersonById { export namespace Http200 { export type ResponseBody = { complex: { }; first_name: string; id: string; last_name: string; optional?: null | string; pets?: null | { age: number; name: string; species?: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }; }; export namespace Http400 { export type ResponseBody = { detail: string; extra?: Record | null | unknown[]; status_code: number; }; }; export interface PathParameters { person_id: string; service_id: number; }; }; export namespace ServiceIdPersonPersonIdPartialUpdatePerson { export namespace Http200 { export type ResponseBody = { complex: { }; first_name: string; id: string; last_name: string; optional: null | string; pets: null | { age: number; name: string; species: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }; }; export namespace Http400 { export type ResponseBody = { detail: string; extra?: Record | null | unknown[]; status_code: number; }; }; export interface PathParameters { person_id: string; service_id: number; }; export type RequestBody = { complex: { }; first_name: string; id: string; last_name: string; optional: null | string; pets: null | { age: number; name: string; species: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }; }; export namespace ServiceIdPersonPersonIdUpdatePerson { export namespace Http200 { export type ResponseBody = { complex: { }; first_name: string; id: string; last_name: string; optional?: null | string; pets?: null | { age: number; name: string; species?: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }; }; export namespace Http400 { export type ResponseBody = { detail: string; extra?: Record | null | unknown[]; status_code: number; }; }; export interface PathParameters { person_id: string; service_id: number; }; export type RequestBody = { complex: { }; first_name: string; id: string; last_name: string; optional?: null | string; pets?: null | { age: number; name: string; species?: "Cat" | "Dog" | "Monkey" | "Pig"; }[]; }; }; };""" ) litestar-2.16.0/tests/unit/test_openapi/test_typescript_converter/test_schema_parsing.py000066400000000000000000000061501500564371300322000ustar00rootroot00000000000000import string from typing import Any, List import pytest from litestar._openapi.typescript_converter.schema_parsing import normalize_typescript_namespace, parse_schema from litestar._openapi.typescript_converter.types import TypeScriptIntersection from litestar.openapi.spec import Schema from litestar.openapi.spec.enums import OpenAPIType object_schema_1 = Schema( type=OpenAPIType.OBJECT, properties={ "first_1": Schema( type=OpenAPIType.STRING, ), "second_1": Schema(type=[OpenAPIType.NUMBER, OpenAPIType.NULL]), }, required=["first_1"], ) object_schema_2 = Schema( type=OpenAPIType.OBJECT, properties={ "first_2": Schema( type=OpenAPIType.BOOLEAN, ), "second_2": Schema( type=OpenAPIType.INTEGER, ), }, required=["first_2"], ) string_schema = Schema(type=[OpenAPIType.STRING]) number_schema = Schema(type=[OpenAPIType.NUMBER]) nullable_integer_schema = Schema(type=[OpenAPIType.INTEGER, OpenAPIType.NULL]) array_schema = Schema(type=OpenAPIType.ARRAY, items=Schema(one_of=[object_schema_1, object_schema_2])) def test_parse_schema_handle_all_of() -> None: result = parse_schema(Schema(all_of=[object_schema_1, object_schema_2])) assert isinstance(result, TypeScriptIntersection) assert ( result.write() == "{\n\tfirst_1: string;\n\tsecond_1?: null | number;\n} & {\n\tfirst_2: boolean;\n\tsecond_2?: number;\n}" ) def test_parse_schema_handle_one_of() -> None: result = parse_schema( Schema(one_of=[object_schema_1, object_schema_2, number_schema, string_schema, nullable_integer_schema]) ) assert ( result.write() == "null | number | number | string | {\n" "\tfirst_1: string;\n" "\tsecond_1?: null | number;\n" "} | {\n" "\tfirst_2: boolean;\n" "\tsecond_2?: number;\n" "}" ) def test_parse_schema_handle_array() -> None: result = parse_schema(array_schema) assert ( result.write() == "({\n\tfirst_1: string;\n\tsecond_1?: null | number;\n} | {\n\tfirst_2: boolean;\n\tsecond_2?: number;\n})[]" ) def test_parse_schema_handle_object() -> None: result = parse_schema(object_schema_1) assert result.write() == "{\n\tfirst_1: string;\n\tsecond_1?: null | number;\n}" @pytest.mark.parametrize( "schema_type, enum, expected", ( (OpenAPIType.STRING, ["a", "b", "c"], '"a" | "b" | "c"'), (OpenAPIType.NUMBER, [1, 2, 3], "1 | 2 | 3"), ( [OpenAPIType.NULL, OpenAPIType.BOOLEAN, OpenAPIType.STRING], [None, True, False, "moishe"], '"moishe" | false | null | true', ), ), ) def test_parse_schema_handle_enum(schema_type: Any, enum: List[Any], expected: str) -> None: result = parse_schema(Schema(type=schema_type, enum=enum)) assert result.write() == expected @pytest.mark.parametrize("namespace", [string.punctuation]) def test_normalize_typescript_namespace_invalid_namespace_raises(namespace: str) -> None: with pytest.raises(ValueError): normalize_typescript_namespace(namespace, False) litestar-2.16.0/tests/unit/test_openapi/test_typescript_converter/test_typescript_types.py000066400000000000000000000100351500564371300326440ustar00rootroot00000000000000from typing import Any import pytest from litestar._openapi.typescript_converter.types import ( TypeScriptAnonymousInterface, TypeScriptArray, TypeScriptConst, TypeScriptEnum, TypeScriptInterface, TypeScriptIntersection, TypeScriptLiteral, TypeScriptNamespace, TypeScriptPrimitive, TypeScriptProperty, TypeScriptType, TypeScriptUnion, ) @pytest.mark.parametrize("value", ("string", "number", "boolean", "any", "null", "undefined", "symbol")) def test_typescript_primitive(value: Any) -> None: assert TypeScriptPrimitive(value).write() == value def test_typescript_intersection() -> None: intersection = TypeScriptIntersection(types=(TypeScriptPrimitive("string"), TypeScriptPrimitive("number"))) assert intersection.write() == "string & number" def test_typescript_union() -> None: union = TypeScriptUnion(types=(TypeScriptPrimitive("string"), TypeScriptPrimitive("number"))) assert union.write() == "number | string" @pytest.mark.parametrize( "value, expected", (("abc", '"abc"'), (123, "123"), (100.123, "100.123"), (True, "true"), (False, "false")) ) def test_typescript_literal(value: Any, expected: str) -> None: assert TypeScriptLiteral(value).write() == expected @pytest.mark.parametrize( "value, expected", ( (TypeScriptPrimitive("string"), "string[]"), (TypeScriptUnion(types=(TypeScriptPrimitive("string"), TypeScriptPrimitive("number"))), "(number | string)[]"), ), ) def test_typescript_array(value: Any, expected: str) -> None: assert TypeScriptArray(item_type=value).write() == expected def test_typescript_property() -> None: prop = TypeScriptProperty(required=True, key="myKey", value=TypeScriptPrimitive("string")) assert prop.write() == "myKey: string;" prop.required = False assert prop.write() == "myKey?: string;" def test_typescript_anonymous_interface() -> None: first_prop = TypeScriptProperty(required=True, key="aProp", value=TypeScriptPrimitive("string")) second_prop = TypeScriptProperty(required=True, key="bProp", value=TypeScriptPrimitive("number")) interface = TypeScriptAnonymousInterface(properties=(first_prop, second_prop)) assert interface.write() == "{\n\taProp: string;\n\tbProp: number;\n}" def test_typescript_named_interface() -> None: first_prop = TypeScriptProperty(required=True, key="aProp", value=TypeScriptPrimitive("string")) second_prop = TypeScriptProperty(required=True, key="bProp", value=TypeScriptPrimitive("number")) interface = TypeScriptInterface(name="MyInterface", properties=(first_prop, second_prop)) assert interface.write() == "export interface MyInterface {\n\taProp: string;\n\tbProp: number;\n};" def test_typescript_enum() -> None: enum = TypeScriptEnum(name="MyEnum", values=(("FIRST", "a"), ("SECOND", "b"))) assert enum.write() == 'export enum MyEnum {\n\tFIRST = "a",\n\tSECOND = "b",\n};' def test_typescript_type() -> None: ts_type = TypeScriptType( name="MyUnion", value=TypeScriptUnion(types=(TypeScriptPrimitive("string"), TypeScriptPrimitive("number"))) ) assert ts_type.write() == "export type MyUnion = number | string;" def test_typescript_const() -> None: const = TypeScriptConst(name="MyConstant", value=TypeScriptPrimitive("number")) assert const.write() == "export const MyConstant: number;" def test_typescript_namespace() -> None: first_prop = TypeScriptProperty(required=True, key="aProp", value=TypeScriptPrimitive("string")) second_prop = TypeScriptProperty(required=True, key="bProp", value=TypeScriptPrimitive("number")) interface = TypeScriptInterface(name="MyInterface", properties=(first_prop, second_prop)) enum = TypeScriptEnum(name="MyEnum", values=(("FIRST", "a"), ("SECOND", "b"))) namespace = TypeScriptNamespace("MyNamespace", values=(interface, enum)) assert ( namespace.write() == 'export namespace MyNamespace {\n\texport enum MyEnum {\n\tFIRST = "a",\n\tSECOND = "b",\n};\n\n\texport interface MyInterface {\n\taProp: string;\n\tbProp: number;\n};\n};' ) litestar-2.16.0/tests/unit/test_openapi/utils.py000066400000000000000000000004231500564371300217570ustar00rootroot00000000000000from enum import Enum from litestar.exceptions import HTTPException class PetException(HTTPException): status_code = 406 class Gender(str, Enum): MALE = "M" FEMALE = "F" OTHER = "O" ANY = "A" class LuckyNumber(int, Enum): TWO = 2 SEVEN = 7 litestar-2.16.0/tests/unit/test_pagination.py000066400000000000000000000265101500564371300213220ustar00rootroot00000000000000from itertools import islice from typing import Any, List, Optional, Tuple import pytest from litestar import get from litestar.app import DEFAULT_OPENAPI_CONFIG from litestar.pagination import ( AbstractAsyncClassicPaginator, AbstractAsyncCursorPaginator, AbstractAsyncOffsetPaginator, AbstractSyncClassicPaginator, AbstractSyncCursorPaginator, AbstractSyncOffsetPaginator, ClassicPagination, CursorPagination, OffsetPagination, ) from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from tests.models import DataclassPerson, DataclassPersonFactory class TestSyncClassicPaginator(AbstractSyncClassicPaginator[DataclassPerson]): __test__ = False def __init__(self, data: List[DataclassPerson]): self.data = data def get_total(self, page_size: int) -> int: return round(len(self.data) / page_size) def get_items(self, page_size: int, current_page: int) -> List[DataclassPerson]: return [self.data[i : i + page_size] for i in range(0, len(self.data), page_size)][current_page - 1] class TestAsyncClassicPaginator(AbstractAsyncClassicPaginator[DataclassPerson]): __test__ = False def __init__(self, data: List[DataclassPerson]): self.data = data async def get_total(self, page_size: int) -> int: return round(len(self.data) / page_size) async def get_items(self, page_size: int, current_page: int) -> List[DataclassPerson]: return [self.data[i : i + page_size] for i in range(0, len(self.data), page_size)][current_page - 1] class TestSyncOffsetPaginator(AbstractSyncOffsetPaginator[DataclassPerson]): __test__ = False def __init__(self, data: List[DataclassPerson]): self.data = data def get_total(self) -> int: return len(self.data) def get_items(self, limit: int, offset: int) -> List[DataclassPerson]: return list(islice(islice(self.data, offset, None), limit)) class TestAsyncOffsetPaginator(AbstractAsyncOffsetPaginator[DataclassPerson]): __test__ = False def __init__(self, data: List[DataclassPerson]): self.data = data async def get_total(self) -> int: return len(self.data) async def get_items(self, limit: int, offset: int) -> List[DataclassPerson]: return list(islice(islice(self.data, offset, None), limit)) data = DataclassPersonFactory.batch(50) @pytest.mark.parametrize("paginator", (TestSyncClassicPaginator(data=data), TestAsyncClassicPaginator(data=data))) def test_classic_pagination_data_shape(paginator: Any) -> None: @get("/async") async def async_handler(page_size: int, current_page: int) -> ClassicPagination[DataclassPerson]: return await paginator(page_size=page_size, current_page=current_page) # type: ignore[no-any-return] @get("/sync") def sync_handler(page_size: int, current_page: int) -> ClassicPagination[DataclassPerson]: return paginator(page_size=page_size, current_page=current_page) # type: ignore[no-any-return] with create_test_client([async_handler, sync_handler]) as client: if isinstance(paginator, TestSyncClassicPaginator): response = client.get("/sync", params={"page_size": 5, "current_page": 1}) else: response = client.get("/async", params={"page_size": 5, "current_page": 1}) assert response.status_code == HTTP_200_OK response_data = response.json() assert len(response_data["items"]) == 5 assert response_data["total_pages"] == 10 assert response_data["page_size"] == 5 assert response_data["current_page"] == 1 @pytest.mark.parametrize("paginator", (TestSyncClassicPaginator(data=data), TestAsyncClassicPaginator(data=data))) def test_classic_pagination_openapi_schema(paginator: Any) -> None: @get("/async") async def async_handler(page_size: int, current_page: int) -> ClassicPagination[DataclassPerson]: return await paginator(page_size=page_size, current_page=current_page) # type: ignore[no-any-return] @get("/sync") def sync_handler(page_size: int, current_page: int) -> ClassicPagination[DataclassPerson]: return paginator(page_size=page_size, current_page=current_page) # type: ignore[no-any-return] with create_test_client([async_handler, sync_handler], openapi_config=DEFAULT_OPENAPI_CONFIG) as client: schema = client.app.openapi_schema assert schema path = "/sync" if isinstance(paginator, TestSyncClassicPaginator) else "/async" spec = schema.to_schema()["paths"][path]["get"]["responses"]["200"]["content"]["application/json"] assert spec == { "schema": { "properties": { "items": { "items": {"$ref": "#/components/schemas/DataclassPerson"}, "type": "array", }, "page_size": {"type": "integer", "description": "Number of items per page."}, "current_page": {"type": "integer", "description": "Current page number."}, "total_pages": {"type": "integer", "description": "Total number of pages."}, }, "type": "object", } } @pytest.mark.parametrize("paginator", (TestSyncOffsetPaginator(data=data), TestAsyncOffsetPaginator(data=data))) def test_limit_offset_pagination_data_shape(paginator: Any) -> None: @get("/async") async def async_handler(limit: int, offset: int) -> OffsetPagination[DataclassPerson]: return await paginator(limit=limit, offset=offset) # type: ignore[no-any-return] @get("/sync") def sync_handler(limit: int, offset: int) -> OffsetPagination[DataclassPerson]: return paginator(limit=limit, offset=offset) # type: ignore[no-any-return] with create_test_client([async_handler, sync_handler]) as client: if isinstance(paginator, TestSyncOffsetPaginator): response = client.get("/sync", params={"limit": 5, "offset": 0}) else: response = client.get("/async", params={"limit": 5, "offset": 0}) assert response.status_code == HTTP_200_OK response_data = response.json() assert len(response_data["items"]) == 5 assert response_data["total"] == 50 assert response_data["limit"] == 5 assert response_data["offset"] == 0 @pytest.mark.parametrize("paginator", (TestSyncOffsetPaginator(data=data), TestAsyncOffsetPaginator(data=data))) def test_limit_offset_pagination_openapi_schema(paginator: Any) -> None: @get("/async") async def async_handler(limit: int, offset: int) -> OffsetPagination[DataclassPerson]: return await paginator(limit=limit, offset=offset) # type: ignore[no-any-return] @get("/sync") def sync_handler(limit: int, offset: int) -> OffsetPagination[DataclassPerson]: return paginator(limit=limit, offset=offset) # type: ignore[no-any-return] with create_test_client([async_handler, sync_handler], openapi_config=DEFAULT_OPENAPI_CONFIG) as client: schema = client.app.openapi_schema assert schema path = "/sync" if isinstance(paginator, TestSyncOffsetPaginator) else "/async" spec = schema.to_schema()["paths"][path]["get"]["responses"]["200"]["content"]["application/json"] assert spec == { "schema": { "properties": { "items": { "items": {"$ref": "#/components/schemas/DataclassPerson"}, "type": "array", }, "limit": {"type": "integer", "description": "Maximal number of items to send."}, "offset": {"type": "integer", "description": "Offset from the beginning of the query."}, "total": {"type": "integer", "description": "Total number of items."}, }, "type": "object", } } class TestSyncCursorPagination(AbstractSyncCursorPaginator[str, DataclassPerson]): __test__ = False def __init__(self, data: List[DataclassPerson]): self.data = data def get_items(self, cursor: Optional[str], results_per_page: int) -> "Tuple[List[DataclassPerson], Optional[str]]": results = self.data[:results_per_page] return results, results[-1].id class TestAsyncCursorPagination(AbstractAsyncCursorPaginator[str, DataclassPerson]): __test__ = False def __init__(self, data: List[DataclassPerson]): self.data = data async def get_items( self, cursor: Optional[str], results_per_page: int ) -> "Tuple[List[DataclassPerson], Optional[str]]": results = self.data[:results_per_page] return results, results[-1].id @pytest.mark.parametrize("paginator", (TestSyncCursorPagination(data=data), TestAsyncCursorPagination(data=data))) def test_cursor_pagination_data_shape(paginator: Any) -> None: @get("/async") async def async_handler(cursor: Optional[str] = None) -> CursorPagination[str, DataclassPerson]: return await paginator(cursor=cursor, results_per_page=5) # type: ignore[no-any-return] @get("/sync") def sync_handler(cursor: Optional[str] = None) -> CursorPagination[str, DataclassPerson]: return paginator(cursor=cursor, results_per_page=5) # type: ignore[no-any-return] with create_test_client([async_handler, sync_handler]) as client: if isinstance(paginator, TestSyncCursorPagination): response = client.get("/sync") else: response = client.get("/async") assert response.status_code == HTTP_200_OK response_data = response.json() assert len(response_data["items"]) == 5 assert response_data["results_per_page"] == 5 assert response_data["cursor"] == data[4].id @pytest.mark.parametrize("paginator", (TestSyncCursorPagination(data=data), TestAsyncCursorPagination(data=data))) def test_cursor_pagination_openapi_schema(paginator: Any) -> None: @get("/async") async def async_handler(cursor: Optional[str] = None) -> CursorPagination[str, DataclassPerson]: return await paginator(cursor=cursor, results_per_page=5) # type: ignore[no-any-return] @get("/sync") def sync_handler(cursor: Optional[str] = None) -> CursorPagination[str, DataclassPerson]: return paginator(cursor=cursor, results_per_page=5) # type: ignore[no-any-return] with create_test_client([async_handler, sync_handler], openapi_config=DEFAULT_OPENAPI_CONFIG) as client: schema = client.app.openapi_schema assert schema path = "/sync" if isinstance(paginator, TestSyncCursorPagination) else "/async" spec = schema.to_schema()["paths"][path]["get"]["responses"]["200"]["content"]["application/json"] assert spec == { "schema": { "properties": { "items": { "items": {"$ref": "#/components/schemas/DataclassPerson"}, "type": "array", }, "cursor": { "type": "string", "description": "Unique ID, designating the last identifier in the given data set. This value can be used to request the 'next' batch of records.", }, "results_per_page": {"type": "integer", "description": "Maximal number of items to send."}, }, "type": "object", } } litestar-2.16.0/tests/unit/test_params.py000066400000000000000000000254071500564371300204600ustar00rootroot00000000000000from typing import Any, Dict, Generator, List, Optional import pytest from typing_extensions import Annotated from litestar import Controller, Litestar, MediaType, get, post from litestar.di import Provide from litestar.exceptions import ImproperlyConfiguredException from litestar.params import Body, Dependency, Parameter from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR from litestar.testing import TestClient, create_test_client def test_parsing_of_parameter_as_annotated() -> None: @get(path="/") def handler(param: Annotated[str, Parameter(min_length=1)]) -> str: return param with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_400_BAD_REQUEST response = client.get("/?param=a") assert response.status_code == HTTP_200_OK def test_parsing_of_parameter_as_default() -> None: @get(path="/") def handler(param: str = Parameter(min_length=1)) -> str: return param with create_test_client(handler) as client: response = client.get("/?param=") assert response.status_code == HTTP_400_BAD_REQUEST response = client.get("/?param=a") assert response.status_code == HTTP_200_OK def test_parsing_of_body_as_annotated() -> None: @post(path="/") def handler(data: Annotated[List[str], Body(min_items=1)]) -> List[str]: return data with create_test_client(handler) as client: response = client.post("/", json=[]) assert response.status_code == HTTP_400_BAD_REQUEST response = client.post("/", json=["a"]) assert response.status_code == HTTP_201_CREATED def test_parsing_of_body_as_default() -> None: @post(path="/") def handler(data: List[str] = Body(min_items=1)) -> List[str]: return data with create_test_client(handler) as client: response = client.post("/", json=[]) assert response.status_code == HTTP_400_BAD_REQUEST response = client.post("/", json=["a"]) assert response.status_code == HTTP_201_CREATED def test_parsing_of_dependency_as_annotated() -> None: @get(path="/", dependencies={"dep": Provide(lambda: None, sync_to_thread=False)}) def handler(dep: Annotated[int, Dependency(skip_validation=True)]) -> int: return dep with create_test_client(handler) as client: response = client.get("/") assert response.text == "null" def test_parsing_of_dependency_as_default() -> None: @get(path="/", dependencies={"dep": Provide(lambda: None, sync_to_thread=False)}) def handler(dep: int = Dependency(skip_validation=True)) -> int: return dep with create_test_client(handler) as client: response = client.get("/") assert response.text == "null" @pytest.mark.parametrize( "dependency, expected", [ (Dependency(), None), (Dependency(default=None), None), (Dependency(default=13), 13), ], ) def test_dependency_defaults(dependency: Any, expected: Optional[int]) -> None: @get("/") def handler(value: Optional[int] = dependency) -> Dict[str, Optional[int]]: return {"value": value} with create_test_client(route_handlers=[handler]) as client: resp = client.get("/") assert resp.json() == {"value": expected} def test_dependency_non_optional_with_default() -> None: @get("/") def handler(value: int = Dependency(default=13)) -> Dict[str, int]: return {"value": value} with create_test_client(route_handlers=[handler]) as client: resp = client.get("/") assert resp.json() == {"value": 13} def test_dependency_no_default() -> None: @get(dependencies={"value": Provide(lambda: 13, sync_to_thread=False)}) def test(value: int = Dependency()) -> Dict[str, int]: return {"value": value} with create_test_client(route_handlers=[test]) as client: resp = client.get("/") assert resp.json() == {"value": 13} def test_dependency_not_provided_and_no_default() -> None: @get() def test(value: int = Dependency()) -> Dict[str, int]: return {"value": value} with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[test]) def test_dependency_provided_on_controller() -> None: """Ensures that we don't only consider the handler's dependencies when checking that an explicit non-optional dependency has been provided. """ class C(Controller): path = "" dependencies = {"value": Provide(lambda: 13, sync_to_thread=False)} @get() def test(self, value: int = Dependency()) -> Dict[str, int]: return {"value": value} with create_test_client(route_handlers=[C]) as client: resp = client.get("/") assert resp.json() == {"value": 13} def test_dependency_skip_validation() -> None: @get("/validated") def validated(value: int = Dependency()) -> Dict[str, int]: return {"value": value} @get("/skipped") def skipped(value: int = Dependency(skip_validation=True)) -> Dict[str, int]: return {"value": value} with create_test_client( route_handlers=[validated, skipped], dependencies={"value": Provide(lambda: "str", sync_to_thread=False)} ) as client: validated_resp = client.get("/validated") assert validated_resp.status_code == HTTP_500_INTERNAL_SERVER_ERROR skipped_resp = client.get("/skipped") assert skipped_resp.status_code == HTTP_200_OK assert skipped_resp.json() == {"value": "str"} def test_dependency_skip_validation_with_default() -> None: @get("/skipped") def skipped(value: int = Dependency(default=1, skip_validation=True)) -> Dict[str, int]: return {"value": value} with create_test_client(route_handlers=[skipped]) as client: skipped_resp = client.get("/skipped") assert skipped_resp.status_code == HTTP_200_OK assert skipped_resp.json() == {"value": 1} def test_dependency_nested_sequence() -> None: class Obj: def __init__(self, seq: List[str]) -> None: self.seq = seq async def provides_obj(seq: List[str]) -> Obj: return Obj(seq) @get("/obj") def get_obj(obj: Obj) -> List[str]: return obj.seq @get("/seq") def get_seq(seq: List[str]) -> List[str]: return seq with create_test_client( route_handlers=[get_obj, get_seq], dependencies={"obj": Provide(provides_obj)}, ) as client: seq = ["a", "b", "c"] resp = client.get("/seq", params={"seq": seq}) assert resp.json() == ["a", "b", "c"] resp = client.get("/obj", params={"seq": seq}) assert resp.json() == ["a", "b", "c"] def test_regex_validation() -> None: # https://github.com/litestar-org/litestar/issues/1860 @get(path="/val_regex", media_type=MediaType.TEXT) async def regex_val(text: Annotated[str, Parameter(title="a or b", pattern="[a|b]")]) -> str: return f"str: {text}" with create_test_client(route_handlers=[regex_val]) as client: for letter in ("a", "b"): response = client.get(f"/val_regex?text={letter}") assert response.status_code == HTTP_200_OK assert response.text == f"str: {letter}" response = client.get("/val_regex?text=c") assert response.status_code == HTTP_400_BAD_REQUEST @pytest.fixture(name="optional_no_default_client") def optional_no_default_client_fixture() -> Generator[TestClient, None, None]: @get("/optional-no-default") def handle_optional(key: Optional[str]) -> Dict[str, Optional[str]]: return {"key": key} @get("/optional-annotated-no-default") def handle_optional_annotated(param: Annotated[Optional[str], Parameter(query="key")]) -> Dict[str, Optional[str]]: return {"key": param} with create_test_client(route_handlers=[handle_optional, handle_optional_annotated], openapi_config=None) as client: yield client def test_optional_query_parameter_consistency_no_default_queried_without_param( optional_no_default_client: TestClient, ) -> None: assert optional_no_default_client.get("/optional-no-default", params={}).json() == {"key": None} assert optional_no_default_client.get("/optional-annotated-no-default", params={}).json() == {"key": None} def test_optional_query_parameter_consistency_no_default_queried_with_expected_param( optional_no_default_client: TestClient, ) -> None: assert optional_no_default_client.get("/optional-no-default", params={"key": "a"}).json() == {"key": "a"} assert optional_no_default_client.get("/optional-annotated-no-default", params={"key": "a"}).json() == {"key": "a"} def test_optional_query_parameter_consistency_no_default_queried_with_other_param( optional_no_default_client: TestClient, ) -> None: assert optional_no_default_client.get("/optional-no-default", params={"param": "a"}).json() == {"key": None} assert optional_no_default_client.get("/optional-annotated-no-default", params={"param": "a"}).json() == { "key": None } @pytest.fixture(name="optional_default_client") def optional_default_client_fixture() -> Generator[TestClient, None, None]: @get("/optional-default") def handle_default(key: Optional[str] = None) -> Dict[str, Optional[str]]: return {"key": key} @get("/optional-annotated-default") def handle_default_annotated( param: Annotated[Optional[str], Parameter(query="key")] = None, ) -> Dict[str, Optional[str]]: return {"key": param} with create_test_client(route_handlers=[handle_default, handle_default_annotated], openapi_config=None) as client: yield client def test_optional_query_parameter_consistency_with_default_queried_without_param( optional_default_client: TestClient, ) -> None: assert optional_default_client.get("/optional-default", params={}).json() == {"key": None} assert optional_default_client.get("/optional-annotated-default", params={}).json() == {"key": None} def test_optional_query_parameter_consistency_with_default_queried_with_expected_param( optional_default_client: TestClient, ) -> None: assert optional_default_client.get("/optional-default", params={"key": "a"}).json() == {"key": "a"} assert optional_default_client.get("/optional-annotated-default", params={"key": "a"}).json() == {"key": "a"} def test_optional_query_parameter_consistency_with_default_queried_with_other_param( optional_default_client: TestClient, ) -> None: assert optional_default_client.get("/optional-default", params={"param": "a"}).json() == {"key": None} assert optional_default_client.get("/optional-annotated-default", params={"abc": "xyz"}).json() == {"key": None} assert optional_default_client.get("/optional-annotated-default", params={"param": "a"}).json() == {"key": None} litestar-2.16.0/tests/unit/test_parsers.py000066400000000000000000000070121500564371300206440ustar00rootroot00000000000000from typing import Any, Dict, Tuple from urllib.parse import urlencode import pytest from litestar import HttpMethod from litestar._parsers import ( parse_cookie_string, parse_query_string, parse_url_encoded_form_data, ) from litestar.datastructures import Cookie, MultiDict from litestar.testing import RequestFactory, create_test_client def test_parse_form_data() -> None: result = parse_url_encoded_form_data( encoded_data=urlencode( [ ("value", "10"), ("value", "12"), ("veggies", '["tomato", "potato", "aubergine"]'), ("nested", '{"some_key": "some_value"}'), ("calories", "122.53"), ("healthy", "true"), ("polluting", "false"), ] ).encode(), ) assert result == { "value": ["10", "12"], "veggies": '["tomato", "potato", "aubergine"]', "nested": '{"some_key": "some_value"}', "calories": "122.53", "healthy": "true", "polluting": "false", } def test_parse_utf8_form_data() -> None: result = parse_url_encoded_form_data( encoded_data=urlencode( [ ("value", "äüß"), ] ).encode(), ) assert result == {"value": "äüß"} @pytest.mark.parametrize( "cookie_string, expected", ( ("ABC = 123; efg = 456", {"ABC": "123", "efg": "456"}), ("foo= ; bar=", {"foo": "", "bar": ""}), ('foo="bar=123456789&name=moisheZuchmir"', {"foo": "bar=123456789&name=moisheZuchmir"}), ("email=%20%22%2c%3b%2f", {"email": ' ",;/'}), ("foo=%1;bar=bar", {"foo": "%1", "bar": "bar"}), ("foo=bar;fizz ; buzz", {"": "buzz", "foo": "bar"}), (" fizz; foo= bar", {"": "fizz", "foo": "bar"}), ("foo=false;bar=bar;foo=true", {"bar": "bar", "foo": "true"}), ("foo=;bar=bar;foo=boo", {"bar": "bar", "foo": "boo"}), ( Cookie(key="abc", value="123", path="/head", domain="localhost").to_header(header=""), {"Domain": "localhost", "Path": "/head", "SameSite": "lax", "abc": "123"}, ), ), ) def test_parse_cookie_string(cookie_string: str, expected: Dict[str, str]) -> None: assert parse_cookie_string(cookie_string) == expected def test_parse_query_string() -> None: query: Dict[str, Any] = { "value": "10", "veggies": ["tomato", "potato", "aubergine"], "calories": "122.53", "healthy": True, "polluting": False, } request = RequestFactory().get(query_params=query) result = MultiDict(parse_query_string(request.scope.get("query_string", b""))) assert result.dict() == { "value": ["10"], "veggies": ["tomato", "potato", "aubergine"], "calories": ["122.53"], "healthy": ["True"], "polluting": ["False"], } @pytest.mark.parametrize( "values", ( (("first", "x@test.com"), ("second", "aaa")), (("first", "&@A.ac"), ("second", "aaa")), (("first", "a@A.ac&"), ("second", "aaa")), (("first", "a@A&.ac"), ("second", "aaa")), ), ) def test_query_parsing_of_escaped_values(values: Tuple[Tuple[str, str], Tuple[str, str]]) -> None: # https://github.com/litestar-org/litestar/issues/915 with create_test_client([]) as client: request = client.build_request(method=HttpMethod.GET, url="http://www.example.com", params=dict(values)) parsed_query = parse_query_string(request.url.query) assert parsed_query == values litestar-2.16.0/tests/unit/test_plugins/000077500000000000000000000000001500564371300202745ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_plugins/__init__.py000066400000000000000000000000001500564371300223730ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_plugins/test_attrs/000077500000000000000000000000001500564371300224705ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_plugins/test_attrs/__init__.py000066400000000000000000000000001500564371300245670ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_plugins/test_attrs/test_inject_attrs_class.py000066400000000000000000000010071500564371300277550ustar00rootroot00000000000000from attrs import define from litestar import get from litestar.di import Provide from litestar.testing import create_test_client def test_inject_attrs_class() -> None: @define class Foo: bar: str @get("/", dependencies={"foo": Provide(Foo, sync_to_thread=False)}) async def handler(foo: Foo) -> Foo: return foo with create_test_client([handler]) as client: res = client.get("/?bar=baz") assert res.status_code == 200 assert res.json() == {"bar": "baz"} litestar-2.16.0/tests/unit/test_plugins/test_attrs/test_schema_plugin.py000066400000000000000000000023001500564371300267120ustar00rootroot00000000000000from typing import Generic, Optional, TypeVar from attrs import define from typing_extensions import Annotated from litestar.openapi.spec import OpenAPIType from litestar.openapi.spec.schema import Schema from litestar.plugins.attrs import AttrsSchemaPlugin from litestar.typing import FieldDefinition from litestar.utils.helpers import get_name from tests.helpers import get_schema_for_field_definition T = TypeVar("T") @define class AttrsGeneric(Generic[T]): foo: T optional_foo: Optional[T] annotated_foo: Annotated[T, object()] def test_schema_generation_with_generic_classes() -> None: cls = AttrsGeneric[int] expected_foo_schema = Schema(type=OpenAPIType.INTEGER) expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.NULL)]) field_definition = FieldDefinition.from_kwarg(name=get_name(cls), annotation=cls) properties = get_schema_for_field_definition(field_definition, plugins=[AttrsSchemaPlugin()]).properties assert properties assert properties["foo"] == expected_foo_schema assert properties["annotated_foo"] == expected_foo_schema assert properties["optional_foo"] == expected_optional_foo_schema litestar-2.16.0/tests/unit/test_plugins/test_attrs/test_schema_spec_generation.py000066400000000000000000000033051500564371300305670ustar00rootroot00000000000000from typing import Dict, List, Optional import attrs from litestar import post from litestar.testing import create_test_client from tests.models import DataclassPet def test_spec_generation() -> None: @attrs.define class Person: first_name: str last_name: str id: str optional: Optional[str] complex: Dict[str, List[Dict[str, str]]] pets: Optional[List[DataclassPet]] @post("/") def handler(data: Person) -> Person: return data with create_test_client(handler) as client: schema = client.app.openapi_schema assert schema assert schema.to_schema()["components"]["schemas"]["test_spec_generation.Person"] == { "properties": { "first_name": {"type": "string"}, "last_name": {"type": "string"}, "id": {"type": "string"}, "optional": {"oneOf": [{"type": "string"}, {"type": "null"}]}, "complex": { "type": "object", "additionalProperties": { "type": "array", "items": {"type": "object", "additionalProperties": {"type": "string"}}, }, }, "pets": { "oneOf": [ { "items": {"$ref": "#/components/schemas/DataclassPet"}, "type": "array", }, {"type": "null"}, ] }, }, "type": "object", "required": ["complex", "first_name", "id", "last_name"], "title": "Person", } litestar-2.16.0/tests/unit/test_plugins/test_attrs/test_signature.py000066400000000000000000000012731500564371300261050ustar00rootroot00000000000000from attrs import define from litestar import post from litestar.status_codes import HTTP_201_CREATED from litestar.testing import create_test_client def test_parse_attrs_data_in_signature() -> None: @define(slots=True, frozen=True) class AttrsUser: name: str email: str @post("/") async def attrs_data(data: AttrsUser) -> AttrsUser: return data with create_test_client([attrs_data]) as client: response = client.post("/", json={"name": "foo", "email": "e@example.com"}) assert response.status_code == HTTP_201_CREATED assert response.json().get("name") == "foo" assert response.json().get("email") == "e@example.com" litestar-2.16.0/tests/unit/test_plugins/test_base.py000066400000000000000000000117421500564371300226240ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import pytest from click import Group from litestar import Litestar, MediaType, get from litestar.constants import UNDEFINED_SENTINELS from litestar.plugins import CLIPluginProtocol, InitPlugin, OpenAPISchemaPlugin, PluginRegistry from litestar.plugins.attrs import AttrsSchemaPlugin from litestar.plugins.core import MsgspecDIPlugin from litestar.plugins.pydantic import PydanticDIPlugin, PydanticInitPlugin, PydanticPlugin, PydanticSchemaPlugin from litestar.plugins.sqlalchemy import SQLAlchemySerializationPlugin from litestar.testing import create_test_client from litestar.typing import FieldDefinition if TYPE_CHECKING: from litestar.config.app import AppConfig def test_plugin_on_app_init() -> None: @get("/", media_type=MediaType.TEXT) def greet() -> str: return "hello world" tag = "on_app_init_called" def on_startup(app: Litestar) -> None: app.state.called = True class PluginWithInitOnly(InitPlugin): def on_app_init(self, app_config: AppConfig) -> AppConfig: app_config.tags.append(tag) app_config.on_startup.append(on_startup) app_config.route_handlers.append(greet) return app_config with create_test_client(plugins=[PluginWithInitOnly()]) as client: response = client.get("/") assert response.text == "hello world" assert tag in client.app.tags assert client.app.state.called def test_plugin_registry() -> None: class CLIPlugin(CLIPluginProtocol): def on_cli_init(self, cli: Group) -> None: pass cli_plugin = CLIPlugin() serialization_plugin = SQLAlchemySerializationPlugin() openapi_plugin = PydanticSchemaPlugin() init_plugin = PydanticInitPlugin() registry = PluginRegistry([cli_plugin, serialization_plugin, openapi_plugin, init_plugin]) assert registry.openapi == (openapi_plugin,) assert registry.cli == (cli_plugin,) assert registry.serialization == (serialization_plugin,) assert registry.init == (init_plugin,) assert openapi_plugin in registry assert serialization_plugin in registry assert init_plugin in registry assert cli_plugin in registry assert set(registry) == {openapi_plugin, cli_plugin, init_plugin, serialization_plugin} def test_plugin_registry_get() -> None: class CLIPlugin(CLIPluginProtocol): def on_cli_init(self, cli: Group) -> None: pass cli_plugin = CLIPlugin() with pytest.raises(KeyError, match="No plugin of type 'CLIPlugin' registered"): PluginRegistry([]).get(CLIPlugin) assert PluginRegistry([cli_plugin]).get(CLIPlugin) is cli_plugin def test_plugin_registry_stringified_get() -> None: class CLIPlugin(CLIPluginProtocol): def on_cli_init(self, cli: Group) -> None: pass cli_plugin = CLIPlugin() pydantic_plugin = PydanticPlugin() with pytest.raises(KeyError): PluginRegistry([CLIPlugin()]).get( "litestar2.plugins.pydantic.PydanticPlugin" ) # not a fqdn. should fail # type: ignore[list-item] PluginRegistry([]).get("CLIPlugin") # not a fqdn. should fail # type: ignore[list-item] assert PluginRegistry([cli_plugin, pydantic_plugin]).get(CLIPlugin) is cli_plugin assert PluginRegistry([cli_plugin, pydantic_plugin]).get(PydanticPlugin) is pydantic_plugin assert PluginRegistry([cli_plugin, pydantic_plugin]).get("PydanticPlugin") is pydantic_plugin assert ( PluginRegistry([cli_plugin, pydantic_plugin]).get("litestar.plugins.pydantic.PydanticPlugin") is pydantic_plugin ) def test_openapi_schema_plugin_is_constrained_field() -> None: assert OpenAPISchemaPlugin.is_constrained_field(FieldDefinition.from_annotation(str)) is False def test_openapi_schema_plugin_is_undefined_sentinel() -> None: for value in UNDEFINED_SENTINELS: assert OpenAPISchemaPlugin.is_undefined_sentinel(value) is False @pytest.mark.parametrize(("init_plugin",), [(PydanticInitPlugin(),), (None,)]) @pytest.mark.parametrize(("schema_plugin",), [(PydanticSchemaPlugin(),), (None,)]) @pytest.mark.parametrize(("attrs_plugin",), [(AttrsSchemaPlugin(),), (None,)]) def test_app_get_default_plugins( init_plugin: PydanticInitPlugin, schema_plugin: PydanticSchemaPlugin, attrs_plugin: AttrsSchemaPlugin ) -> None: plugins = [p for p in (init_plugin, schema_plugin, attrs_plugin) if p is not None] any_pydantic = bool(init_plugin) or bool(schema_plugin) default_plugins = Litestar._get_default_plugins(plugins) # type: ignore[arg-type] if not any_pydantic: assert {type(p) for p in default_plugins} == { PydanticPlugin, AttrsSchemaPlugin, PydanticDIPlugin, MsgspecDIPlugin, } else: assert {type(p) for p in default_plugins} == { PydanticInitPlugin, PydanticSchemaPlugin, AttrsSchemaPlugin, PydanticDIPlugin, MsgspecDIPlugin, } litestar-2.16.0/tests/unit/test_plugins/test_flash.py000066400000000000000000000074471500564371300230160ustar00rootroot00000000000000from __future__ import annotations from enum import Enum from pathlib import Path import pytest from litestar import Litestar, Request, get, post from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.exceptions import ImproperlyConfiguredException from litestar.middleware.rate_limit import RateLimitConfig from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.plugins.flash import FlashConfig, FlashPlugin, flash from litestar.response import Redirect, Template from litestar.template import TemplateConfig, TemplateEngineProtocol from litestar.testing import create_test_client text_html_jinja = """{% for message in get_flashes() %}{{ message.message }}{% endfor %}""" text_html_mako = """<% messages = get_flashes() %>\\ % for m in messages: ${m['message']}\\ % endfor """ class CustomCategory(str, Enum): custom1 = "1" custom2 = "2" custom3 = "3" class FlashCategory(str, Enum): info = "INFO" error = "ERROR" warning = "WARNING" success = "SUCCESS" @pytest.mark.parametrize( "engine, template_str", ( (JinjaTemplateEngine, text_html_jinja), (MakoTemplateEngine, text_html_mako), (MiniJinjaTemplateEngine, text_html_jinja), ), ids=("jinja", "mako", "minijinja"), ) @pytest.mark.parametrize( "category_enum", (CustomCategory, FlashCategory), ids=("custom_category", "flash_category"), ) def test_flash_plugin( tmp_path: Path, engine: type[TemplateEngineProtocol], template_str: str, category_enum: Enum, ) -> None: Path(tmp_path / "flash.html").write_text(template_str) @get("/") async def index() -> Redirect: return Redirect("/login") @get("/login") async def login(request: Request) -> Template: flash(request, "Flash Test!", category="info") return Template("flash.html") @post("/check") async def check(request: Request) -> Redirect: flash(request, "User not Found!", category="warning") return Redirect("/login") template_config: TemplateConfig = TemplateConfig( directory=Path(tmp_path), engine=engine, ) session_config = ServerSideSessionConfig() flash_config = FlashConfig(template_config=template_config) with create_test_client( plugins=[FlashPlugin(config=flash_config)], route_handlers=[index, login, check], template_config=template_config, middleware=[session_config.middleware], ) as client: r = client.get("/") assert r.status_code == 200 assert "Flash Test!" in r.text r = client.get("/login") assert r.status_code == 200 assert "Flash Test!" in r.text r = client.post("/check") assert r.status_code == 200 assert "User not Found!" in r.text assert "Flash Test!" in r.text def test_flash_config_doesnt_have_session() -> None: template_config = TemplateConfig(directory=Path("tests/templates"), engine=JinjaTemplateEngine) flash_config = FlashConfig(template_config=template_config) with pytest.raises(ImproperlyConfiguredException): Litestar(plugins=[FlashPlugin(config=flash_config)]) def test_flash_config_has_wrong_middleware_type() -> None: template_config = TemplateConfig(directory=Path("tests/templates"), engine=JinjaTemplateEngine) flash_config = FlashConfig(template_config=template_config) rate_limit_config = RateLimitConfig(rate_limit=("minute", 1), exclude=["/schema"]) with pytest.raises(ImproperlyConfiguredException): Litestar(plugins=[FlashPlugin(config=flash_config)], middleware=[rate_limit_config.middleware]) litestar-2.16.0/tests/unit/test_plugins/test_problem_details.py000066400000000000000000000102151500564371300250510ustar00rootroot00000000000000from __future__ import annotations from http import HTTPStatus from typing import Any import pytest from litestar import get from litestar.exceptions.http_exceptions import HTTPException, ValidationException from litestar.plugins.problem_details import ProblemDetailsConfig, ProblemDetailsException, ProblemDetailsPlugin from litestar.testing.helpers import create_test_client @pytest.mark.parametrize( ("exception", "expected"), [ ( ProblemDetailsException(), { "status": 500, "detail": HTTPStatus(500).phrase, }, ), ( ProblemDetailsException(status_code=400, detail="validation error", instance="https://example.net/error"), { "status": 400, "detail": "validation error", "instance": "https://example.net/error", }, ), ( ProblemDetailsException( status_code=400, detail="validation error", extra={"error": "must be positive integer", "pointer": "#age"}, ), { "status": 400, "detail": "validation error", "error": "must be positive integer", "pointer": "#age", }, ), ( ProblemDetailsException( status_code=400, detail="validation error", extra=[{"error": "must be positive integer", "pointer": "#age"}], ), { "status": 400, "detail": "validation error", "extra": [{"error": "must be positive integer", "pointer": "#age"}], }, ), ( ProblemDetailsException(type_="https://example.net/validation-error"), { "type": "https://example.net/validation-error", "status": 500, "detail": HTTPStatus(500).phrase, }, ), ], ) def test_raising_problem_details_exception(exception: ProblemDetailsException, expected: dict[str, Any]) -> None: @get("/") async def get_foo() -> None: raise exception with create_test_client([get_foo], plugins=[ProblemDetailsPlugin()]) as client: response = client.get("/") assert response.headers["content-type"] == "application/problem+json" assert response.json() == expected assert response.status_code == expected["status"] @pytest.mark.parametrize("enable", (True, False)) def test_enable_for_all_http_exceptions(enable: bool) -> None: @get("/") async def get_foo() -> None: raise HTTPException() config = ProblemDetailsConfig(enable_for_all_http_exceptions=enable) with create_test_client([get_foo], plugins=[ProblemDetailsPlugin(config)]) as client: response = client.get("/") if enable: assert response.headers["content-type"] == "application/problem+json" else: assert response.headers["content-type"] != "application/problem+json" def test_exception_to_problem_detail_map() -> None: def validation_exception_to_problem_details_exception(exc: ValidationException) -> ProblemDetailsException: return ProblemDetailsException( type_="validation-error", detail=exc.detail, extra=exc.extra, status_code=exc.status_code ) @get("/") async def get_foo() -> None: raise ValidationException(detail="Not enough balance", extra=errors) errors = {"accounts": ["/account/1", "/account/2"]} config = ProblemDetailsConfig( exception_to_problem_detail_map={ValidationException: validation_exception_to_problem_details_exception} ) with create_test_client([get_foo], plugins=[ProblemDetailsPlugin(config)]) as client: response = client.get("/") assert response.status_code == 400 assert response.headers["content-type"] == "application/problem+json" assert response.json() == { "type": "validation-error", "status": 400, "detail": "Not enough balance", "accounts": ["/account/1", "/account/2"], } litestar-2.16.0/tests/unit/test_plugins/test_prometheus.py000066400000000000000000000166051500564371300241100ustar00rootroot00000000000000import re import time from http.client import HTTPException from pathlib import Path from typing import Any import pytest from _pytest.monkeypatch import MonkeyPatch from prometheus_client import REGISTRY from pytest_mock import MockerFixture from litestar import get, post, websocket_listener from litestar.plugins.prometheus import PrometheusConfig, PrometheusController, PrometheusMiddleware from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client def create_config(**kwargs: Any) -> PrometheusConfig: collectors = list(REGISTRY._collector_to_names.keys()) for collector in collectors: REGISTRY.unregister(collector) PrometheusMiddleware._metrics = {} return PrometheusConfig(**kwargs) @pytest.mark.flaky(reruns=5) def test_prometheus_exporter_metrics_with_http() -> None: config = create_config() @get("/duration") def duration_handler() -> dict: time.sleep(0.1) return {"hello": "world"} @get("/error") def handler_error() -> dict: raise HTTPException("Error Occurred") with create_test_client( [duration_handler, handler_error, PrometheusController], middleware=[config.middleware] ) as client: client.get("/error") client.get("/duration") metrics_exporter_response = client.get("/metrics") assert metrics_exporter_response.status_code == HTTP_200_OK metrics = metrics_exporter_response.content.decode() assert ( """litestar_request_duration_seconds_sum{app_name="litestar",method="GET",path="/duration",status_code="200"}""" in metrics ) assert ( """litestar_requests_error_total{app_name="litestar",method="GET",path="/error",status_code="500"} 1.0""" in metrics ) assert ( """litestar_request_duration_seconds_bucket{app_name="litestar",le="0.005",method="GET",path="/error",status_code="500"} 1.0""" in metrics ) assert ( """litestar_requests_in_progress{app_name="litestar",method="GET",path="/metrics",status_code="200"} 1.0""" in metrics ) assert ( """litestar_requests_in_progress{app_name="litestar",method="GET",path="/duration",status_code="200"} 0.0""" in metrics ) duration_metric_matches = re.findall( r"""litestar_request_duration_seconds_sum{app_name="litestar",method="GET",path="/duration",status_code="200"} (\d+\.\d+)""", metrics, ) assert duration_metric_matches != [] assert round(float(duration_metric_matches[0]), 1) == 0.1 client.get("/duration") metrics = client.get("/metrics").content.decode() assert ( """litestar_requests_total{app_name="litestar",method="GET",path="/duration",status_code="200"} 2.0""" in metrics ) assert ( """litestar_requests_in_progress{app_name="litestar",method="GET",path="/error",status_code="200"} 0.0""" in metrics ) assert ( """litestar_requests_in_progress{app_name="litestar",method="GET",path="/metrics",status_code="200"} 1.0""" in metrics ) def test_prometheus_middleware_configurations() -> None: labels = {"foo": "bar", "baz": lambda a: "qux"} config = create_config( app_name="litestar_test", prefix="litestar_rocks", labels=labels, buckets=[0.1, 0.5, 1.0], excluded_http_methods=["POST"], ) @get("/test") def test() -> dict: return {"hello": "world"} @post("/ignore") def ignore() -> dict: return {"hello": "world"} with create_test_client([test, ignore, PrometheusController], middleware=[config.middleware]) as client: client.get("/test") client.post("/ignore") metrics_exporter_response = client.get("/metrics") assert metrics_exporter_response.status_code == HTTP_200_OK metrics = metrics_exporter_response.content.decode() assert ( """litestar_rocks_requests_total{app_name="litestar_test",baz="qux",foo="bar",method="GET",path="/test",status_code="200"} 1.0""" in metrics ) assert ( """litestar_rocks_requests_total{app_name="litestar_test",baz="qux",foo="bar",method="POST",path="/ignore",status_code="201"} 1.0""" not in metrics ) assert ( """litestar_rocks_request_duration_seconds_bucket{app_name="litestar_test",baz="qux",foo="bar",le="0.1",method="GET",path="/test",status_code="200"} 1.0""" in metrics ) assert ( """litestar_rocks_request_duration_seconds_bucket{app_name="litestar_test",baz="qux",foo="bar",le="0.5",method="GET",path="/test",status_code="200"} 1.0""" in metrics ) assert ( """litestar_rocks_request_duration_seconds_bucket{app_name="litestar_test",baz="qux",foo="bar",le="1.0",method="GET",path="/test",status_code="200"} 1.0""" in metrics ) def test_prometheus_controller_configurations() -> None: config = create_config( exemplars=lambda a: {"trace_id": "1234"}, ) class CustomPrometheusController(PrometheusController): path: str = "/metrics/custom" openmetrics_format: bool = True @get("/test") def test() -> dict: return {"hello": "world"} with create_test_client([test, CustomPrometheusController], middleware=[config.middleware]) as client: client.get("/test") metrics_exporter_response = client.get("/metrics/custom") assert metrics_exporter_response.status_code == HTTP_200_OK metrics = metrics_exporter_response.content.decode() assert ( """litestar_requests_total{app_name="litestar",method="GET",path="/test",status_code="200"} 1.0 # {trace_id="1234"} 1.0""" in metrics ) def test_prometheus_with_websocket() -> None: config = create_config() @websocket_listener("/test") def test(data: str) -> dict: return {"hello": data} with create_test_client([test, PrometheusController], middleware=[config.middleware]) as client: with client.websocket_connect("/test") as websocket: websocket.send_text("litestar") websocket.receive_json() metrics_exporter_response = client.get("/metrics") assert metrics_exporter_response.status_code == HTTP_200_OK metrics = metrics_exporter_response.content.decode() assert ( """litestar_requests_total{app_name="litestar",method="websocket",path="/test",status_code="200"} 1.0""" in metrics ) @pytest.mark.parametrize("env_var", ["PROMETHEUS_MULTIPROC_DIR", "prometheus_multiproc_dir"]) def test_procdir(monkeypatch: MonkeyPatch, tmp_path: Path, mocker: MockerFixture, env_var: str) -> None: proc_dir = tmp_path / "something" proc_dir.mkdir() monkeypatch.setenv(env_var, str(proc_dir)) config = create_config() mock_registry = mocker.patch("litestar.plugins.prometheus.controller.CollectorRegistry") mock_collector = mocker.patch("litestar.plugins.prometheus.controller.multiprocess.MultiProcessCollector") with create_test_client([PrometheusController], middleware=[config.middleware]) as client: client.get("/metrics") mock_collector.assert_called_once_with(mock_registry.return_value) litestar-2.16.0/tests/unit/test_plugins/test_pydantic/000077500000000000000000000000001500564371300231465ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_plugins/test_pydantic/__init__.py000066400000000000000000000005221500564371300252560ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Literal if TYPE_CHECKING: import pydantic as pydantic_v2 from pydantic import v1 as pydantic_v1 from typing_extensions import TypeAlias PydanticVersion = Literal["v1", "v2"] BaseModelType: TypeAlias = "type[pydantic_v1.BaseModel| pydantic_v2.BaseModel]" litestar-2.16.0/tests/unit/test_plugins/test_pydantic/conftest.py000066400000000000000000000012071500564371300253450ustar00rootroot00000000000000from __future__ import annotations from typing import Callable import pydantic import pytest from pydantic import v1 as pydantic_v1 from pytest import FixtureRequest from . import PydanticVersion @pytest.fixture def int_factory() -> Callable[[], int]: return lambda: 2 @pytest.fixture(params=["v1", "v2"]) def pydantic_version(request: FixtureRequest) -> PydanticVersion: return request.param # type: ignore[no-any-return] @pytest.fixture() def base_model(pydantic_version: PydanticVersion) -> type[pydantic.BaseModel | pydantic_v1.BaseModel]: return pydantic_v1.BaseModel if pydantic_version == "v1" else pydantic.BaseModel litestar-2.16.0/tests/unit/test_plugins/test_pydantic/models.py000066400000000000000000000024021500564371300250010ustar00rootroot00000000000000from typing import Dict, List, Optional, Union from pydantic import BaseModel from pydantic.dataclasses import dataclass as pydantic_dataclass from pydantic.v1 import BaseModel as BaseModelV1 from pydantic.v1.dataclasses import dataclass as dataclass_v1 from tests.models import DataclassPet @pydantic_dataclass class PydanticDataclassPerson: first_name: str last_name: str id: str optional: Optional[str] complex: Dict[str, List[Dict[str, str]]] union: Union[int, List[str]] pets: Optional[List[DataclassPet]] = None class PydanticPerson(BaseModel): first_name: str last_name: str id: str optional: Optional[str] complex: Dict[str, List[Dict[str, str]]] union: Union[int, List[str]] pets: Optional[List[DataclassPet]] = None class PydanticV1Person(BaseModelV1): first_name: str last_name: str id: str optional: Optional[str] complex: Dict[str, List[Dict[str, str]]] union: Union[int, List[str]] pets: Optional[List[DataclassPet]] = None @dataclass_v1 class PydanticV1DataclassPerson: first_name: str last_name: str id: str optional: Optional[str] complex: Dict[str, List[Dict[str, str]]] union: Union[int, List[str]] pets: Optional[List[DataclassPet]] = None litestar-2.16.0/tests/unit/test_plugins/test_pydantic/test_beanie_integration.py000066400000000000000000000011251500564371300304040ustar00rootroot00000000000000from typing import Optional import beanie from pydantic import BaseModel from litestar.plugins.pydantic import PydanticDTO def test_generate_field_definitions_from_beanie_models() -> None: class Category(BaseModel): name: str description: str class Product(beanie.Document): name: str description: Optional[str] = None price: float category: Category field_names = [field.name for field in PydanticDTO.generate_field_definitions(Product)] assert field_names == ["id", "revision_id", "name", "description", "price", "category"] litestar-2.16.0/tests/unit/test_plugins/test_pydantic/test_dto.py000066400000000000000000000135151500564371300253520ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Optional, cast import pydantic as pydantic_v2 import pytest from pydantic import v1 as pydantic_v1 from typing_extensions import Annotated, Literal from litestar import Request, post from litestar.dto import DTOConfig from litestar.plugins.pydantic import PydanticDTO, _model_dump_json from litestar.testing import create_test_client from litestar.types import Empty from litestar.typing import FieldDefinition if TYPE_CHECKING: from collections.abc import Callable from types import ModuleType from pydantic import BaseModel from litestar import Litestar def test_schema_required_fields_with_pydantic_dto( use_experimental_dto_backend: bool, base_model: type[BaseModel] ) -> None: class PydanticUser(base_model): # type: ignore[misc, valid-type] age: int name: str class UserDTO(PydanticDTO[PydanticUser]): config = DTOConfig(experimental_codegen_backend=use_experimental_dto_backend) @post(dto=UserDTO, return_dto=None, signature_types=[PydanticUser]) def handler(data: PydanticUser, request: Request) -> dict: schema = request.app.openapi_schema return schema.to_schema() with create_test_client(handler) as client: data = PydanticUser(name="A", age=10) headers = {"Content-Type": "application/json; charset=utf-8"} received = client.post( "/", content=_model_dump_json(data), headers=headers, ) required = next(iter(received.json()["components"]["schemas"].values()))["required"] assert len(required) == 2 def test_field_definition_implicit_optional_default(base_model: type[BaseModel]) -> None: class Model(base_model): # type: ignore[misc, valid-type] a: Optional[str] # noqa: UP007 dto_type = PydanticDTO[Model] field_defs = list(dto_type.generate_field_definitions(Model)) assert len(field_defs) == 1 assert field_defs[0].default is None def test_detect_nested_field_pydantic_v1(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr("litestar.plugins.pydantic.dto.pydantic_v2", Empty) class Model(pydantic_v1.BaseModel): a: str dto_type = PydanticDTO[Model] assert dto_type.detect_nested_field(FieldDefinition.from_annotation(Model)) is True assert dto_type.detect_nested_field(FieldDefinition.from_annotation(int)) is False def test_pydantic_field_descriptions(create_module: Callable[[str], ModuleType]) -> None: module = create_module( """ from litestar import Litestar, get from litestar.plugins.pydantic import PydanticDTO from litestar.dto import DTOConfig from pydantic import BaseModel, Field from typing_extensions import Annotated class User(BaseModel): id: Annotated[ int, Field(description="This is a test (id description).", gt=1), ] class DataCollectionDTO(PydanticDTO[User]): config = DTOConfig(rename_strategy="camel") @get("/user", return_dto=DataCollectionDTO, sync_to_thread=False) def get_user() -> User: return User(id=user_id) app = Litestar(route_handlers=[get_user]) """ ) app = cast("Litestar", module.app) schema = app.openapi_schema assert schema.components.schemas is not None component_schema = schema.components.schemas["GetUserUserResponseBody"] assert component_schema.properties is not None assert component_schema.properties["id"].description == "This is a test (id description)." assert component_schema.properties["id"].exclusive_minimum == 1 # type: ignore[union-attr] @pytest.mark.parametrize( "model_config_option, forbid_unknown_fields_default, expected_dto_config_option", [ ("forbid", False, True), ("forbid", True, True), ("allow", False, False), ("allow", True, True), ("ignore", True, True), ("ignore", False, False), ], ) def test_forbid_unknown_fields_if_forbid_extra_is_set_v1( use_experimental_dto_backend: bool, forbid_unknown_fields_default: bool, model_config_option: Literal["forbid", "allow", "ignore"], expected_dto_config_option: bool, ) -> None: class Model(pydantic_v1.BaseModel): class Config: extra = model_config_option a: str dto_config = DTOConfig( experimental_codegen_backend=use_experimental_dto_backend, # config set on the pydantic model should take precedence forbid_unknown_fields=forbid_unknown_fields_default, ) dto = PydanticDTO[Annotated[Model, dto_config]] assert dto.config.forbid_unknown_fields is expected_dto_config_option # ensure the config is merged assert dto.config.experimental_codegen_backend is use_experimental_dto_backend @pytest.mark.parametrize( "model_config_option, forbid_unknown_fields_default, expected_dto_config_option", [ ("forbid", False, True), ("forbid", True, True), ("allow", False, False), ("allow", True, True), ("ignore", True, True), ("ignore", False, False), ], ) def test_forbid_unknown_fields_if_forbid_extra_is_set_v2( use_experimental_dto_backend: bool, forbid_unknown_fields_default: bool, model_config_option: Literal["forbid", "allow", "ignore"], expected_dto_config_option: bool, ) -> None: class Model(pydantic_v2.BaseModel): a: str model_config = pydantic_v2.ConfigDict(extra=model_config_option) dto_config = DTOConfig( experimental_codegen_backend=use_experimental_dto_backend, # config set on the pydantic model should take precedence forbid_unknown_fields=forbid_unknown_fields_default, ) dto = PydanticDTO[Annotated[Model, dto_config]] assert dto.config.forbid_unknown_fields is expected_dto_config_option # ensure the config is merged assert dto.config.experimental_codegen_backend is use_experimental_dto_backend litestar-2.16.0/tests/unit/test_plugins/test_pydantic/test_inject_pydantic.py000066400000000000000000000013021500564371300277220ustar00rootroot00000000000000import pydantic as pydantic_v2 import pytest from pydantic import v1 as pydantic_v1 from litestar import get from litestar.di import Provide from litestar.testing import create_test_client @pytest.mark.parametrize("base_model", [pydantic_v1.BaseModel, pydantic_v2.BaseModel]) def test_inject_pydantic_model(base_model: type) -> None: class Foo(base_model): # type: ignore[misc] bar: str @get("/", dependencies={"foo": Provide(Foo, sync_to_thread=False)}) async def handler(foo: Foo) -> Foo: return foo with create_test_client([handler]) as client: res = client.get("/?bar=baz") assert res.status_code == 200 assert res.json() == {"bar": "baz"} litestar-2.16.0/tests/unit/test_plugins/test_pydantic/test_integration.py000066400000000000000000000360571500564371300271150ustar00rootroot00000000000000from typing import Any, Dict, List from unittest.mock import ANY import pydantic as pydantic_v2 import pytest from pydantic import v1 as pydantic_v1 from typing_extensions import Annotated from litestar import get, post from litestar.enums import RequestEncodingType from litestar.params import Body, Parameter from litestar.plugins.pydantic import PydanticDTO, PydanticInitPlugin, PydanticPlugin from litestar.status_codes import HTTP_400_BAD_REQUEST from litestar.testing import create_test_client from tests.unit.test_plugins.test_pydantic.models import PydanticPerson, PydanticV1Person from . import BaseModelType, PydanticVersion @pytest.mark.parametrize(("meta",), [(None,), (Body(media_type=RequestEncodingType.URL_ENCODED),)]) def test_pydantic_v1_validation_error_raises_400(meta: Any) -> None: class Model(pydantic_v1.BaseModel): foo: str = pydantic_v1.Field(max_length=2) ModelDTO = PydanticDTO[Model] annotation: Any annotation = Annotated[Model, meta] if meta is not None else Model @post(dto=ModelDTO, signature_namespace={"annotation": annotation}) def handler(data: annotation) -> Any: # pyright: ignore return data model_json = {"foo": "too long"} expected_errors: List[Dict[str, Any]] expected_errors = [ { "loc": ["foo"], "msg": "ensure this value has at most 2 characters", "type": "value_error.any_str.max_length", "ctx": {"limit_value": 2}, } ] with create_test_client(route_handlers=handler) as client: kws = {"data": model_json} if meta else {"json": model_json} response = client.post("/", **kws) # type: ignore[arg-type] extra = response.json()["extra"] assert response.status_code == 400 assert extra == expected_errors @pytest.mark.parametrize(("meta",), [(None,), (Body(media_type=RequestEncodingType.URL_ENCODED),)]) def test_pydantic_v2_validation_error_raises_400(meta: Any) -> None: class Model(pydantic_v2.BaseModel): foo: str = pydantic_v2.Field(max_length=2) ModelDTO = PydanticDTO[Model] annotation: Any annotation = Annotated[Model, meta] if meta is not None else Model @post(dto=ModelDTO, signature_namespace={"annotation": annotation}) def handler(data: annotation) -> Any: # pyright: ignore return data model_json = {"foo": "too long"} expected_errors: List[Dict[str, Any]] expected_errors = [ { "type": "string_too_long", "loc": ["foo"], "msg": "String should have at most 2 characters", "input": "too long", "ctx": {"max_length": 2}, } ] with create_test_client(route_handlers=handler) as client: kws = {"data": model_json} if meta else {"json": model_json} response = client.post("/", **kws) # type: ignore[arg-type] extra = response.json()["extra"] extra[0].pop("url") assert response.status_code == 400 assert extra == expected_errors def test_default_error_handling() -> None: @post("/{param:int}") def my_route_handler(param: int, data: PydanticPerson) -> None: ... with create_test_client(my_route_handler) as client: response = client.post("/123", json={"first_name": "moishe"}) extra = response.json().get("extra") assert extra is not None assert len(extra) == 5 def test_default_error_handling_v1() -> None: @post("/{param:int}") def my_route_handler(param: int, data: PydanticV1Person) -> None: ... with create_test_client(my_route_handler) as client: response = client.post("/123", json={"first_name": "moishe"}) extra = response.json().get("extra") assert extra is not None assert len(extra) == 4 def test_serialize_raw_errors_v2() -> None: # https://github.com/litestar-org/litestar/issues/2365 class User(pydantic_v2.BaseModel): user_id: int @pydantic_v2.field_validator("user_id") @classmethod def validate_user_id(cls, user_id: int) -> None: raise ValueError("user id must be greater than 0") @post("/", dto=PydanticDTO[User]) async def create_user(data: User) -> User: return data with create_test_client(create_user) as client: response = client.post("/", json={"user_id": -1}) extra = response.json().get("extra") assert extra == [ { "type": "value_error", "loc": ["user_id"], "msg": "Value error, user id must be greater than 0", "input": -1, "ctx": {"error": "ValueError"}, "url": ANY, } ] def test_signature_model_invalid_input(base_model: BaseModelType, pydantic_version: PydanticVersion) -> None: class OtherChild(base_model): # type: ignore[misc, valid-type] val: List[int] class Child(base_model): # type: ignore[misc, valid-type] val: int other_val: int class Parent(base_model): # type: ignore[misc, valid-type] child: Child other_child: OtherChild @post("/") def test( data: Parent, int_param: int, length_param: str = Parameter(min_length=2), int_header: int = Parameter(header="X-SOME-INT"), int_cookie: int = Parameter(cookie="int-cookie"), ) -> None: ... with create_test_client(route_handlers=[test], signature_types=[Parent]) as client: client.cookies.update({"int-cookie": "cookie"}) response = client.post( "/", json={"child": {"val": "a", "other_val": "b"}, "other_child": {"val": [1, "c"]}}, params={"int_param": "param", "length_param": "d"}, headers={"X-SOME-INT": "header"}, ) assert response.status_code == HTTP_400_BAD_REQUEST data = response.json() assert data if pydantic_version == "v1": assert data["extra"] == [ {"key": "child.val", "message": "value is not a valid integer"}, {"key": "child.other_val", "message": "value is not a valid integer"}, {"key": "other_child.val.1", "message": "value is not a valid integer"}, ] else: assert data["extra"] == [ { "message": "Input should be a valid integer, unable to parse string as an integer", "key": "child.val", }, { "message": "Input should be a valid integer, unable to parse string as an integer", "key": "child.other_val", }, { "message": "Input should be a valid integer, unable to parse string as an integer", "key": "other_child.val.1", }, ] class V1ModelWithPrivateFields(pydantic_v1.BaseModel): class Config: underscore_fields_are_private = True _field: str = pydantic_v1.PrivateAttr() # include an invalid annotation here to ensure we never touch those fields _underscore_field: "foo" # type: ignore[name-defined] # noqa: F821 bar: str class V2ModelWithPrivateFields(pydantic_v2.BaseModel): class Config: underscore_fields_are_private = True _field: str = pydantic_v2.PrivateAttr() bar: str @pytest.mark.parametrize("model_type", [V1ModelWithPrivateFields, V2ModelWithPrivateFields]) def test_private_fields(model_type: BaseModelType) -> None: @post("/") async def handler(data: V2ModelWithPrivateFields) -> V2ModelWithPrivateFields: return data with create_test_client([handler]) as client: res = client.post("/", json={"bar": "value"}) assert res.status_code == 201 assert res.json() == {"bar": "value"} @pytest.mark.parametrize( ("base_model", "type_", "in_"), [ pytest.param(pydantic_v2.BaseModel, pydantic_v2.JsonValue, {"foo": "bar"}, id="pydantic_v2.JsonValue"), pytest.param( pydantic_v1.BaseModel, pydantic_v1.IPvAnyAddress, "127.0.0.1", id="pydantic_v1.IPvAnyAddress (v4)" ), pytest.param( pydantic_v2.BaseModel, pydantic_v2.IPvAnyAddress, "127.0.0.1", id="pydantic_v2.IPvAnyAddress (v4)" ), pytest.param( pydantic_v1.BaseModel, pydantic_v1.IPvAnyAddress, "2001:db8::ff00:42:8329", id="pydantic_v1.IPvAnyAddress (v6)", ), pytest.param( pydantic_v2.BaseModel, pydantic_v2.IPvAnyAddress, "2001:db8::ff00:42:8329", id="pydantic_v2.IPvAnyAddress (v6)", ), pytest.param( pydantic_v1.BaseModel, pydantic_v1.IPvAnyInterface, "127.0.0.1/24", id="pydantic_v1.IPvAnyInterface (v4)" ), pytest.param( pydantic_v2.BaseModel, pydantic_v2.IPvAnyInterface, "127.0.0.1/24", id="pydantic_v2.IPvAnyInterface (v4)" ), pytest.param( pydantic_v1.BaseModel, pydantic_v1.IPvAnyInterface, "2001:db8::ff00:42:8329/128", id="pydantic_v1.IPvAnyInterface (v6)", ), pytest.param( pydantic_v2.BaseModel, pydantic_v2.IPvAnyInterface, "2001:db8::ff00:42:8329/128", id="pydantic_v2.IPvAnyInterface (v6)", ), pytest.param( pydantic_v1.BaseModel, pydantic_v1.IPvAnyNetwork, "127.0.0.1/32", id="pydantic_v1.IPvAnyNetwork (v4)" ), pytest.param( pydantic_v2.BaseModel, pydantic_v2.IPvAnyNetwork, "127.0.0.1/32", id="pydantic_v2.IPvAnyNetwork (v4)" ), pytest.param( pydantic_v1.BaseModel, pydantic_v1.IPvAnyNetwork, "2001:db8::ff00:42:8329/128", id="pydantic_v1.IPvAnyNetwork (v6)", ), pytest.param( pydantic_v2.BaseModel, pydantic_v2.IPvAnyNetwork, "2001:db8::ff00:42:8329/128", id="pydantic_v2.IPvAnyNetwork (v6)", ), pytest.param(pydantic_v1.BaseModel, pydantic_v1.EmailStr, "test@example.com", id="pydantic_v1.EmailStr"), pytest.param(pydantic_v2.BaseModel, pydantic_v2.EmailStr, "test@example.com", id="pydantic_v2.EmailStr"), ], ) def test_dto_with_non_instantiable_types(base_model: BaseModelType, type_: Any, in_: Any) -> None: class Model(base_model): # type: ignore[misc, valid-type] foo: type_ @post("/", dto=PydanticDTO[Model]) async def handler(data: Model) -> Model: return data with create_test_client(handler) as client: res = client.post("/", json={"foo": in_}) assert res.status_code == 201 assert res.json() == {"foo": in_} @pytest.mark.parametrize( "plugin_params, response", ( ( {"exclude": {"alias"}}, { "none": None, "default": "default", }, ), ({"exclude_defaults": True}, {"alias": "prefer_alias"}), ({"exclude_none": True}, {"alias": "prefer_alias", "default": "default"}), ({"exclude_unset": True}, {"alias": "prefer_alias"}), ({"include": {"alias"}}, {"alias": "prefer_alias"}), ({"prefer_alias": True}, {"prefer_alias": "prefer_alias", "default": "default", "none": None}), ), ids=( "Exclude alias field", "Exclude default fields", "Exclude None field", "Exclude unset fields", "Include alias field", "Use alias in response", ), ) def test_params_with_v1_and_v2_models(plugin_params: dict, response: dict) -> None: class ModelV1(pydantic_v1.BaseModel): # pyright: ignore alias: str = pydantic_v1.fields.Field(alias="prefer_alias") # pyright: ignore default: str = "default" none: None = None class Config: allow_population_by_field_name = True class ModelV2(pydantic_v2.BaseModel): alias: str = pydantic_v2.fields.Field(serialization_alias="prefer_alias") default: str = "default" none: None = None @post("/v1") async def handler_v1() -> ModelV1: return ModelV1(alias="prefer_alias") # type: ignore[call-arg] @post("/v2") async def handler_v2() -> ModelV2: return ModelV2(alias="prefer_alias") with create_test_client([handler_v1, handler_v2], plugins=[PydanticPlugin(**plugin_params)]) as client: assert client.post("/v1").json() == response assert client.post("/v2").json() == response @pytest.mark.parametrize( "validate_strict,expect_error", [ (False, False), (None, False), (True, True), ], ) def test_v2_strict_validate( validate_strict: bool, expect_error: bool, ) -> None: # https://github.com/litestar-org/litestar/issues/3572 class Model(pydantic_v2.BaseModel): test_bool: pydantic_v2.StrictBool @post("/") async def handler(data: Model) -> None: return None plugins = [] if validate_strict is not None: plugins.append(PydanticInitPlugin(validate_strict=validate_strict)) with create_test_client([handler], plugins=plugins) as client: res = client.post("/", json={"test_bool": "YES"}) assert res.status_code == 400 if expect_error else 201 def test_model_defaults(pydantic_version: PydanticVersion) -> None: lib = pydantic_v1 if pydantic_version == "v1" else pydantic_v2 class Model(lib.BaseModel): # type: ignore[misc, name-defined] a: int b: int = lib.Field(default=1) c: int = lib.Field(default_factory=lambda: 3) @post("/") async def handler(data: Model) -> Dict[str, int]: return {"a": data.a, "b": data.b, "c": data.c} with create_test_client([handler]) as client: schema = client.app.openapi_schema.components.schemas["test_model_defaults.Model"] res = client.post("/", json={"a": 5}) assert res.status_code == 201 assert res.json() == {"a": 5, "b": 1, "c": 3} assert schema.required == ["a"] assert schema.properties["b"].default == 1 assert schema.properties["c"].default is None @pytest.mark.parametrize("with_dto", [True, False]) def test_v2_computed_fields(with_dto: bool) -> None: # https://github.com/litestar-org/litestar/issues/3656 class Model(pydantic_v2.BaseModel): foo: int = 1 @pydantic_v2.computed_field def bar(self) -> int: return 2 @pydantic_v2.computed_field(examples=[1], json_schema_extra={"title": "this is computed"}) def baz(self) -> int: return 3 @get("/", return_dto=PydanticDTO[Model] if with_dto else None) async def handler() -> Model: return Model() component_name = "HandlerModelResponseBody" if with_dto else "test_v2_computed_fields.Model" with create_test_client([handler]) as client: schema = client.app.openapi_schema.components.schemas[component_name] res = client.get("/") assert list(schema.properties.keys()) == ["foo", "bar", "baz"] assert schema.properties["baz"].title == "this is computed" assert schema.properties["baz"].examples == [1] assert res.json() == {"foo": 1, "bar": 2, "baz": 3} litestar-2.16.0/tests/unit/test_plugins/test_pydantic/test_openapi.py000066400000000000000000001042701500564371300262160ustar00rootroot00000000000000# pyright: reportOptionalSubscript=false, reportGeneralTypeIssues=false from datetime import date, timedelta from decimal import Decimal from types import ModuleType from typing import Any, Callable, Dict, List, Optional, Pattern, Type, Union, cast import annotated_types import pydantic as pydantic_v2 import pytest from pydantic import v1 as pydantic_v1 from typing_extensions import Annotated from litestar import Litestar, get, post from litestar._openapi.schema_generation.schema import SchemaCreator from litestar.openapi import OpenAPIConfig from litestar.openapi.spec import Reference, Schema from litestar.openapi.spec.enums import OpenAPIFormat, OpenAPIType from litestar.plugins.pydantic import PydanticPlugin, PydanticSchemaPlugin from litestar.testing import TestClient, create_test_client from litestar.typing import FieldDefinition from litestar.utils import is_class_and_subclass from tests.helpers import get_schema_for_field_definition from tests.unit.test_plugins.test_pydantic.models import ( PydanticDataclassPerson, PydanticPerson, PydanticV1DataclassPerson, PydanticV1Person, ) from . import PydanticVersion AnyBaseModelType = Type[Union[pydantic_v1.BaseModel, pydantic_v2.BaseModel]] constrained_string_v1 = [ pydantic_v1.constr(regex="^[a-zA-Z]$"), pydantic_v1.constr(to_upper=True, min_length=1, regex="^[a-zA-Z]$"), pydantic_v1.constr(to_lower=True, min_length=1, regex="^[a-zA-Z]$"), pydantic_v1.constr(to_lower=True, min_length=10, regex="^[a-zA-Z]$"), pydantic_v1.constr(to_lower=True, min_length=10, max_length=100, regex="^[a-zA-Z]$"), pydantic_v1.constr(min_length=1), pydantic_v1.constr(min_length=10), pydantic_v1.constr(min_length=10, max_length=100), pydantic_v1.conbytes(min_length=1), pydantic_v1.conbytes(min_length=10), pydantic_v1.conbytes(min_length=10, max_length=100), ] constrained_string_v2 = [ pydantic_v2.constr(pattern="^[a-zA-Z]$"), pydantic_v2.constr(to_upper=True, min_length=1, pattern="^[a-zA-Z]$"), pydantic_v2.constr(to_lower=True, min_length=1, pattern="^[a-zA-Z]$"), pydantic_v2.constr(to_lower=True, min_length=10, pattern="^[a-zA-Z]$"), pydantic_v2.constr(to_lower=True, min_length=10, max_length=100, pattern="^[a-zA-Z]$"), pydantic_v2.constr(min_length=1), pydantic_v2.constr(min_length=10), pydantic_v2.constr(min_length=10, max_length=100), ] constrained_bytes_v2 = [ pydantic_v2.conbytes(min_length=1), pydantic_v2.conbytes(min_length=10), pydantic_v2.conbytes(min_length=10, max_length=100), ] constrained_collection_v1 = [ pydantic_v1.conlist(int, min_items=1), pydantic_v1.conlist(int, min_items=1, max_items=10), pydantic_v1.conset(int, min_items=1), pydantic_v1.conset(int, min_items=1, max_items=10), ] constrained_collection_v2 = [ pydantic_v2.conlist(int, min_length=1), pydantic_v2.conlist(int, min_length=1, max_length=10), pydantic_v2.conset(int, min_length=1), pydantic_v2.conset(int, min_length=1, max_length=10), ] constrained_numbers_v1 = [ pydantic_v1.conint(gt=10, lt=100), pydantic_v1.conint(ge=10, le=100), pydantic_v1.conint(ge=10, le=100, multiple_of=7), pydantic_v1.confloat(gt=10, lt=100), pydantic_v1.confloat(ge=10, le=100), pydantic_v1.confloat(ge=10, le=100, multiple_of=4.2), pydantic_v1.confloat(gt=10, lt=100, multiple_of=10), pydantic_v1.condecimal(gt=Decimal("10"), lt=Decimal("100")), pydantic_v1.condecimal(ge=Decimal("10"), le=Decimal("100")), pydantic_v1.condecimal(gt=Decimal("10"), lt=Decimal("100"), multiple_of=Decimal("5")), pydantic_v1.condecimal(ge=Decimal("10"), le=Decimal("100"), multiple_of=Decimal("2")), ] constrained_numbers_v2 = [ pydantic_v2.conint(gt=10, lt=100), pydantic_v2.conint(ge=10, le=100), pydantic_v2.conint(ge=10, le=100, multiple_of=7), pydantic_v2.confloat(gt=10, lt=100), pydantic_v2.confloat(ge=10, le=100), pydantic_v2.confloat(ge=10, le=100, multiple_of=4.2), pydantic_v2.confloat(gt=10, lt=100, multiple_of=10), pydantic_v2.condecimal(gt=Decimal("10"), lt=Decimal("100")), pydantic_v2.condecimal(ge=Decimal("10"), le=Decimal("100")), pydantic_v2.condecimal(gt=Decimal("10"), lt=Decimal("100"), multiple_of=Decimal("5")), pydantic_v2.condecimal(ge=Decimal("10"), le=Decimal("100"), multiple_of=Decimal("2")), ] constrained_dates_v1 = [ pydantic_v1.condate(gt=date.today() - timedelta(days=10), lt=date.today() + timedelta(days=100)), pydantic_v1.condate(ge=date.today() - timedelta(days=10), le=date.today() + timedelta(days=100)), pydantic_v1.condate(gt=date.today() - timedelta(days=10), lt=date.today() + timedelta(days=100)), pydantic_v1.condate(ge=date.today() - timedelta(days=10), le=date.today() + timedelta(days=100)), ] constrained_dates_v2 = [ pydantic_v2.condate(gt=date.today() - timedelta(days=10), lt=date.today() + timedelta(days=100)), pydantic_v2.condate(ge=date.today() - timedelta(days=10), le=date.today() + timedelta(days=100)), pydantic_v2.condate(gt=date.today() - timedelta(days=10), lt=date.today() + timedelta(days=100)), pydantic_v2.condate(ge=date.today() - timedelta(days=10), le=date.today() + timedelta(days=100)), ] @pytest.fixture() def schema_creator(plugin: PydanticSchemaPlugin) -> SchemaCreator: return SchemaCreator(plugins=[plugin]) @pytest.fixture() def plugin() -> PydanticSchemaPlugin: return PydanticSchemaPlugin() @pytest.mark.parametrize("annotation", constrained_collection_v1) def test_create_collection_constrained_field_schema_pydantic_v1( annotation: Any, schema_creator: SchemaCreator, plugin: PydanticSchemaPlugin, ) -> None: class Model(pydantic_v1.BaseModel): field: annotation schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] assert schema.type == OpenAPIType.ARRAY # pyright: ignore[reportAttributeAccessIssue] assert schema.items.type == OpenAPIType.INTEGER # type: ignore[union-attr] # pyright: ignore[reportAttributeAccessIssue] assert schema.min_items == annotation.min_items # pyright: ignore[reportAttributeAccessIssue] assert schema.max_items == annotation.max_items # pyright: ignore[reportAttributeAccessIssue] @pytest.mark.parametrize("make_constraint", [pydantic_v2.conlist, pydantic_v2.conset, pydantic_v2.confrozenset]) @pytest.mark.parametrize( "min_length, max_length", [ (None, None), (1, None), (1, 1), (None, 1), ], ) def test_create_collection_constrained_field_schema_pydantic_v2( make_constraint: Callable[..., Any], min_length: Optional[int], max_length: Optional[int], schema_creator: SchemaCreator, plugin: PydanticSchemaPlugin, ) -> None: class Model(pydantic_v2.BaseModel): field: make_constraint(int, min_length=min_length, max_length=max_length) # type: ignore[valid-type] schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] assert schema.type == OpenAPIType.ARRAY # pyright: ignore[reportAttributeAccessIssue] assert schema.items.type == OpenAPIType.INTEGER # type: ignore[union-attr] assert schema.min_items == min_length # pyright: ignore[reportAttributeAccessIssue] assert schema.max_items == max_length # pyright: ignore[reportAttributeAccessIssue] @pytest.fixture() def conset(pydantic_version: PydanticVersion) -> Any: return pydantic_v1.conset if pydantic_version == "v1" else pydantic_v2.conset @pytest.fixture() def conlist(pydantic_version: PydanticVersion) -> Any: return pydantic_v1.conlist if pydantic_version == "v1" else pydantic_v2.conlist def test_create_collection_constrained_field_schema_sub_fields( pydantic_version: PydanticVersion, conset: Any, conlist: Any, schema_creator: SchemaCreator, plugin: PydanticSchemaPlugin, ) -> None: if pydantic_version == "v1": class Modelv1(pydantic_v1.BaseModel): set_field: conset(Union[str, int], min_items=1, max_items=10) # type: ignore[valid-type] list_field: conlist(Union[str, int], min_items=1, max_items=10) # type: ignore[valid-type] model_schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Modelv1), plugin) else: class Modelv2(pydantic_v2.BaseModel): set_field: conset(Union[str, int], min_length=1, max_length=10) # type: ignore[valid-type] list_field: conlist(Union[str, int], min_length=1, max_length=10) # type: ignore[valid-type] model_schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Modelv2), plugin) def _get_schema_type(s: Any) -> OpenAPIType: assert isinstance(s, Schema) assert isinstance(s.type, OpenAPIType) return s.type for field_name in ["set_field", "list_field"]: schema = model_schema.properties[field_name] # pyright: ignore[reportAttributeAccessIssue] assert schema.type == OpenAPIType.ARRAY # pyright: ignore[reportAttributeAccessIssue] assert schema.max_items == 10 # pyright: ignore[reportAttributeAccessIssue] assert schema.min_items == 1 # pyright: ignore[reportAttributeAccessIssue] assert isinstance(schema.items, Schema) # pyright: ignore[reportAttributeAccessIssue] assert schema.items.one_of is not None # pyright: ignore[reportAttributeAccessIssue] # https://github.com/litestar-org/litestar/pull/2570#issuecomment-1788122570 assert {_get_schema_type(s) for s in schema.items.one_of} == {OpenAPIType.STRING, OpenAPIType.INTEGER} # pyright: ignore[reportAttributeAccessIssue] # set should have uniqueItems always assert model_schema.properties["set_field"].unique_items # pyright: ignore[reportAttributeAccessIssue] @pytest.mark.parametrize("annotation", constrained_string_v1) def test_create_string_constrained_field_schema_pydantic_v1( annotation: Any, schema_creator: SchemaCreator, plugin: PydanticSchemaPlugin, ) -> None: class Model(pydantic_v1.BaseModel): field: annotation schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] assert schema.type == OpenAPIType.STRING # pyright: ignore[reportAttributeAccessIssue] assert schema.min_length == annotation.min_length # pyright: ignore[reportAttributeAccessIssue] assert schema.max_length == annotation.max_length # pyright: ignore[reportAttributeAccessIssue] if pattern := getattr(annotation, "regex", None): assert schema.pattern == pattern.pattern if isinstance(pattern, Pattern) else pattern # pyright: ignore[reportAttributeAccessIssue] if annotation.to_lower: assert schema.description if annotation.to_upper: assert schema.description @pytest.mark.parametrize("annotation", constrained_string_v2) def test_create_string_constrained_field_schema_pydantic_v2( annotation: Any, schema_creator: SchemaCreator, plugin: PydanticSchemaPlugin, ) -> None: constraint: pydantic_v2.types.StringConstraints = annotation.__metadata__[0] class Model(pydantic_v2.BaseModel): field: annotation schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] assert schema.type == OpenAPIType.STRING # pyright: ignore[reportAttributeAccessIssue] assert schema.min_length == constraint.min_length # pyright: ignore[reportAttributeAccessIssue] assert schema.max_length == constraint.max_length # pyright: ignore[reportAttributeAccessIssue] assert schema.pattern == constraint.pattern # pyright: ignore[reportAttributeAccessIssue] if constraint.to_upper: assert schema.description == "must be in upper case" if constraint.to_lower: assert schema.description == "must be in lower case" @pytest.mark.parametrize("annotation", constrained_bytes_v2) def test_create_byte_constrained_field_schema_pydantic_v2( annotation: Any, schema_creator: SchemaCreator, plugin: PydanticSchemaPlugin, ) -> None: constraint: annotated_types.Len = annotation.__metadata__[1] class Model(pydantic_v2.BaseModel): field: annotation schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] assert schema.type == OpenAPIType.STRING # pyright: ignore[reportAttributeAccessIssue] assert schema.min_length == constraint.min_length # pyright: ignore[reportAttributeAccessIssue] assert schema.max_length == constraint.max_length # pyright: ignore[reportAttributeAccessIssue] @pytest.mark.parametrize("annotation", constrained_numbers_v1) def test_create_numerical_constrained_field_schema_pydantic_v1( annotation: Any, schema_creator: SchemaCreator, plugin: PydanticSchemaPlugin, ) -> None: from pydantic.v1.types import ConstrainedDecimal, ConstrainedFloat, ConstrainedInt annotation = cast(Union[ConstrainedInt, ConstrainedFloat, ConstrainedDecimal], annotation) class Model(pydantic_v1.BaseModel): field: annotation schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] assert ( schema.type == OpenAPIType.INTEGER if is_class_and_subclass(annotation, ConstrainedInt) else OpenAPIType.NUMBER # pyright: ignore[reportAttributeAccessIssue] ) assert schema.exclusive_minimum == annotation.gt # pyright: ignore[reportAttributeAccessIssue] assert schema.minimum == annotation.ge # pyright: ignore[reportAttributeAccessIssue] assert schema.exclusive_maximum == annotation.lt # pyright: ignore[reportAttributeAccessIssue] assert schema.maximum == annotation.le # pyright: ignore[reportAttributeAccessIssue] assert schema.multiple_of == annotation.multiple_of # pyright: ignore[reportAttributeAccessIssue] @pytest.mark.parametrize( "make_constraint, constraint_kwargs", [ (pydantic_v2.conint, {"gt": 10, "lt": 100}), (pydantic_v2.conint, {"ge": 10, "le": 100}), (pydantic_v2.conint, {"ge": 10, "le": 100, "multiple_of": 7}), (pydantic_v2.confloat, {"gt": 10, "lt": 100}), (pydantic_v2.confloat, {"ge": 10, "le": 100}), (pydantic_v2.confloat, {"ge": 10, "le": 100, "multiple_of": 4.2}), (pydantic_v2.confloat, {"gt": 10, "lt": 100, "multiple_of": 10}), (pydantic_v2.condecimal, {"gt": Decimal("10"), "lt": Decimal("100")}), (pydantic_v2.condecimal, {"ge": Decimal("10"), "le": Decimal("100")}), (pydantic_v2.condecimal, {"gt": Decimal("10"), "lt": Decimal("100"), "multiple_of": Decimal("5")}), (pydantic_v2.condecimal, {"ge": Decimal("10"), "le": Decimal("100"), "multiple_of": Decimal("2")}), ], ) def test_create_numerical_constrained_field_schema_pydantic_v2( make_constraint: Any, constraint_kwargs: Dict[str, Any], schema_creator: SchemaCreator, plugin: PydanticSchemaPlugin, ) -> None: annotation = make_constraint(**constraint_kwargs) class Model(pydantic_v1.BaseModel): field: annotation # type: ignore[valid-type] schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] assert schema.type == OpenAPIType.INTEGER if is_class_and_subclass(annotation, int) else OpenAPIType.NUMBER # pyright: ignore[reportAttributeAccessIssue] assert schema.exclusive_minimum == constraint_kwargs.get("gt") # pyright: ignore[reportAttributeAccessIssue] assert schema.minimum == constraint_kwargs.get("ge") # pyright: ignore[reportAttributeAccessIssue] assert schema.exclusive_maximum == constraint_kwargs.get("lt") # pyright: ignore[reportAttributeAccessIssue] assert schema.maximum == constraint_kwargs.get("le") # pyright: ignore[reportAttributeAccessIssue] assert schema.multiple_of == constraint_kwargs.get("multiple_of") # pyright: ignore[reportAttributeAccessIssue] @pytest.mark.parametrize("annotation", constrained_dates_v1) def test_create_date_constrained_field_schema_pydantic_v1( annotation: Any, schema_creator: SchemaCreator, plugin: PydanticSchemaPlugin, ) -> None: class Model(pydantic_v1.BaseModel): field: annotation schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] assert schema.type == OpenAPIType.STRING # pyright: ignore[reportAttributeAccessIssue] assert schema.format == OpenAPIFormat.DATE # pyright: ignore[reportAttributeAccessIssue] if gt := annotation.gt: assert date.fromtimestamp(schema.exclusive_minimum) == gt # type: ignore[arg-type] # pyright: ignore[reportArgumentType] if ge := annotation.ge: assert date.fromtimestamp(schema.minimum) == ge # type: ignore[arg-type] if lt := annotation.lt: assert date.fromtimestamp(schema.exclusive_maximum) == lt # type: ignore[arg-type] if le := annotation.le: assert date.fromtimestamp(schema.maximum) == le # type: ignore[arg-type] @pytest.mark.parametrize( "constraints", [ {"gt": date.today() - timedelta(days=10), "lt": date.today() + timedelta(days=100)}, {"ge": date.today() - timedelta(days=10), "le": date.today() + timedelta(days=100)}, {"gt": date.today() - timedelta(days=10), "lt": date.today() + timedelta(days=100)}, {"ge": date.today() - timedelta(days=10), "le": date.today() + timedelta(days=100)}, ], ) def test_create_date_constrained_field_schema_pydantic_v2( constraints: Dict[str, Any], schema_creator: SchemaCreator, plugin: PydanticSchemaPlugin, ) -> None: class Model(pydantic_v2.BaseModel): field: pydantic_v2.condate(**constraints) # type: ignore[valid-type] schema = schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] assert schema.type == OpenAPIType.STRING # pyright: ignore[reportAttributeAccessIssue] assert schema.format == OpenAPIFormat.DATE # pyright: ignore[reportAttributeAccessIssue] if gt := constraints.get("gt"): assert date.fromtimestamp(schema.exclusive_minimum) == gt # type: ignore[arg-type] if ge := constraints.get("ge"): assert date.fromtimestamp(schema.minimum) == ge # type: ignore[arg-type] if lt := constraints.get("lt"): assert date.fromtimestamp(schema.exclusive_maximum) == lt # type: ignore[arg-type] if le := constraints.get("le"): assert date.fromtimestamp(schema.maximum) == le # type: ignore[arg-type] @pytest.mark.parametrize( "annotation", [ *constrained_numbers_v1, *constrained_collection_v1, *constrained_string_v1, *constrained_dates_v1, ], ) def test_create_constrained_field_schema_v1( annotation: Any, schema_creator: SchemaCreator, plugin: PydanticSchemaPlugin, ) -> None: class Model(pydantic_v1.BaseModel): field: annotation assert schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # pyright: ignore[reportAttributeAccessIssue] @pytest.mark.parametrize( "annotation", [ *constrained_numbers_v2, *constrained_collection_v2, *constrained_string_v2, *constrained_dates_v2, ], ) def test_create_constrained_field_schema_v2( annotation: Any, schema_creator: SchemaCreator, plugin: PydanticSchemaPlugin, ) -> None: class Model(pydantic_v2.BaseModel): field: annotation assert schema_creator.for_plugin(FieldDefinition.from_annotation(Model), plugin).properties["field"] # type: ignore[index, union-attr] @pytest.mark.parametrize("cls", (PydanticPerson, PydanticDataclassPerson, PydanticV1Person, PydanticV1DataclassPerson)) def test_spec_generation(cls: Any) -> None: @post("/") def handler(data: cls) -> cls: return data with create_test_client(handler) as client: schema = client.app.openapi_schema assert schema assert schema.to_schema()["components"]["schemas"][cls.__name__] == { "properties": { "first_name": {"type": "string"}, "last_name": {"type": "string"}, "id": {"type": "string"}, "optional": {"oneOf": [{"type": "string"}, {"type": "null"}]}, "complex": { "type": "object", "additionalProperties": { "type": "array", "items": {"type": "object", "additionalProperties": {"type": "string"}}, }, }, "union": {"oneOf": [{"type": "integer"}, {"items": {"type": "string"}, "type": "array"}]}, "pets": { "oneOf": [ { "items": {"$ref": "#/components/schemas/DataclassPet"}, "type": "array", }, {"type": "null"}, ] }, }, "type": "object", "required": ["complex", "first_name", "id", "last_name", "union"], "title": f"{cls.__name__}", } def test_schema_generation_v1() -> None: class Lookup(pydantic_v1.BaseModel): id: Annotated[ str, pydantic_v1.Field( min_length=12, max_length=16, description="A unique identifier", example="e4eaaaf2-d142-11e1-b3e4-080027620cdd", # pyright: ignore examples=["31", "32"], ), ] with_title: str = pydantic_v1.Field(title="WITH_title") @post("/example") async def example_route() -> Lookup: return Lookup(id="1234567812345678", with_title="1") app = Litestar([example_route]) schema = app.openapi_schema.to_schema() lookup_schema = schema["components"]["schemas"]["test_schema_generation_v1.Lookup"]["properties"] assert lookup_schema["id"] == { "description": "A unique identifier", "examples": ["e4eaaaf2-d142-11e1-b3e4-080027620cdd", "31", "32"], "maxLength": 16, "minLength": 12, "type": "string", } assert lookup_schema["with_title"] == {"title": "WITH_title", "type": "string"} def test_schema_generation_v2() -> None: class Lookup(pydantic_v2.BaseModel): id: Annotated[ str, pydantic_v2.Field( min_length=12, max_length=16, description="A unique identifier", # we expect these examples to be merged json_schema_extra={"example": "e4eaaaf2-d142-11e1-b3e4-080027620cdd", "examples": ["31"]}, examples=["32"], ), ] # title should work if given on the field with_title: str = pydantic_v2.Field(title="WITH_title") # or as an extra with_extra_title: str = pydantic_v2.Field(json_schema_extra={"title": "WITH_extra"}) # moreover, we allow json_schema_extra to use names that exactly match the JSONSchema spec without_duplicates: List[str] = pydantic_v2.Field(json_schema_extra={"uniqueItems": True}) @post("/example") async def example_route() -> Lookup: return Lookup(id="1234567812345678", with_title="1", with_extra_title="2", without_duplicates=[]) app = Litestar([example_route]) schema = app.openapi_schema.to_schema() lookup_schema = schema["components"]["schemas"]["test_schema_generation_v2.Lookup"]["properties"] assert lookup_schema["id"] == { "description": "A unique identifier", "examples": ["e4eaaaf2-d142-11e1-b3e4-080027620cdd", "31", "32"], "maxLength": 16, "minLength": 12, "type": "string", } assert lookup_schema["with_title"] == {"title": "WITH_title", "type": "string"} assert lookup_schema["with_extra_title"] == {"title": "WITH_extra", "type": "string"} assert lookup_schema["without_duplicates"] == {"type": "array", "items": {"type": "string"}, "uniqueItems": True} def test_create_examples(pydantic_version: PydanticVersion) -> None: lib = pydantic_v1 if pydantic_version == "v1" else pydantic_v2 class Model(lib.BaseModel): # type: ignore[name-defined, misc] foo: str = lib.Field(examples=["32"]) bar: str @get("/example") async def handler() -> Model: return Model(foo="1", bar="2") app = Litestar( [handler], openapi_config=OpenAPIConfig( title="Test", version="0", create_examples=True, ), ) schema = app.openapi_schema.to_schema() lookup_schema = schema["components"]["schemas"]["test_create_examples.Model"]["properties"] assert lookup_schema["foo"]["examples"] == ["32"] assert lookup_schema["bar"]["examples"] def test_v2_json_schema_extra_callable_raises() -> None: class Model(pydantic_v2.BaseModel): field: str = pydantic_v2.Field(json_schema_extra=lambda e: None) @get("/example") def handler() -> Model: return Model(field="1") app = Litestar([handler]) with pytest.raises(ValueError, match="Callables not supported"): app.openapi_schema def test_schema_by_alias(base_model: AnyBaseModelType, pydantic_version: PydanticVersion) -> None: class RequestWithAlias(base_model): # type: ignore[valid-type,misc] first: str = (pydantic_v1.Field if pydantic_version == "v1" else pydantic_v2.Field)(alias="second") class ResponseWithAlias(base_model): # type: ignore[valid-type,misc] first: str = (pydantic_v1.Field if pydantic_version == "v1" else pydantic_v2.Field)(alias="second") @post("/", signature_types=[RequestWithAlias, ResponseWithAlias]) def handler(data: RequestWithAlias) -> ResponseWithAlias: return ResponseWithAlias(second=data.first) app = Litestar(route_handlers=[handler], openapi_config=OpenAPIConfig(title="my title", version="1.0.0")) assert app.openapi_schema schemas = app.openapi_schema.to_schema()["components"]["schemas"] request_key = "second" assert schemas["test_schema_by_alias.RequestWithAlias"] == { "properties": {request_key: {"type": "string"}}, "type": "object", "required": [request_key], "title": "RequestWithAlias", } response_key = "first" assert schemas["test_schema_by_alias.ResponseWithAlias"] == { "properties": {response_key: {"type": "string"}}, "type": "object", "required": [response_key], "title": "ResponseWithAlias", } with TestClient(app) as client: response = client.post("/", json={request_key: "foo"}) assert response.json() == {response_key: "foo"} def test_schema_by_alias_plugin_override(base_model: AnyBaseModelType, pydantic_version: PydanticVersion) -> None: class RequestWithAlias(base_model): # type: ignore[misc, valid-type] first: str = (pydantic_v1.Field if pydantic_version == "v1" else pydantic_v2.Field)(alias="second") class ResponseWithAlias(base_model): # type: ignore[misc, valid-type] first: str = (pydantic_v1.Field if pydantic_version == "v1" else pydantic_v2.Field)(alias="second") @post("/", signature_types=[RequestWithAlias, ResponseWithAlias]) def handler(data: RequestWithAlias) -> ResponseWithAlias: return ResponseWithAlias(second=data.first) app = Litestar( route_handlers=[handler], openapi_config=OpenAPIConfig(title="my title", version="1.0.0"), plugins=[PydanticPlugin(prefer_alias=True)], ) assert app.openapi_schema schemas = app.openapi_schema.to_schema()["components"]["schemas"] request_key = "second" assert schemas["test_schema_by_alias_plugin_override.RequestWithAlias"] == { "properties": {request_key: {"type": "string"}}, "type": "object", "required": [request_key], "title": "RequestWithAlias", } response_key = "second" assert schemas["test_schema_by_alias_plugin_override.ResponseWithAlias"] == { "properties": {response_key: {"type": "string"}}, "type": "object", "required": [response_key], "title": "ResponseWithAlias", } with TestClient(app) as client: response = client.post("/", json={request_key: "foo"}) assert response.json() == {response_key: "foo"} def test_create_schema_for_field_v1() -> None: class Model(pydantic_v1.BaseModel): value: str = pydantic_v1.Field( title="title", description="description", example="example", max_length=16, # pyright: ignore ) schema = get_schema_for_field_definition( FieldDefinition.from_kwarg(name="Model", annotation=Model), plugins=[PydanticSchemaPlugin()] ) assert schema.properties value = schema.properties["value"] assert isinstance(value, Schema) assert value.description == "description" assert value.title == "title" assert value.examples == ["example"] def test_create_schema_for_field_v2() -> None: class Model(pydantic_v2.BaseModel): value: str = pydantic_v2.Field( title="title", description="description", max_length=16, json_schema_extra={"example": "example"} ) schema = get_schema_for_field_definition( FieldDefinition.from_kwarg(name="Model", annotation=Model), plugins=[PydanticSchemaPlugin()] ) assert schema.properties value = schema.properties["value"] assert isinstance(value, Schema) assert value.description == "description" assert value.title == "title" assert value.examples == ["example"] def test_create_schema_for_field_v2_examples() -> None: class Model(pydantic_v2.BaseModel): value: str = pydantic_v2.Field( title="title", description="description", max_length=16, json_schema_extra={"examples": ["example"]} ) schema = get_schema_for_field_definition( FieldDefinition.from_kwarg(name="Model", annotation=Model), plugins=[PydanticSchemaPlugin()] ) assert schema.properties value = schema.properties["value"] assert isinstance(value, Schema) assert value.description == "description" assert value.title == "title" assert value.examples == ["example"] @pytest.mark.parametrize("with_future_annotations", [True, False]) def test_create_schema_for_pydantic_model_with_annotated_model_attribute( with_future_annotations: bool, create_module: "Callable[[str], ModuleType]", pydantic_version: PydanticVersion ) -> None: """Test that a model with an annotated attribute is correctly handled.""" module = create_module( f""" {'from __future__ import annotations' if with_future_annotations else ''} from typing_extensions import Annotated {'from pydantic import BaseModel' if pydantic_version == 'v2' else 'from pydantic.v1 import BaseModel'} class Foo(BaseModel): foo: Annotated[int, "Foo description"] """ ) creator = SchemaCreator(plugins=[PydanticSchemaPlugin()]) creator.for_field_definition(FieldDefinition.from_annotation(module.Foo)) schemas = creator.schema_registry.generate_components_schemas() schema = schemas["Foo"] assert schema.properties and "foo" in schema.properties def test_create_schema_for_pydantic_model_with_unhashable_literal_default( create_module: "Callable[[str], ModuleType]", ) -> None: """Test that a model with unhashable literal defaults is correctly handled.""" module = create_module( """ from pydantic import BaseModel, Field class Model(BaseModel): id: int dict_default: dict = {} dict_default_in_field: dict = Field(default={}) dict_default_in_factory: dict = Field(default_factory=dict) list_default: list = [] list_default_in_field: list = Field(default=[]) list_default_in_factory: list = Field(default_factory=list) """ ) creator = SchemaCreator(plugins=[PydanticSchemaPlugin()]) creator.for_field_definition(FieldDefinition.from_annotation(module.Model)) schemas = creator.schema_registry.generate_components_schemas() schema = schemas["Model"] assert schema.properties assert "dict_default" in schema.properties assert "dict_default_in_field" in schema.properties assert "dict_default_in_factory" in schema.properties assert "list_default" in schema.properties assert "list_default_in_field" in schema.properties assert "list_default_in_factory" in schema.properties @pytest.mark.parametrize("field_type", [pydantic_v2.AnyUrl, pydantic_v2.AnyHttpUrl, pydantic_v2.HttpUrl]) def test_create_for_url_v2(field_type: Any) -> None: field_definition = FieldDefinition.from_annotation(field_type) schema = SchemaCreator(plugins=[PydanticSchemaPlugin()]).for_field_definition(field_definition) assert schema.type == OpenAPIType.STRING # type: ignore[union-attr] assert schema.format == OpenAPIFormat.URL # type: ignore[union-attr] @pytest.mark.parametrize("prefer_alias", [True, False]) def test_create_for_computed_field(prefer_alias: bool) -> None: class Sample(pydantic_v2.BaseModel): property_one: str @pydantic_v2.computed_field( description="a description", title="a title", alias="prop_two" if prefer_alias else None ) def property_two(self) -> bool: return True field_definition = FieldDefinition.from_annotation(Sample) schema_creator = SchemaCreator(plugins=[PydanticSchemaPlugin()]) ref = schema_creator.for_field_definition(field_definition) assert isinstance(ref, Reference) registered_schemas = list(schema_creator.schema_registry) assert len(registered_schemas) == 1 schema = registered_schemas[0].schema assert schema.required == ["property_one", "property_two"] if not prefer_alias else ["property_one", "prop_two"] properties = schema.properties assert properties is not None assert properties.keys() == {"property_one", "property_two"} if not prefer_alias else {"property_one", "prop_two"} property_two = properties["property_two"] if not prefer_alias else properties["prop_two"] assert isinstance(property_two, Schema) assert property_two.type == OpenAPIType.BOOLEAN assert property_two.description == "a description" assert property_two.title == "a title" assert property_two.read_only litestar-2.16.0/tests/unit/test_plugins/test_pydantic/test_plugin_serialization.py000066400000000000000000000215421500564371300310160ustar00rootroot00000000000000from __future__ import annotations import datetime import json from decimal import Decimal from functools import partial from pathlib import Path from typing import Any import pydantic as pydantic_v2 import pytest from pydantic import v1 as pydantic_v1 from pydantic.v1.color import Color as ColorV1 from pydantic_extra_types.color import Color as ColorV2 from litestar.exceptions import SerializationException from litestar.plugins.pydantic import PydanticInitPlugin, _model_dump, _model_dump_json from litestar.serialization import ( decode_json, decode_msgpack, default_serializer, encode_json, encode_msgpack, get_serializer, ) from . import PydanticVersion TODAY = datetime.date.today() class CustomStr(str): pass class CustomInt(int): pass class CustomFloat(float): pass class CustomList(list): pass class CustomSet(set): pass class CustomFrozenSet(frozenset): pass class CustomTuple(tuple): pass class ModelV1(pydantic_v1.BaseModel): class Config: arbitrary_types_allowed = True custom_str: CustomStr = CustomStr() custom_int: CustomInt = CustomInt() custom_float: CustomFloat = CustomFloat() custom_list: CustomList = CustomList() custom_set: CustomSet = CustomSet() custom_frozenset: CustomFrozenSet = CustomFrozenSet() custom_tuple: CustomTuple = CustomTuple() conset: pydantic_v1.conset(int, min_items=1) # type: ignore[valid-type] confrozenset: pydantic_v1.confrozenset(int, min_items=1) # type: ignore[valid-type] conlist: pydantic_v1.conlist(int, min_items=1) # type: ignore[valid-type] path: Path email_str: pydantic_v1.EmailStr name_email: pydantic_v1.NameEmail color: ColorV1 bytesize: pydantic_v1.ByteSize secret_str: pydantic_v1.SecretStr secret_bytes: pydantic_v1.SecretBytes payment_card_number: pydantic_v1.PaymentCardNumber constr: pydantic_v1.constr(min_length=1) # type: ignore[valid-type] conbytes: pydantic_v1.conbytes(min_length=1) # type: ignore[valid-type] condate: pydantic_v1.condate(ge=TODAY) # type: ignore[valid-type] condecimal: pydantic_v1.condecimal(ge=Decimal("1")) # type: ignore[valid-type] confloat: pydantic_v1.confloat(ge=0) # type: ignore[valid-type] conint: pydantic_v1.conint(ge=0) # type: ignore[valid-type] url: pydantic_v1.AnyUrl http_url: pydantic_v1.HttpUrl class ModelV2(pydantic_v2.BaseModel): model_config = {"arbitrary_types_allowed": True} conset: pydantic_v2.conset(int, min_length=1) # type: ignore[valid-type] confrozenset: pydantic_v2.confrozenset(int, min_length=1) # type: ignore[valid-type] conlist: pydantic_v2.conlist(int, min_length=1) # type: ignore[valid-type] path: Path email_str: pydantic_v2.EmailStr name_email: pydantic_v2.NameEmail color: ColorV2 bytesize: pydantic_v2.ByteSize secret_str: pydantic_v2.SecretStr secret_bytes: pydantic_v2.SecretBytes payment_card_number: pydantic_v2.PaymentCardNumber constr: pydantic_v2.constr(min_length=1) # type: ignore[valid-type] conbytes: pydantic_v2.conbytes(min_length=1) # type: ignore[valid-type] condate: pydantic_v2.condate(ge=TODAY) # type: ignore[valid-type] condecimal: pydantic_v2.condecimal(ge=Decimal("1")) # type: ignore[valid-type] confloat: pydantic_v2.confloat(ge=0) # type: ignore[valid-type] conint: pydantic_v2.conint(ge=0) # type: ignore[valid-type] url: pydantic_v2.AnyUrl http_url: pydantic_v2.HttpUrl serializer = partial(default_serializer, type_encoders=PydanticInitPlugin.encoders()) @pytest.fixture() def model_type(pydantic_version: PydanticVersion) -> type[ModelV1 | ModelV2]: return ModelV1 if pydantic_version == "v1" else ModelV2 @pytest.fixture() def model(pydantic_version: PydanticVersion) -> ModelV1 | ModelV2: if pydantic_version == "v1": return ModelV1( path=Path("example"), email_str=pydantic_v1.parse_obj_as(pydantic_v1.EmailStr, "info@example.org"), name_email=pydantic_v1.NameEmail("info", "info@example.org"), color=ColorV1("rgb(255, 255, 255)"), bytesize=pydantic_v1.ByteSize(100), secret_str=pydantic_v1.SecretStr("hello"), secret_bytes=pydantic_v1.SecretBytes(b"hello"), payment_card_number=pydantic_v1.PaymentCardNumber("4000000000000002"), constr="hello", conbytes=b"hello", condate=TODAY, condecimal=Decimal("3.14"), confloat=1.0, conset={1}, confrozenset=frozenset([1]), conint=1, conlist=[1], url="some://example.org/", # type: ignore[arg-type] http_url="http://example.org/", # type: ignore[arg-type] ) return ModelV2( path=Path("example"), email_str=pydantic_v2.parse_obj_as(pydantic_v2.EmailStr, "info@example.org"), # pyright: ignore[reportArgumentType] name_email=pydantic_v2.NameEmail("info", "info@example.org"), color=ColorV2("rgb(255, 255, 255)"), bytesize=pydantic_v2.ByteSize(100), secret_str=pydantic_v2.SecretStr("hello"), secret_bytes=pydantic_v2.SecretBytes(b"hello"), payment_card_number=pydantic_v2.PaymentCardNumber("4000000000000002"), constr="hello", conbytes=b"hello", condate=TODAY, condecimal=Decimal("3.14"), confloat=1.0, conset={1}, confrozenset=frozenset([1]), conint=1, conlist=[1], url="some://example.org/", # type: ignore[arg-type] http_url="http://example.org/", # type: ignore[arg-type] ) @pytest.mark.parametrize( "attribute_name, expected", [ ("path", "example"), ("email_str", "info@example.org"), ("name_email", "info "), ("color", "white"), ("bytesize", 100), ("secret_str", "**********"), ("secret_bytes", "**********"), ("payment_card_number", "4000000000000002"), ("constr", "hello"), ("conbytes", b"hello"), ("condate", TODAY.isoformat()), ("condecimal", 3.14), ("conset", {1}), ("confrozenset", frozenset([1])), ("conint", 1), ("url", "some://example.org/"), ("http_url", "http://example.org/"), ], ) def test_default_serializer(model: ModelV1 | ModelV2, attribute_name: str, expected: Any) -> None: assert serializer(getattr(model, attribute_name)) == expected def test_serialization_of_model_instance(model: ModelV1 | ModelV2) -> None: assert serializer(getattr(model, "conbytes")) == b"hello" assert serializer(model) == _model_dump(model) @pytest.mark.parametrize("prefer_alias", [False, True]) def test_pydantic_json_compatibility( model: ModelV1 | ModelV2, prefer_alias: bool, pydantic_version: PydanticVersion ) -> None: raw = _model_dump_json(model, by_alias=prefer_alias) encoded_json = encode_json(model, serializer=get_serializer(PydanticInitPlugin.encoders(prefer_alias=prefer_alias))) raw_result = json.loads(raw) encoded_result = json.loads(encoded_json) if pydantic_version == "v1": # pydantic v1 dumps decimals into floats as json, we therefore regard this as an error assert raw_result.get("condecimal") == float(encoded_result.get("condecimal")) del raw_result["condecimal"] del encoded_result["condecimal"] assert raw_result == encoded_result @pytest.mark.parametrize("encoder", [encode_json, encode_msgpack]) def test_encoder_raises_serialization_exception(model: ModelV1 | ModelV2, encoder: Any) -> None: with pytest.raises(SerializationException): encoder(object()) @pytest.mark.parametrize("decoder", [decode_json, decode_msgpack]) def test_decode_json_raises_serialization_exception(model: ModelV1 | ModelV2, decoder: Any) -> None: with pytest.raises(SerializationException): decoder(b"str") @pytest.mark.parametrize("prefer_alias", [False, True]) def test_decode_json_typed(model: ModelV1 | ModelV2, prefer_alias: bool, model_type: type[ModelV1 | ModelV2]) -> None: dumped_model = _model_dump_json(model, by_alias=prefer_alias) decoded_model = decode_json(value=dumped_model, target_type=model_type, type_decoders=PydanticInitPlugin.decoders()) assert _model_dump_json(decoded_model, by_alias=prefer_alias) == dumped_model # type: ignore[arg-type] @pytest.mark.parametrize("prefer_alias", [False, True]) def test_decode_msgpack_typed( model: ModelV1 | ModelV2, model_type: type[ModelV1 | ModelV2], prefer_alias: bool ) -> None: model_json = _model_dump_json(model, by_alias=prefer_alias) assert ( decode_msgpack( encode_msgpack(model, serializer=get_serializer(PydanticInitPlugin.encoders(prefer_alias=prefer_alias))), model_type, type_decoders=PydanticInitPlugin.decoders(), ).json() # type: ignore[attr-defined] == model_json ) litestar-2.16.0/tests/unit/test_plugins/test_pydantic/test_pydantic_dto_factory.py000066400000000000000000000133531500564371300307740ustar00rootroot00000000000000from __future__ import annotations from dataclasses import replace from typing import TYPE_CHECKING, Callable, Optional from unittest.mock import ANY import pydantic as pydantic_v2 import pytest from pydantic import v1 as pydantic_v1 from typing_extensions import Annotated from litestar.dto import DTOField, DTOFieldDefinition, Mark, dto_field from litestar.plugins.pydantic import PydanticDTO from litestar.typing import FieldDefinition from . import PydanticVersion if TYPE_CHECKING: from typing import Callable @pytest.fixture def expected_field_defs(int_factory: Callable[[], int]) -> list[DTOFieldDefinition]: return [ DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( annotation=int, name="a", ), model_name=ANY, default_factory=None, dto_field=DTOField(), passthrough_constraints=False, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( annotation=int, name="b", ), model_name=ANY, default_factory=None, dto_field=DTOField(mark=Mark.READ_ONLY), ), metadata=ANY, type_wrappers=ANY, raw=ANY, kwarg_definition=ANY, passthrough_constraints=False, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( annotation=int, name="c", ), model_name=ANY, default_factory=None, dto_field=DTOField(), ), metadata=ANY, type_wrappers=ANY, raw=ANY, kwarg_definition=ANY, passthrough_constraints=False, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( annotation=int, name="d", default=1, ), model_name=ANY, default_factory=None, dto_field=DTOField(), ), metadata=ANY, type_wrappers=ANY, raw=ANY, kwarg_definition=ANY, passthrough_constraints=False, ), replace( DTOFieldDefinition.from_field_definition( field_definition=FieldDefinition.from_kwarg( annotation=Optional[int], name="e", ), model_name=ANY, default_factory=int_factory, dto_field=DTOField(), ), default=None, metadata=ANY, type_wrappers=ANY, raw=ANY, kwarg_definition=ANY, passthrough_constraints=False, ), ] def test_field_definition_generation_v1( int_factory: Callable[[], int], expected_field_defs: list[DTOFieldDefinition], ) -> None: class TestModel(pydantic_v1.BaseModel): a: int b: Annotated[int, DTOField("read-only")] c: Annotated[int, pydantic_v1.Field(gt=1)] d: int = pydantic_v1.Field(default=1) e: int = pydantic_v1.Field(default_factory=int_factory) field_defs = list(PydanticDTO.generate_field_definitions(TestModel)) assert field_defs[0].model_name == "TestModel" for field_def, exp in zip(field_defs, expected_field_defs): assert field_def == exp def test_field_definition_generation_v2( int_factory: Callable[[], int], expected_field_defs: list[DTOFieldDefinition], ) -> None: class TestModel(pydantic_v2.BaseModel): a: int b: Annotated[int, DTOField("read-only")] c: Annotated[int, pydantic_v2.Field(gt=1)] d: int = pydantic_v2.Field(default=1) e: int = pydantic_v2.Field(default_factory=int_factory) field_defs = list(PydanticDTO.generate_field_definitions(TestModel)) assert field_defs[0].model_name == "TestModel" for field_def, exp in zip(field_defs, expected_field_defs): assert field_def == exp def test_detect_nested_field(base_model: type[pydantic_v1.BaseModel | pydantic_v2.BaseModel]) -> None: class TestModel(base_model): # type: ignore[misc, valid-type] a: int class NotModel: pass assert PydanticDTO.detect_nested_field(FieldDefinition.from_annotation(TestModel)) is True assert PydanticDTO.detect_nested_field(FieldDefinition.from_annotation(NotModel)) is False ReadOnlyInt = Annotated[int, DTOField("read-only")] def test_pydantic_dto_annotated_dto_field(base_model: type[pydantic_v1.BaseModel | pydantic_v2.BaseModel]) -> None: class Model(base_model): # type: ignore[misc, valid-type] a: Annotated[int, DTOField("read-only")] b: ReadOnlyInt dto_type = PydanticDTO[Model] fields = list(dto_type.generate_field_definitions(Model)) assert fields[0].dto_field == DTOField("read-only") assert fields[1].dto_field == DTOField("read-only") def test_dto_field_via_pydantic_field_extra_deprecation( pydantic_version: PydanticVersion, ) -> None: if pydantic_version == "v1": class Model(pydantic_v1.BaseModel): # pyright: ignore a: int = pydantic_v1.Field(**dto_field("read-only")) # type: ignore[arg-type, misc] else: class Model(pydantic_v2.BaseModel): # type: ignore[no-redef] a: int = pydantic_v2.Field(**dto_field("read-only")) # type: ignore[call-overload] with pytest.warns(DeprecationWarning): next(PydanticDTO.generate_field_definitions(Model)) litestar-2.16.0/tests/unit/test_plugins/test_pydantic/test_schema_plugin.py000066400000000000000000000130141500564371300273740ustar00rootroot00000000000000import datetime from decimal import Decimal from typing import Any, Generic, Optional, Type, TypeVar, Union import pydantic as pydantic_v2 import pytest from pydantic import v1 as pydantic_v1 from pydantic.v1.generics import GenericModel from typing_extensions import Annotated from litestar import Litestar, post from litestar._openapi.schema_generation import SchemaCreator from litestar.openapi.spec import OpenAPIType from litestar.openapi.spec.schema import Schema from litestar.plugins.pydantic import PydanticSchemaPlugin from litestar.typing import FieldDefinition from litestar.utils.helpers import get_name from tests.helpers import get_schema_for_field_definition T = TypeVar("T") class PydanticV1Generic(GenericModel, Generic[T]): foo: T optional_foo: Optional[T] annotated_foo: Annotated[T, object()] class PydanticV2Generic(pydantic_v2.BaseModel, Generic[T]): foo: T optional_foo: Optional[T] annotated_foo: Annotated[T, object()] @pytest.mark.parametrize("model", [PydanticV1Generic, PydanticV2Generic]) def test_schema_generation_with_generic_classes(model: Type[Union[PydanticV1Generic, PydanticV2Generic]]) -> None: cls = model[int] # type: ignore[index] field_definition = FieldDefinition.from_kwarg(name=get_name(cls), annotation=cls) properties = get_schema_for_field_definition(field_definition, plugins=[PydanticSchemaPlugin()]).properties expected_foo_schema = Schema(type=OpenAPIType.INTEGER) expected_optional_foo_schema = Schema(one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.NULL)]) assert properties assert properties["foo"] == expected_foo_schema assert properties["annotated_foo"] == expected_foo_schema assert properties["optional_foo"] == expected_optional_foo_schema @pytest.mark.parametrize( "constrained", [ pydantic_v1.constr(regex="^[a-zA-Z]$"), pydantic_v1.conlist(int, min_items=1), pydantic_v1.conset(int, min_items=1), pydantic_v1.conint(gt=10, lt=100), pydantic_v1.confloat(gt=10, lt=100), pydantic_v1.condecimal(gt=Decimal("10")), pydantic_v1.condate(gt=datetime.date.today()), pydantic_v2.constr(pattern="^[a-zA-Z]$"), pydantic_v2.conlist(int, min_length=1), pydantic_v2.conset(int, min_length=1), pydantic_v2.conint(gt=10, lt=100), pydantic_v2.confloat(ge=10, le=100), pydantic_v2.condecimal(gt=Decimal("10")), pydantic_v2.condate(gt=datetime.date.today()), ], ) def test_is_pydantic_constrained_field(constrained: Any) -> None: PydanticSchemaPlugin.is_constrained_field(FieldDefinition.from_annotation(constrained)) def test_v2_constrained_secrets() -> None: # https://github.com/litestar-org/litestar/issues/3148 class Model(pydantic_v2.BaseModel): string: pydantic_v2.SecretStr = pydantic_v2.Field(min_length=1) bytes_: pydantic_v2.SecretBytes = pydantic_v2.Field(min_length=1) schema = PydanticSchemaPlugin.for_pydantic_model( FieldDefinition.from_annotation(Model), schema_creator=SchemaCreator(plugins=[PydanticSchemaPlugin()]) ) assert schema.properties assert schema.properties["string"] == Schema(min_length=1, type=OpenAPIType.STRING) assert schema.properties["bytes_"] == Schema(min_length=1, type=OpenAPIType.STRING) class V1ModelWithPrivateFields(pydantic_v1.BaseModel): class Config: underscore_fields_are_private = True _field: str = pydantic_v1.PrivateAttr() # include an invalid annotation here to ensure we never touch those fields _underscore_field: str = "foo" class V1GenericModelWithPrivateFields(pydantic_v1.generics.GenericModel, Generic[T]): # pyright: ignore class Config: underscore_fields_are_private = True _field: str = pydantic_v1.PrivateAttr() # include an invalid annotation here to ensure we never touch those fields _underscore_field: str = "foo" class V2ModelWithPrivateFields(pydantic_v2.BaseModel): _field: str = pydantic_v2.PrivateAttr() # include an invalid annotation here to ensure we never touch those fields _underscore_field: str = "foo" class V2GenericModelWithPrivateFields(pydantic_v2.BaseModel, Generic[T]): _field: str = pydantic_v2.PrivateAttr() # include an invalid annotation here to ensure we never touch those fields _underscore_field: str = "foo" @pytest.mark.parametrize( "model_class", [ V1ModelWithPrivateFields, V1GenericModelWithPrivateFields, V2ModelWithPrivateFields, V2GenericModelWithPrivateFields, ], ) def test_exclude_private_fields(model_class: Type[Union[pydantic_v1.BaseModel, pydantic_v2.BaseModel]]) -> None: # https://github.com/litestar-org/litestar/issues/3150 schema = PydanticSchemaPlugin.for_pydantic_model( FieldDefinition.from_annotation(model_class), schema_creator=SchemaCreator(plugins=[PydanticSchemaPlugin()]) ) assert not schema.properties def test_v1_constrained_str_with_default_factory_does_not_generate_title() -> None: # https://github.com/litestar-org/litestar/issues/3710 class Model(pydantic_v1.BaseModel): test_str: str = pydantic_v1.Field(default_factory=str, max_length=600) @post(path="/") async def test(data: Model) -> str: return "success" schema = Litestar(route_handlers=[test]).openapi_schema.to_schema() assert ( "title" not in schema["components"]["schemas"][ "test_v1_constrained_str_with_default_factory_does_not_generate_title.Model" ]["properties"]["test_str"]["oneOf"][1] ) litestar-2.16.0/tests/unit/test_plugins/test_pydantic/test_utils.py000066400000000000000000000016131500564371300257200ustar00rootroot00000000000000from typing import Any, Dict, Generic, Tuple, TypeVar import pytest from pydantic import BaseModel from litestar.plugins.pydantic.utils import pydantic_get_type_hints_with_generics_resolved T = TypeVar("T") class GenericPydanticModel(BaseModel, Generic[T]): foo: T @pytest.mark.parametrize( ("annotation", "expected_type_hints"), ( (GenericPydanticModel, {"foo": T}), (GenericPydanticModel[int], {"foo": int}), ), ) def test_get_pydantic_type_hints_with_generics_resolved(annotation: Any, expected_type_hints: Dict[str, Any]) -> None: type_hints = pydantic_get_type_hints_with_generics_resolved(annotation) # In Python 3.12 and Pydantic V1, `__slots__` is returned in `get_type_hints`. slots_type = type_hints.pop("__slots__", None) if slots_type is not None: assert slots_type == Tuple[str, ...] assert type_hints == expected_type_hints litestar-2.16.0/tests/unit/test_plugins/test_sqlalchemy.py000066400000000000000000000054111500564371300240500ustar00rootroot00000000000000import pytest from advanced_alchemy.extensions import litestar as sa_litestar from advanced_alchemy.extensions.litestar import base as sa_base from advanced_alchemy.extensions.litestar import exceptions as sa_exceptions from advanced_alchemy.extensions.litestar import filters as sa_filters from advanced_alchemy.extensions.litestar import mixins as sa_mixins from advanced_alchemy.extensions.litestar import repository as sa_repository from advanced_alchemy.extensions.litestar import service as sa_service from advanced_alchemy.extensions.litestar import types as sa_types from advanced_alchemy.extensions.litestar import utils as sa_utils from litestar.pagination import OffsetPagination from litestar.plugins import sqlalchemy def test_re_exports() -> None: assert sqlalchemy.base is sa_base assert sqlalchemy.filters is sa_filters assert sqlalchemy.types is sa_types assert sqlalchemy.mixins is sa_mixins assert sqlalchemy.utils is sa_utils assert sqlalchemy.repository is sa_repository assert sqlalchemy.service is sa_service assert sqlalchemy.exceptions is sa_exceptions assert OffsetPagination is sa_service.OffsetPagination assert sqlalchemy.AlembicAsyncConfig is sa_litestar.AlembicAsyncConfig assert sqlalchemy.AlembicCommands is sa_litestar.AlembicCommands assert sqlalchemy.AlembicSyncConfig is sa_litestar.AlembicSyncConfig assert sqlalchemy.AsyncSessionConfig is sa_litestar.AsyncSessionConfig assert sqlalchemy.EngineConfig is sa_litestar.EngineConfig assert sqlalchemy.SQLAlchemyAsyncConfig is sa_litestar.SQLAlchemyAsyncConfig assert sqlalchemy.SQLAlchemyDTO is sa_litestar.SQLAlchemyDTO assert sqlalchemy.SQLAlchemyDTOConfig is sa_litestar.SQLAlchemyDTOConfig assert sqlalchemy.SQLAlchemyInitPlugin is sa_litestar.SQLAlchemyInitPlugin assert sqlalchemy.SQLAlchemyPlugin is sa_litestar.SQLAlchemyPlugin assert sqlalchemy.SQLAlchemySerializationPlugin is sa_litestar.SQLAlchemySerializationPlugin assert sqlalchemy.SQLAlchemySyncConfig is sa_litestar.SQLAlchemySyncConfig assert sqlalchemy.SyncSessionConfig is sa_litestar.SyncSessionConfig # deprecated, to be removed later with pytest.warns(DeprecationWarning): assert sqlalchemy.AuditColumns is sa_base.AuditColumns assert sqlalchemy.BigIntAuditBase is sa_base.BigIntAuditBase assert sqlalchemy.BigIntBase is sa_base.BigIntBase assert sqlalchemy.BigIntPrimaryKey is sa_base.BigIntPrimaryKey assert sqlalchemy.CommonTableAttributes is sa_base.CommonTableAttributes assert sqlalchemy.UUIDAuditBase is sa_base.UUIDAuditBase assert sqlalchemy.UUIDBase is sa_base.UUIDBase assert sqlalchemy.UUIDPrimaryKey is sa_base.UUIDPrimaryKey assert sqlalchemy.orm_registry is sa_base.orm_registry litestar-2.16.0/tests/unit/test_repository/000077500000000000000000000000001500564371300210325ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_repository/__init__.py000066400000000000000000000000001500564371300231310ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_repository/models_bigint.py000066400000000000000000000112071500564371300242240ustar00rootroot00000000000000"""Example domain objects for testing.""" from __future__ import annotations from datetime import date, datetime from typing import List from advanced_alchemy.base import BigIntAuditBase, BigIntBase from advanced_alchemy.repository import SQLAlchemyAsyncRepository, SQLAlchemySyncRepository from sqlalchemy import Column, FetchedValue, ForeignKey, String, Table, func from sqlalchemy.orm import Mapped, mapped_column, relationship class BigIntAuthor(BigIntAuditBase): """The Author domain object.""" name: Mapped[str] = mapped_column(String(length=100)) # pyright: ignore dob: Mapped[date] = mapped_column(nullable=True) # pyright: ignore books: Mapped[List[BigIntBook]] = relationship( # noqa lazy="selectin", back_populates="author", cascade="all, delete", ) class BigIntBook(BigIntBase): """The Book domain object.""" title: Mapped[str] = mapped_column(String(length=250)) # pyright: ignore author_id: Mapped[int] = mapped_column(ForeignKey("big_int_author.id")) # pyright: ignore author: Mapped[BigIntAuthor] = relationship( # pyright: ignore lazy="joined", innerjoin=True, back_populates="books", ) class BigIntEventLog(BigIntAuditBase): """The event log domain object.""" logged_at: Mapped[datetime] = mapped_column(default=datetime.now()) # pyright: ignore payload: Mapped[dict] = mapped_column(default=lambda: {}) # pyright: ignore class BigIntModelWithFetchedValue(BigIntBase): """The ModelWithFetchedValue BigIntBase.""" val: Mapped[int] # pyright: ignore updated: Mapped[datetime] = mapped_column( # pyright: ignore server_default=func.current_timestamp(), onupdate=func.current_timestamp(), server_onupdate=FetchedValue(), ) bigint_item_tag = Table( "bigint_item_tag", BigIntBase.metadata, Column("item_id", ForeignKey("big_int_item.id"), primary_key=True), Column("tag_id", ForeignKey("big_int_tag.id"), primary_key=True), ) class BigIntItem(BigIntBase): name: Mapped[str] = mapped_column(String(length=50)) # pyright: ignore description: Mapped[str] = mapped_column(String(length=100), nullable=True) # pyright: ignore tags: Mapped[List[BigIntTag]] = relationship(secondary=lambda: bigint_item_tag, back_populates="items") # noqa class BigIntTag(BigIntBase): """The event log domain object.""" name: Mapped[str] = mapped_column(String(length=50)) # pyright: ignore items: Mapped[List[BigIntItem]] = relationship(secondary=lambda: bigint_item_tag, back_populates="tags") # noqa class BigIntRule(BigIntAuditBase): """The rule domain object.""" name: Mapped[str] = mapped_column(String(length=250)) # pyright: ignore config: Mapped[dict] = mapped_column(default=lambda: {}) # pyright: ignore class RuleAsyncRepository(SQLAlchemyAsyncRepository[BigIntRule]): """Rule repository.""" model_type = BigIntRule class AuthorAsyncRepository(SQLAlchemyAsyncRepository[BigIntAuthor]): """Author repository.""" model_type = BigIntAuthor class BookAsyncRepository(SQLAlchemyAsyncRepository[BigIntBook]): """Book repository.""" model_type = BigIntBook class EventLogAsyncRepository(SQLAlchemyAsyncRepository[BigIntEventLog]): """Event log repository.""" model_type = BigIntEventLog class ModelWithFetchedValueAsyncRepository(SQLAlchemyAsyncRepository[BigIntModelWithFetchedValue]): """BigIntModelWithFetchedValue repository.""" model_type = BigIntModelWithFetchedValue class TagAsyncRepository(SQLAlchemyAsyncRepository[BigIntTag]): """Tag repository.""" model_type = BigIntTag class ItemAsyncRepository(SQLAlchemyAsyncRepository[BigIntItem]): """Item repository.""" model_type = BigIntItem class AuthorSyncRepository(SQLAlchemySyncRepository[BigIntAuthor]): """Author repository.""" model_type = BigIntAuthor class BookSyncRepository(SQLAlchemySyncRepository[BigIntBook]): """Book repository.""" model_type = BigIntBook class EventLogSyncRepository(SQLAlchemySyncRepository[BigIntEventLog]): """Event log repository.""" model_type = BigIntEventLog class RuleSyncRepository(SQLAlchemySyncRepository[BigIntRule]): """Rule repository.""" model_type = BigIntRule class ModelWithFetchedValueSyncRepository(SQLAlchemySyncRepository[BigIntModelWithFetchedValue]): """ModelWithFetchedValue repository.""" model_type = BigIntModelWithFetchedValue class TagSyncRepository(SQLAlchemySyncRepository[BigIntTag]): """Tag repository.""" model_type = BigIntTag class ItemSyncRepository(SQLAlchemySyncRepository[BigIntItem]): """Item repository.""" model_type = BigIntItem litestar-2.16.0/tests/unit/test_repository/models_uuid.py000066400000000000000000000110311500564371300237110ustar00rootroot00000000000000"""Example domain objects for testing.""" from __future__ import annotations from datetime import date, datetime from typing import List from uuid import UUID from advanced_alchemy import base from advanced_alchemy.repository import SQLAlchemyAsyncRepository, SQLAlchemySyncRepository from sqlalchemy import Column, FetchedValue, ForeignKey, String, Table, func from sqlalchemy.orm import Mapped, mapped_column, relationship class UUIDAuthor(base.UUIDAuditBase): """The UUIDAuthor domain object.""" name: Mapped[str] = mapped_column(String(length=100)) # pyright: ignore dob: Mapped[date] = mapped_column(nullable=True) # pyright: ignore books: Mapped[List[UUIDBook]] = relationship( # noqa lazy="selectin", back_populates="author", cascade="all, delete", ) class UUIDBook(base.UUIDBase): """The Book domain object.""" title: Mapped[str] = mapped_column(String(length=250)) # pyright: ignore author_id: Mapped[UUID] = mapped_column(ForeignKey("uuid_author.id")) # pyright: ignore author: Mapped[UUIDAuthor] = relationship(lazy="joined", innerjoin=True, back_populates="books") # pyright: ignore class UUIDEventLog(base.UUIDAuditBase): """The event log domain object.""" logged_at: Mapped[datetime] = mapped_column(default=datetime.now()) # pyright: ignore payload: Mapped[dict] = mapped_column(default={}) # pyright: ignore class UUIDModelWithFetchedValue(base.UUIDBase): """The ModelWithFetchedValue UUIDBase.""" val: Mapped[int] # pyright: ignore updated: Mapped[datetime] = mapped_column( # pyright: ignore server_default=func.current_timestamp(), onupdate=func.current_timestamp(), server_onupdate=FetchedValue(), ) uuid_item_tag = Table( "uuid_item_tag", base.orm_registry.metadata, Column("item_id", ForeignKey("uuid_item.id"), primary_key=True), Column("tag_id", ForeignKey("uuid_tag.id"), primary_key=True), ) class UUIDItem(base.UUIDBase): name: Mapped[str] = mapped_column(String(length=50)) # pyright: ignore description: Mapped[str] = mapped_column(String(length=100), nullable=True) # pyright: ignore tags: Mapped[List[UUIDTag]] = relationship(secondary=lambda: uuid_item_tag, back_populates="items") # noqa class UUIDTag(base.UUIDAuditBase): """The event log domain object.""" name: Mapped[str] = mapped_column(String(length=50)) # pyright: ignore items: Mapped[List[UUIDItem]] = relationship(secondary=lambda: uuid_item_tag, back_populates="tags") # noqa class UUIDRule(base.UUIDAuditBase): """The rule domain object.""" name: Mapped[str] = mapped_column(String(length=250)) # pyright: ignore config: Mapped[dict] = mapped_column(default=lambda: {}) # pyright: ignore class RuleAsyncRepository(SQLAlchemyAsyncRepository[UUIDRule]): """Rule repository.""" model_type = UUIDRule class AuthorAsyncRepository(SQLAlchemyAsyncRepository[UUIDAuthor]): """Author repository.""" model_type = UUIDAuthor class BookAsyncRepository(SQLAlchemyAsyncRepository[UUIDBook]): """Book repository.""" model_type = UUIDBook class EventLogAsyncRepository(SQLAlchemyAsyncRepository[UUIDEventLog]): """Event log repository.""" model_type = UUIDEventLog class ModelWithFetchedValueAsyncRepository(SQLAlchemyAsyncRepository[UUIDModelWithFetchedValue]): """ModelWithFetchedValue repository.""" model_type = UUIDModelWithFetchedValue class TagAsyncRepository(SQLAlchemyAsyncRepository[UUIDTag]): """Tag repository.""" model_type = UUIDTag class ItemAsyncRepository(SQLAlchemyAsyncRepository[UUIDItem]): """Item repository.""" model_type = UUIDItem class AuthorSyncRepository(SQLAlchemySyncRepository[UUIDAuthor]): """Author repository.""" model_type = UUIDAuthor class BookSyncRepository(SQLAlchemySyncRepository[UUIDBook]): """Book repository.""" model_type = UUIDBook class EventLogSyncRepository(SQLAlchemySyncRepository[UUIDEventLog]): """Event log repository.""" model_type = UUIDEventLog class RuleSyncRepository(SQLAlchemySyncRepository[UUIDRule]): """Rule repository.""" model_type = UUIDRule class ModelWithFetchedValueSyncRepository(SQLAlchemySyncRepository[UUIDModelWithFetchedValue]): """ModelWithFetchedValue repository.""" model_type = UUIDModelWithFetchedValue class TagSyncRepository(SQLAlchemySyncRepository[UUIDTag]): """Tag repository.""" model_type = UUIDTag class ItemSyncRepository(SQLAlchemySyncRepository[UUIDItem]): """Item repository.""" model_type = UUIDItem litestar-2.16.0/tests/unit/test_repository/test_generic_mock_repository.py000066400000000000000000000447641500564371300274060ustar00rootroot00000000000000from __future__ import annotations from datetime import date, datetime from typing import Protocol, Type, Union, cast from uuid import uuid4 import pytest from _pytest.fixtures import FixtureRequest from sqlalchemy import String from sqlalchemy.orm import Mapped, mapped_column from litestar.contrib.sqlalchemy import base from litestar.repository.exceptions import ConflictError, RepositoryError from litestar.repository.filters import LimitOffset from litestar.repository.testing.generic_mock_repository import ( GenericAsyncMockRepository, GenericSyncMockRepository, ) from tests.helpers import maybe_async from tests.unit.test_repository.models_uuid import UUIDAuthor, UUIDBook AuthorRepository = GenericAsyncMockRepository[UUIDAuthor] AuthorRepositoryType = Type[AuthorRepository] ModelType = Type[Union[base.UUIDBase, base.BigIntBase]] AuditModelType = Type[Union[base.UUIDAuditBase, base.BigIntAuditBase]] class CreateAuditModelFixture(Protocol): def __call__(self, extra_columns: dict[str, type[Mapped] | Mapped] | None = None) -> AuditModelType: ... @pytest.fixture(name="authors") def fx_authors() -> list[UUIDAuthor]: """Collection of Author models.""" return [ UUIDAuthor(id=uuid4(), name=name, dob=dob, created_at=datetime.min, updated_at=datetime.min) for name, dob in [("Agatha Christie", date(1890, 9, 15)), ("Leo Tolstoy", date(1828, 9, 9))] ] @pytest.fixture(params=[GenericAsyncMockRepository, GenericSyncMockRepository], ids=["async", "sync"]) def repository_type(request: FixtureRequest) -> type[GenericAsyncMockRepository]: return cast("type[GenericAsyncMockRepository]", request.param) @pytest.fixture(name="author_repository_type", params=[GenericAsyncMockRepository, GenericSyncMockRepository]) def fx_author_repository_type( authors: list[UUIDAuthor], monkeypatch: pytest.MonkeyPatch, repository_type: type[GenericAsyncMockRepository] ) -> AuthorRepositoryType: """Mock Author repository, pre-seeded with collection data.""" repo = repository_type[UUIDAuthor] # type: ignore[index] repo.seed_collection(authors) return cast("type[GenericAsyncMockRepository]", repo) @pytest.fixture(name="author_repository") def fx_author_repository( author_repository_type: type[GenericAsyncMockRepository[UUIDAuthor]], ) -> GenericAsyncMockRepository[UUIDAuthor]: """Mock Author repository instance.""" return author_repository_type() @pytest.fixture(params=[base.UUIDBase, base.BigIntBase]) def model_type(request: FixtureRequest) -> ModelType: return cast(ModelType, type(f"{request.node.nodeid}Model", (request.param,), {})) @pytest.fixture(params=[base.UUIDAuditBase, base.BigIntAuditBase]) def create_audit_model_type(request: FixtureRequest) -> CreateAuditModelFixture: def create(extra_columns: dict[str, type[Mapped] | Mapped] | None = None) -> AuditModelType: return cast(AuditModelType, type(f"{request.node.nodeid}AuditModel", (request.param,), extra_columns or {})) return create @pytest.fixture() def audit_model_type(create_audit_model_type: CreateAuditModelFixture) -> AuditModelType: return create_audit_model_type() async def test_repo_raises_conflict_if_add_with_id( authors: list[UUIDAuthor], author_repository: AuthorRepository ) -> None: """Test mock repo raises conflict if add identified entity.""" with pytest.raises(ConflictError): await maybe_async(author_repository.add(authors[0])) async def test_repo_raises_conflict_if_add_many_with_id( authors: list[UUIDAuthor], author_repository: AuthorRepository ) -> None: """Test mock repo raises conflict if add identified entity.""" with pytest.raises(ConflictError): await maybe_async(author_repository.add_many(authors)) def test_generic_mock_repository_parametrization(repository_type: type[GenericAsyncMockRepository]) -> None: """Test that the mock repository handles multiple types.""" author_repo = repository_type[UUIDAuthor] # type: ignore[index] book_repo = repository_type[UUIDBook] # type: ignore[index] assert author_repo.model_type is UUIDAuthor assert book_repo.model_type is UUIDBook def test_generic_mock_repository_seed_collection(author_repository_type: AuthorRepositoryType) -> None: """Test seeding instances.""" author_repository_type.seed_collection([UUIDAuthor(id="abc")]) assert "abc" in author_repository_type.collection def test_generic_mock_repository_clear_collection(author_repository_type: AuthorRepositoryType) -> None: """Test clearing collection for type.""" author_repository_type.clear_collection() assert not author_repository_type.collection def test_generic_mock_repository_filter_collection_by_kwargs(author_repository: AuthorRepository) -> None: """Test filtering the repository collection by kwargs.""" collection = author_repository.filter_collection_by_kwargs(author_repository.collection, name="Leo Tolstoy") assert len(collection) == 1 assert next(iter(collection.values())).name == "Leo Tolstoy" def test_generic_mock_repository_filter_collection_by_kwargs_and_semantics(author_repository: AuthorRepository) -> None: """Test that filtering by kwargs has `AND` semantics when multiple kwargs, not `OR`.""" collection = author_repository.filter_collection_by_kwargs( author_repository.collection, name="Agatha Christie", dob="1828-09-09" ) assert len(collection) == 0 def test_generic_mock_repository_raises_repository_exception_if_named_attribute_doesnt_exist( author_repository: AuthorRepository, ) -> None: """Test that a repo exception is raised if a named attribute doesn't exist.""" with pytest.raises(RepositoryError): _ = author_repository.filter_collection_by_kwargs(author_repository.collection, cricket="ball") async def test_sets_created_updated_on_add( repository_type: type[GenericAsyncMockRepository], audit_model_type: AuditModelType ) -> None: """Test that the repository updates the 'created_at' and 'updated_at' timestamps if necessary.""" instance = audit_model_type() assert "created_at" not in vars(instance) assert "updated_at" not in vars(instance) instance = await maybe_async(repository_type[audit_model_type]().add(instance)) # type: ignore[index] assert "created_at" in vars(instance) assert "updated_at" in vars(instance) async def test_sets_updated_on_update(author_repository: AuthorRepository) -> None: """Test that the repository updates the 'updated' timestamp if necessary.""" instance = next(iter(author_repository.collection.values())) original_updated = instance.updated_at instance = await maybe_async(author_repository.update(instance)) assert instance.updated_at > original_updated async def test_does_not_set_created_updated( repository_type: type[GenericAsyncMockRepository], model_type: ModelType ) -> None: """Test that the repository does not update the 'updated' timestamps when appropriate.""" instance = model_type() repo = repository_type[model_type]() # type: ignore[index] assert "created_at" not in vars(instance) assert "updated_at" not in vars(instance) instance = await maybe_async(repo.add(instance)) assert "created_at" not in vars(instance) assert "updated_at" not in vars(instance) instance = await maybe_async(repo.update(instance)) assert "created_at" not in vars(instance) assert "updated_at" not in vars(instance) async def test_add(repository_type: type[GenericAsyncMockRepository], model_type: ModelType) -> None: """Test that the repository add method works correctly.""" instance = model_type() inserted_uuid_instance = await maybe_async(repository_type[model_type]().add(instance)) # type: ignore[index] assert inserted_uuid_instance == instance async def test_add_many(repository_type: type[GenericAsyncMockRepository], model_type: ModelType) -> None: """Test that the repository add_many method works correctly.""" instances = [model_type(), model_type()] inserted_uuid_instances = await maybe_async(repository_type[model_type]().add_many(instances)) # type: ignore[index] assert len(instances) == len(inserted_uuid_instances) async def test_update( repository_type: type[GenericAsyncMockRepository], create_audit_model_type: CreateAuditModelFixture ) -> None: """Test that the repository update method works correctly.""" Model = create_audit_model_type({"random_column": Mapped[str]}) mock_repo = repository_type[Model]() # type: ignore[index] instance = await maybe_async(mock_repo.add(Model(random_column="A"))) instance.random_column = "B" updated_instance = await maybe_async(mock_repo.update(instance)) assert updated_instance == instance async def test_update_many( repository_type: type[GenericAsyncMockRepository], create_audit_model_type: CreateAuditModelFixture ) -> None: """Test that the repository add_many method works correctly.""" Model = create_audit_model_type({"random_column": Mapped[str]}) mock_repo = repository_type[Model]() # type: ignore[index] instances = [Model(random_column="A"), Model(random_column="B")] inserted_instances = await maybe_async(mock_repo.add_many(instances)) for instance in inserted_instances: instance.random_column = "C" updated_instances = await maybe_async(mock_repo.update_many(instances)) for instance in updated_instances: assert instance.random_column == "C" assert len(instances) == len(updated_instances) async def test_upsert( repository_type: type[GenericAsyncMockRepository], create_audit_model_type: CreateAuditModelFixture ) -> None: """Test that the repository upsert method works correctly.""" Model = create_audit_model_type({"random_column": Mapped[str]}) mock_repo = repository_type[Model]() # type: ignore[index] instance = await maybe_async(mock_repo.upsert(Model(random_column="A"))) instance.random_column = "B" updated_instance = await maybe_async(mock_repo.upsert(instance)) assert updated_instance == instance async def test_upsert_many( repository_type: type[GenericAsyncMockRepository], create_audit_model_type: CreateAuditModelFixture ) -> None: """Test that the repository upsert method works correctly.""" Model = create_audit_model_type({"random_column": Mapped[str]}) mock_repo = repository_type[Model]() # type: ignore[index] instance = await maybe_async(mock_repo.upsert(Model(random_column="A"))) instance.random_column = "B" new_instance = Model(random_column="C") updated_instances = await maybe_async(mock_repo.upsert_many([instance, new_instance])) assert new_instance in updated_instances assert instance in updated_instances async def test_list(repository_type: type[GenericAsyncMockRepository], audit_model_type: AuditModelType) -> None: """Test that the repository list returns records.""" mock_repo = repository_type[audit_model_type]() # type: ignore[index] inserted_instances = await maybe_async(mock_repo.add_many([audit_model_type(), audit_model_type()])) listed_instances = await maybe_async(mock_repo.list()) assert inserted_instances == listed_instances async def test_delete(repository_type: type[GenericAsyncMockRepository], audit_model_type: AuditModelType) -> None: """Test that the repository delete functionality.""" mock_repo = repository_type[audit_model_type]() # type: ignore[index] inserted_instances = await maybe_async(mock_repo.add_many([audit_model_type(), audit_model_type()])) delete_instance = await maybe_async(mock_repo.delete(inserted_instances[0].id)) assert delete_instance.id == inserted_instances[0].id count = await maybe_async(mock_repo.count()) assert count == 1 async def test_delete_many(repository_type: type[GenericAsyncMockRepository], audit_model_type: AuditModelType) -> None: """Test that the repository delete many functionality.""" mock_repo = repository_type[audit_model_type]() # type: ignore[index] inserted_instances = await maybe_async(mock_repo.add_many([audit_model_type(), audit_model_type()])) delete_instances = await maybe_async(mock_repo.delete_many([obj.id for obj in inserted_instances])) assert len(delete_instances) == 2 count = await maybe_async(mock_repo.count()) assert count == 0 async def test_list_and_count( repository_type: type[GenericAsyncMockRepository], audit_model_type: AuditModelType ) -> None: """Test that the repository list_and_count returns records and the total record count.""" instances = [audit_model_type(), audit_model_type()] mock_repo = repository_type[audit_model_type]() # type: ignore[index] inserted_instances = await maybe_async(mock_repo.add_many(instances)) listed_instances, count = await maybe_async(mock_repo.list_and_count()) assert inserted_instances == listed_instances assert count == len(instances) async def test_exists( repository_type: type[GenericAsyncMockRepository], create_audit_model_type: CreateAuditModelFixture ) -> None: """Test that the repository exists returns booleans.""" Model = create_audit_model_type({"random_column": Mapped[str]}) instances = [Model(random_column="value 1"), Model(random_column="value 2")] mock_repo = repository_type[Model]() # type: ignore[index] _ = await maybe_async(mock_repo.add_many(instances)) exists = await maybe_async(mock_repo.exists(random_column="value 1")) assert exists async def test_exists_with_filter( repository_type: type[GenericAsyncMockRepository], create_audit_model_type: CreateAuditModelFixture ) -> None: """Test that the repository exists returns booleans. with filter argument""" limit_filter = LimitOffset(limit=1, offset=0) Model = create_audit_model_type({"random_column": Mapped[str]}) instances = [Model(random_column="value 1"), Model(random_column="value 2")] mock_repo = repository_type[Model]() # type: ignore[index] _ = await maybe_async(mock_repo.add_many(instances)) exists = await maybe_async(mock_repo.exists(limit_filter, random_column="value 1")) assert exists async def test_count(repository_type: type[GenericAsyncMockRepository], audit_model_type: AuditModelType) -> None: """Test that the repository count returns the total record count.""" instances = [audit_model_type(), audit_model_type()] mock_repo = repository_type[audit_model_type]() # type: ignore[index] _ = await maybe_async(mock_repo.add_many(instances)) count = await maybe_async(mock_repo.count()) assert count == len(instances) async def test_get( repository_type: type[GenericAsyncMockRepository], create_audit_model_type: CreateAuditModelFixture ) -> None: """Test that the repository get returns a model record correctly.""" Model = create_audit_model_type({"random_column": Mapped[str]}) instances = [Model(random_column="value 1"), Model(random_column="value 2")] mock_repo = repository_type[Model]() # type: ignore[index] inserted_instances = await maybe_async(mock_repo.add_many(instances)) item_id = inserted_instances[0].id fetched_instance = await maybe_async(mock_repo.get(item_id)) assert inserted_instances[0] == fetched_instance async def test_get_one( repository_type: type[GenericAsyncMockRepository], create_audit_model_type: CreateAuditModelFixture ) -> None: """Test that the repository get_one returns a model record correctly.""" Model = create_audit_model_type({"random_column": Mapped[str]}) instances = [Model(random_column="value 1"), Model(random_column="value 2")] mock_repo = repository_type[Model]() # type: ignore[index] inserted_instances = await maybe_async(mock_repo.add_many(instances)) fetched_instance = await maybe_async(mock_repo.get_one(random_column="value 1")) assert inserted_instances[0] == fetched_instance with pytest.raises(RepositoryError): _ = await maybe_async(mock_repo.get_one(random_column="value 3")) async def test_get_one_or_none( repository_type: type[GenericAsyncMockRepository], create_audit_model_type: CreateAuditModelFixture ) -> None: """Test that the repository get_one_or_none returns a model record correctly.""" Model = create_audit_model_type({"random_column": Mapped[str]}) instances = [Model(random_column="value 1"), Model(random_column="value 2")] mock_repo = repository_type[Model]() # type: ignore[index] inserted_instances = await maybe_async(mock_repo.add_many(instances)) fetched_instance = await maybe_async(mock_repo.get_one_or_none(random_column="value 1")) assert inserted_instances[0] == fetched_instance none_instance = await maybe_async(mock_repo.get_one_or_none(random_column="value 3")) assert none_instance is None async def test_get_or_create( repository_type: type[GenericAsyncMockRepository], create_audit_model_type: CreateAuditModelFixture ) -> None: """Test that the repository get_or_create returns a model record correctly.""" Model = create_audit_model_type( {"random_column": Mapped[str], "cool_attribute": mapped_column(String, nullable=True)} ) instances = [Model(random_column="value 1", cool_attribute="yep"), Model(random_column="value 2")] mock_repo = repository_type[Model]() # type: ignore[index] inserted_instances = await maybe_async(mock_repo.add_many(instances)) fetched_instance, fetched_created = await maybe_async(mock_repo.get_or_create(random_column="value 2")) assert await maybe_async(mock_repo.count()) == 2 assert inserted_instances[1] == fetched_instance assert fetched_created is False _, created = await maybe_async(mock_repo.get_or_create(random_column="value 3")) assert await maybe_async(mock_repo.count()) == 3 assert created async def test_get_or_create_match_fields( repository_type: type[GenericAsyncMockRepository], create_audit_model_type: CreateAuditModelFixture ) -> None: """Test that the repository get_or_create returns a model record correctly.""" Model = create_audit_model_type( {"random_column": Mapped[str], "cool_attribute": mapped_column(String, nullable=True)} ) instances = [Model(random_column="value 1", cool_attribute="yep"), Model(random_column="value 2")] mock_repo = repository_type[Model]() # type: ignore[index] inserted_instances = await maybe_async(mock_repo.add_many(instances)) fetched_instance, fetched_created = await maybe_async( mock_repo.get_or_create(match_fields=["random_column"], random_column="value 1", cool_attribute="other thing") ) assert await maybe_async(mock_repo.count()) == 2 assert inserted_instances[0] == fetched_instance assert fetched_created is False litestar-2.16.0/tests/unit/test_request_class_resolution.py000066400000000000000000000047351500564371300243360ustar00rootroot00000000000000from typing import Optional, Type import pytest from litestar import Controller, HttpMethod, Litestar, Request, Router, get from litestar.handlers.http_handlers.base import HTTPRouteHandler RouterRequest: Type[Request] = type("RouterRequest", (Request,), {}) ControllerRequest: Type[Request] = type("ControllerRequest", (Request,), {}) AppRequest: Type[Request] = type("AppRequest", (Request,), {}) HandlerRequest: Type[Request] = type("HandlerRequest", (Request,), {}) @pytest.mark.parametrize( "handler_request_class, controller_request_class, router_request_class, app_request_class, has_default_app_class, expected", ( (HandlerRequest, ControllerRequest, RouterRequest, AppRequest, True, HandlerRequest), (None, ControllerRequest, RouterRequest, AppRequest, True, ControllerRequest), (None, None, RouterRequest, AppRequest, True, RouterRequest), (None, None, None, AppRequest, True, AppRequest), (None, None, None, None, True, Request), (None, None, None, None, False, Request), ), ids=( "Custom class for all layers", "Custom class for all above handler layer", "Custom class for all above controller layer", "Custom class for all above router layer", "No custom class for layers", "No default class in app", ), ) def test_request_class_resolution_of_layers( handler_request_class: Optional[Type[Request]], controller_request_class: Optional[Type[Request]], router_request_class: Optional[Type[Request]], app_request_class: Optional[Type[Request]], has_default_app_class: bool, expected: Type[Request], ) -> None: class MyController(Controller): @get() def handler(self, request: Request) -> None: assert type(request) is expected if controller_request_class: MyController.request_class = ControllerRequest router = Router(path="/", route_handlers=[MyController]) if router_request_class: router.request_class = router_request_class app = Litestar(route_handlers=[router]) if app_request_class or not has_default_app_class: app.request_class = app_request_class # type: ignore[assignment] route_handler: HTTPRouteHandler = app.route_handler_method_map["/"][HttpMethod.GET] # type: ignore[assignment] if handler_request_class: route_handler.request_class = handler_request_class request_class = route_handler.resolve_request_class() assert request_class is expected litestar-2.16.0/tests/unit/test_response/000077500000000000000000000000001500564371300204515ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_response/__init__.py000066400000000000000000000000001500564371300225500ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_response/test_base_response.py000066400000000000000000000171611500564371300247200ustar00rootroot00000000000000from pathlib import PurePosixPath from typing import Any, Optional import pytest from litestar import MediaType, get from litestar.datastructures import Cookie from litestar.exceptions import ImproperlyConfiguredException from litestar.response import Response from litestar.response.base import ASGIResponse from litestar.serialization import default_serializer, get_serializer from litestar.status_codes import ( HTTP_100_CONTINUE, HTTP_101_SWITCHING_PROTOCOLS, HTTP_102_PROCESSING, HTTP_103_EARLY_HINTS, HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_304_NOT_MODIFIED, HTTP_500_INTERNAL_SERVER_ERROR, ) from litestar.testing import create_test_client from litestar.types import Empty def test_response_headers() -> None: @get("/") def handler() -> Response: return Response(content="hello world", media_type=MediaType.TEXT, headers={"first": "123", "second": "456"}) with create_test_client(handler) as client: response = client.get("/") assert response.headers["first"] == "123" assert response.headers["second"] == "456" assert response.headers["content-length"] == "11" assert response.headers["content-type"] == "text/plain; charset=utf-8" def test_response_headers_do_not_lowercase_values() -> None: # reproduces: https://github.com/litestar-org/litestar/issues/693 @get("/") def handler() -> Response: return Response( content="hello world", media_type=MediaType.TEXT, headers={ "foo": "BaR" # codespell:ignore }, ) with create_test_client(handler) as client: response = client.get("/") assert response.headers["foo"] == "BaR" # codespell:ignore @pytest.mark.parametrize("as_instance", [True, False]) def test_set_cookie(as_instance: bool) -> None: @get("/") def handler() -> Response: response = Response(content=None) if as_instance: response.set_cookie(Cookie(key="test", value="abc", max_age=60, expires=60, secure=True, httponly=True)) else: response.set_cookie(key="test", value="abc", max_age=60, expires=60, secure=True, httponly=True) assert len(response.cookies) == 1 return response with create_test_client(handler) as client: response = client.get("/") assert response.cookies.get("test") == "abc" def test_delete_cookie() -> None: @get("/create") def create_cookie_handler() -> Response: response = Response(content=None) response.set_cookie("test", "abc", max_age=60, expires=60, secure=True, httponly=True) assert len(response.cookies) == 1 return response @get("/delete") def delete_cookie_handler() -> Response: response = Response(content=None) response.delete_cookie( "test", "abc", ) assert len(response.cookies) == 1 return response with create_test_client(route_handlers=[create_cookie_handler, delete_cookie_handler]) as client: response = client.get("/create") assert response.cookies.get("test") == "abc" assert client.cookies.get("test") == "abc" response = client.get("/delete") assert response.cookies.get("test") is None # the commented out assert fails, because of the starlette test client's behaviour - which doesn't clear # cookies. @pytest.mark.parametrize( "media_type, expected, should_have_content_length", ((MediaType.TEXT, b"", False), (MediaType.HTML, b"", False), (MediaType.JSON, b"null", True)), ) def test_empty_response(media_type: MediaType, expected: bytes, should_have_content_length: bool) -> None: @get("/", media_type=media_type) def handler() -> None: return with create_test_client(handler) as client: response = client.get("/") assert response.content == expected assert response.headers["content-length"] == str(len(expected)) @pytest.mark.parametrize("status_code", (HTTP_204_NO_CONTENT, HTTP_304_NOT_MODIFIED)) def test_response_without_payload(status_code: int) -> None: @get("/") def handler() -> Response: return Response(b"", status_code=status_code) with create_test_client(handler) as client: response = client.get("/") assert "content-type" not in response.headers assert "content-length" not in response.headers @pytest.mark.parametrize( "status, body, should_raise", ( (HTTP_100_CONTINUE, None, False), (HTTP_101_SWITCHING_PROTOCOLS, None, False), (HTTP_102_PROCESSING, None, False), (HTTP_103_EARLY_HINTS, None, False), (HTTP_204_NO_CONTENT, None, False), (HTTP_100_CONTINUE, "1", True), (HTTP_101_SWITCHING_PROTOCOLS, "1", True), (HTTP_102_PROCESSING, "1", True), (HTTP_103_EARLY_HINTS, "1", True), (HTTP_204_NO_CONTENT, "1", True), ), ) def test_statuses_without_body(status: int, body: Optional[str], should_raise: bool) -> None: @get("/") def handler() -> Response: return Response(content=body, status_code=status) with create_test_client(handler) as client: response = client.get("/") if should_raise: assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR else: assert response.status_code == status assert "content-length" not in response.headers @pytest.mark.parametrize( "body, media_type, should_raise", ( ("", MediaType.TEXT, False), ("abc", MediaType.TEXT, False), (b"", MediaType.HTML, False), (b"abc", MediaType.HTML, False), ({"key": "value"}, MediaType.TEXT, True), ([1, 2, 3], MediaType.TEXT, True), ({"key": "value"}, MediaType.HTML, True), ([1, 2, 3], MediaType.HTML, True), ([], MediaType.HTML, False), ([], MediaType.TEXT, False), ({}, MediaType.HTML, False), ({}, MediaType.TEXT, False), ({"abc": "def"}, MediaType.JSON, False), (Empty, MediaType.JSON, True), ({"key": "value"}, "application/something+json", False), ), ) def test_render_method(body: Any, media_type: str, should_raise: bool) -> None: @get("/", media_type=media_type) def handler() -> Any: return body with create_test_client(handler) as client: response = client.get("/") if should_raise: assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR else: assert response.status_code == HTTP_200_OK def test_get_serializer() -> None: class Foo: pass foo_encoder = {Foo: lambda f: "it's a foo"} path_encoder = {PurePosixPath: lambda p: "it's a path"} class FooResponse(Response): type_encoders = foo_encoder assert get_serializer() is default_serializer assert get_serializer(type_encoders=foo_encoder)(Foo()) == "it's a foo" assert get_serializer(type_encoders=path_encoder)(PurePosixPath()) == "it's a path" assert get_serializer(FooResponse(None).type_encoders)(Foo()) == "it's a foo" assert ( get_serializer(FooResponse(None, type_encoders={Foo: lambda f: "foo"}).response_type_encoders)(Foo()) == "foo" ) def test_head_response_doesnt_support_content() -> None: with pytest.raises(ImproperlyConfiguredException): ASGIResponse(body=b"hello world", media_type=MediaType.TEXT, is_head_response=True) def test_asgi_response_encoded_headers() -> None: response = ASGIResponse(encoded_headers=[(b"foo", b"bar")]) assert response.encode_headers() == [ (b"foo", b"bar"), (b"content-type", b"application/json"), (b"content-length", b"0"), ] litestar-2.16.0/tests/unit/test_response/test_file_response.py000066400000000000000000000347561500564371300247360ustar00rootroot00000000000000import os from datetime import datetime, timezone from email.utils import formatdate from os import stat, urandom from pathlib import Path from typing import Any, Coroutine import pytest from fsspec.implementations.local import LocalFileSystem from litestar import get from litestar.connection.base import empty_send from litestar.datastructures import ETag from litestar.exceptions import ImproperlyConfiguredException from litestar.file_system import BaseLocalFileSystem, FileSystemAdapter from litestar.response.file import ASGIFileResponse, File, async_file_iterator from litestar.status_codes import HTTP_200_OK, HTTP_500_INTERNAL_SERVER_ERROR from litestar.testing import create_test_client from litestar.types import FileSystemProtocol @pytest.mark.parametrize("content_disposition_type", ("inline", "attachment")) def test_file_response_default_content_type(tmpdir: Path, content_disposition_type: Any) -> None: path = Path(tmpdir / "image.png") path.write_bytes(b"") @get("/") def handler() -> File: return File(path=path, content_disposition_type=content_disposition_type) with create_test_client(handler, openapi_config=None) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers["content-type"] == "application/octet-stream" assert response.headers["content-disposition"] == f'{content_disposition_type}; filename=""' @pytest.mark.parametrize("content_disposition_type", ("inline", "attachment")) def test_file_response_infer_content_type(tmpdir: Path, content_disposition_type: Any) -> None: path = Path(tmpdir / "image.png") path.write_bytes(b"") @get("/") def handler() -> File: return File(path=path, filename="image.png", content_disposition_type=content_disposition_type) with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers["content-type"] == "image/png" assert response.headers["content-disposition"] == f'{content_disposition_type}; filename="image.png"' @pytest.mark.parametrize("filename, expected", (("Jacky Chen", "Jacky%20Chen"), ("成龍", "%E6%88%90%E9%BE%8D"))) def test_filename(tmpdir: Path, filename: str, expected: str) -> None: path = Path(tmpdir / f"{filename}.txt") path.write_bytes(b"") @get("/") def handler() -> File: return File(path=path, filename=f"{filename}.txt") with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers["content-disposition"] == f"attachment; filename*=utf-8''{expected}.txt" def test_file_response_content_length(tmpdir: Path) -> None: content = urandom(1024 * 10) path = Path(tmpdir / "file.txt") path.write_bytes(content) @get("/") def handler() -> File: return File(path=path) with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.content == content assert response.headers["content-length"] == str(len(content)) def test_file_response_last_modified(tmpdir: Path) -> None: path = Path(tmpdir / "file.txt") path.write_bytes(b"") @get("/") def handler() -> File: return File(path=path, filename="image.png") with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers["last-modified"].lower() == formatdate(path.stat().st_mtime, usegmt=True).lower() @pytest.mark.parametrize( "mtime,expected_last_modified", [ pytest.param( datetime(2000, 1, 2, 3, 4, 5, tzinfo=timezone.utc).timestamp(), "Sun, 02 Jan 2000 03:04:05 GMT", id="timestamp", ), pytest.param( datetime(2000, 1, 2, 3, 4, 5, tzinfo=timezone.utc), "Sun, 02 Jan 2000 03:04:05 GMT", id="datetime" ), pytest.param( datetime(2000, 1, 2, 3, 4, 5, tzinfo=timezone.utc).isoformat(), "Sun, 02 Jan 2000 03:04:05 GMT", id="isoformat", ), ], ) @pytest.mark.parametrize( "mtime_key", [ "mtime", "ctime", "Last-Modified", "updated_at", "modification_time", "last_changed", "change_time", "last_modified", "last_updated", "timestamp", ], ) def test_file_response_last_modified_file_info_formats( tmpdir: Path, mtime: Any, mtime_key: str, expected_last_modified: str ) -> None: path = Path(tmpdir / "file.txt") path.write_bytes(b"") file_info = {"name": "file.txt", "size": 0, "type": "file", mtime_key: mtime} @get("/") def handler() -> File: return File( path=path, filename="image.png", file_info=file_info, # type: ignore[arg-type] ) with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers["last-modified"].lower() == expected_last_modified.lower() def test_file_response_last_modified_unsupported_mtime_type(tmpdir: Path) -> None: path = Path(tmpdir / "file.txt") path.write_bytes(b"") file_info = {"name": "file.txt", "size": 0, "type": "file", "last_updated": object()} @get("/") def handler() -> File: return File( path=path, filename="image.png", file_info=file_info, # type: ignore[arg-type] ) with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR assert "last-modified" not in response.headers def test_file_response_last_modified_mtime_not_given(tmpdir: Path) -> None: path = Path(tmpdir / "file.txt") path.write_bytes(b"") file_info = {"name": "file.txt", "size": 0, "type": "file"} @get("/") def handler() -> File: return File( path=path, filename="image.png", file_info=file_info, # type: ignore[arg-type] ) with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert "last-modified" not in response.headers def test_file_response_etag_without_mtime(tmpdir: Path) -> None: path = Path(tmpdir / "file.txt") path.write_bytes(b"") file_info = {"name": "file.txt", "size": 0, "type": "file"} @get("/") def handler() -> File: return File( path=path, filename="image.png", file_info=file_info, # type: ignore[arg-type] ) with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK # we expect etag to only have 2 parts here because no mtime was given assert len(response.headers.get("etag", "").split("-")) == 2 async def test_file_response_with_directory_raises_error(tmpdir: Path) -> None: with pytest.raises(ImproperlyConfiguredException): asgi_response = ASGIFileResponse(file_path=tmpdir, filename="example.png") await asgi_response.start_response(empty_send) @pytest.mark.parametrize("chunk_size", [4, 8, 16, 256, 512, 1024, 2048]) async def test_file_iterator(tmpdir: Path, chunk_size: int) -> None: content = urandom(1024) path = Path(tmpdir / "file.txt") path.write_bytes(content) result = b"".join( [ chunk async for chunk in async_file_iterator( file_path=path, chunk_size=chunk_size, adapter=FileSystemAdapter(BaseLocalFileSystem()) ) ] ) assert result == content @pytest.mark.parametrize("size", (1024, 2048, 4096, 1024 * 10, 2048 * 10, 4096 * 10)) def test_large_files(tmpdir: Path, size: int) -> None: content = urandom(1024 * size) path = Path(tmpdir / "file.txt") path.write_bytes(content) @get("/") def handler() -> File: return File(path=path) with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.content == content assert response.headers["content-length"] == str(len(content)) @pytest.mark.parametrize("file_system", (BaseLocalFileSystem(), LocalFileSystem())) def test_file_with_different_file_systems(tmpdir: "Path", file_system: "FileSystemProtocol") -> None: path = tmpdir / "text.txt" path.write_text("content", "utf-8") @get("/", media_type="application/octet-stream") def handler() -> File: return File( filename="text.txt", path=path, file_system=file_system, ) with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "content" assert response.headers.get("content-disposition") == 'attachment; filename="text.txt"' def test_file_with_passed_in_file_info(tmpdir: "Path") -> None: path = tmpdir / "text.txt" path.write_text("content", "utf-8") fs = LocalFileSystem() fs_info = fs.info(tmpdir / "text.txt") assert fs_info @get("/", media_type="application/octet-stream") def handler() -> File: return File(filename="text.txt", path=path, file_system=fs, file_info=fs_info) # pyright: ignore with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK, response.text assert response.text == "content" assert response.headers.get("content-disposition") == 'attachment; filename="text.txt"' def test_file_with_passed_in_stat_result(tmpdir: "Path") -> None: path = tmpdir / "text.txt" path.write_text("content", "utf-8") fs = LocalFileSystem() stat_result = stat(path) @get("/", media_type="application/octet-stream") def handler() -> File: return File(filename="text.txt", path=path, file_system=fs, stat_result=stat_result) # pyright: ignore with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "content" assert response.headers.get("content-disposition") == 'attachment; filename="text.txt"' async def test_file_with_symbolic_link(tmpdir: "Path") -> None: path = tmpdir / "text.txt" path.write_text("content", "utf-8") linked = tmpdir / "alt.txt" os.symlink(path, linked, target_is_directory=False) fs = BaseLocalFileSystem() file_info = await fs.info(linked) assert file_info["islink"] @get("/", media_type="application/octet-stream") def handler() -> File: return File(filename="alt.txt", path=linked, file_system=fs, file_info=file_info) with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "content" assert response.headers.get("content-disposition") == 'attachment; filename="alt.txt"' async def test_file_sets_etag_correctly(tmpdir: "Path") -> None: path = tmpdir / "file.txt" content = b"" Path(path).write_bytes(content) etag = ETag(value="special") @get("/") def handler() -> File: return File(path=path, etag=etag) with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers["etag"] == '"special"' def test_file_system_validation(tmpdir: "Path") -> None: path = tmpdir / "text.txt" path.write_text("content", "utf-8") class FSWithoutOpen: def info(self) -> None: return with pytest.raises(ImproperlyConfiguredException): File( filename="text.txt", path=path, file_system=FSWithoutOpen(), # type:ignore[arg-type] ) class FSWithoutInfo: def open(self) -> None: return with pytest.raises(ImproperlyConfiguredException): File( filename="text.txt", path=path, file_system=FSWithoutInfo(), # type:ignore[arg-type] ) class ImplementedFS: def info(self) -> None: return def open(self) -> None: return assert File( filename="text.txt", path=path, file_system=ImplementedFS(), # type:ignore[arg-type] ) async def test_file_response_with_missing_file_raises_error(tmpdir: Path) -> None: path = tmpdir / "404.txt" with pytest.raises(ImproperlyConfiguredException): asgi_response = ASGIFileResponse(file_path=path, filename="404.txt") await asgi_response.start_response(empty_send) @pytest.fixture() def file(tmpdir: Path) -> Path: path = tmpdir / "file.txt" content = b"a" Path(path).write_bytes(content) return path @pytest.mark.parametrize( "header_name", [ "content-length", "Content-Length", "contenT-leNgTh", # codespell:ignore ], ) def test_does_not_override_existing_content_length_header(header_name: str, file: Path) -> None: @get("/") def handler() -> File: return File(path=file, headers={header_name: "2"}) with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers.get_list("content-length") == ["2"] @pytest.mark.parametrize("header_name", ["last-modified", "Last-Modified", "LasT-modiFieD"]) def test_does_not_override_existing_last_modified_header(header_name: str, tmpdir: Path) -> None: path = Path(tmpdir / "file.txt") path.write_bytes(b"") @get("/") def handler() -> File: return File(path=path, headers={header_name: "foo"}) with create_test_client(handler) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.headers.get_list("last-modified") == ["foo"] def test_asgi_response_encoded_headers(file: Path) -> None: response = ASGIFileResponse(encoded_headers=[(b"foo", b"bar")], file_path=file) if isinstance(response.file_info, Coroutine): response.file_info.close() # silence the ResourceWarning assert response.encode_headers() == [ (b"foo", b"bar"), (b"content-type", b"application/octet-stream"), (b"content-disposition", b'attachment; filename=""'), ] litestar-2.16.0/tests/unit/test_response/test_redirect_response.py000066400000000000000000000117131500564371300256040ustar00rootroot00000000000000"""A large part of the tests in this file were adapted from: https://github.com/encode/starlette/blob/master/tests/test_responses.py And are meant to ensure our compatibility with their API. """ from __future__ import annotations from typing import TYPE_CHECKING import pytest from litestar import get from litestar.datastructures import MultiDict from litestar.exceptions import ImproperlyConfiguredException from litestar.response.base import ASGIResponse from litestar.response.redirect import ASGIRedirectResponse, Redirect from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient, create_test_client if TYPE_CHECKING: from litestar.types import Receive, Scope, Send def test_redirect_response() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: if scope["path"] == "/": response = ASGIResponse(body=b"hello, world", media_type="text/plain") else: response = ASGIRedirectResponse(path="/") await response(scope, receive, send) client = TestClient(app) response = client.get("/redirect") assert response.text == "hello, world" assert response.url == "http://testserver.local/" def test_quoting_redirect_response() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: if scope["path"] == "/test/": response = ASGIResponse(body=b"hello, world", media_type="text/plain") else: response = ASGIRedirectResponse(path="/test/") await response(scope, receive, send) client = TestClient(app) response = client.get("/redirect", follow_redirects=True) assert response.text == "hello, world" assert str(response.url) == "http://testserver.local/test/" def test_redirect_response_content_length_header() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: if scope["path"] == "/": response = ASGIResponse(body=b"hello", media_type="text/plain") else: response = ASGIRedirectResponse(path="/") await response(scope, receive, send) client: TestClient = TestClient(app) response = client.request("GET", "/redirect", follow_redirects=False) assert str(response.url) == "http://testserver.local/redirect" assert "content-length" in response.headers def test_redirect_response_status_validation() -> None: with pytest.raises(ImproperlyConfiguredException): ASGIRedirectResponse(path="/", status_code=HTTP_200_OK) # type:ignore[arg-type] def test_redirect_response_html_media_type() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: if scope["path"] == "/": response = ASGIResponse(body=b"hello") else: response = ASGIRedirectResponse(path="/", media_type="text/html") await response(scope, receive, send) client: TestClient = TestClient(app) response = client.request("GET", "/redirect", follow_redirects=False) assert str(response.url) == "http://testserver.local/redirect" assert "text/html" in str(response.headers["Content-Type"]) def test_redirect_response_media_type_validation() -> None: with pytest.raises(ImproperlyConfiguredException): ASGIRedirectResponse(path="/", media_type="application/mspgpack") @pytest.mark.parametrize( "status_code,expected_status_code", [ (301, 301), (302, 302), (303, 303), (307, 307), (308, 308), ], ) def test_redirect_dynamic_status_code(status_code: int | None, expected_status_code: int) -> None: @get("/") def handler() -> Redirect: return Redirect(path="/something-else", status_code=status_code) # type: ignore[arg-type] with create_test_client( [handler], ) as client: res = client.get("/", follow_redirects=False) assert res.status_code == expected_status_code @pytest.mark.parametrize( "query_params", [{"single": "a", "list": ["b", "c"]}, MultiDict([("single", "a"), ("list", "b"), ("list", "c")])] ) def test_redirect_with_query_params(query_params: dict[str, str | list[str]] | MultiDict) -> None: @get("/") def handler() -> Redirect: return Redirect(path="/something-else", query_params=query_params) with create_test_client([handler]) as client: location_header = client.get("/", follow_redirects=False).headers["location"] expected = "/something-else?single=a&list=b&list=c" assert location_header == expected @pytest.mark.parametrize("handler_status_code", [301, 307, None]) def test_redirect(handler_status_code: int | None) -> None: @get("/", status_code=handler_status_code) def handler() -> Redirect: return Redirect(path="/something-else", status_code=handler_status_code) # type: ignore[arg-type] with create_test_client([handler]) as client: res = client.get("/", follow_redirects=False) assert res.status_code == 302 if handler_status_code is None else handler_status_code litestar-2.16.0/tests/unit/test_response/test_response_cookies.py000066400000000000000000000120621500564371300254350ustar00rootroot00000000000000from uuid import uuid4 from litestar import Controller, HttpMethod, Litestar, Response, Router, get from litestar.datastructures import Cookie from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client def test_response_cookies() -> None: router_first = Cookie(key="second", value="1") router_second = Cookie(key="third", value="2") controller_first = Cookie(key="first", value="3") controller_second = Cookie(key="second", value="4") app_first = Cookie(key="first", value="5") app_second = Cookie(key="fourth", value="6") local_first = Cookie(key="first", value="7") test_path = "/test" class MyController(Controller): path = test_path response_cookies = [controller_first, controller_second] @get( path="/{path_param:str}", response_cookies=[local_first], ) def test_method(self) -> None: pass first_router = Router(path="/users", response_cookies=[router_first, router_second], route_handlers=[MyController]) second_router = Router(path="/external", response_cookies=[Cookie(key="external", value="nope")], route_handlers=[]) app = Litestar( openapi_config=None, response_cookies=[app_first, app_second], route_handlers=[first_router, second_router], ) route_handler, _ = app.routes[0].route_handler_map[HttpMethod.GET] # type: ignore[union-attr] response_cookies = {cookie.key: cookie.value for cookie in route_handler.resolve_response_cookies()} assert response_cookies["first"] == local_first.value assert response_cookies["second"] == controller_second.value assert response_cookies["third"] == router_second.value assert response_cookies["fourth"] == app_second.value assert "external" not in response_cookies def test_response_cookies_mapping() -> None: @get(response_cookies={"foo": "bar"}) def handler_one() -> None: pass @get(response_cookies=[Cookie(key="foo", value="bar")]) def handler_two() -> None: pass assert handler_one.resolve_response_cookies() == handler_two.resolve_response_cookies() def test_response_cookies_mapping_unresolved() -> None: # this should never happen, as there's no way to create this situation which type-checks. # we test for it nevertheless @get() def handler_one() -> None: pass handler_one.response_cookies = {"foo": "bar"} # type: ignore[assignment] assert handler_one.resolve_response_cookies() == frozenset([Cookie(key="foo", value="bar")]) def test_response_cookie_rendering() -> None: @get( "/", response_cookies=[Cookie(key="test", value="123")], ) def test_method() -> None: return None with create_test_client(test_method) as client: response = client.get("/") assert response.headers["Set-Cookie"] == "test=123; Path=/; SameSite=lax" def test_response_cookie_documentation_only_not_rendering() -> None: @get( "/", response_cookies=[ Cookie( key="my-cookie", description="my-cookie documentations", documentation_only=True, ) ], ) def test_method() -> None: return None with create_test_client(test_method) as client: response = client.get("/") assert "Set-Cookie" not in response.headers def test_response_cookie_documentation_only_not_producing_second_header() -> None: # https://github.com/litestar-org/litestar/issues/870 def after_request(response: Response) -> Response: response.set_cookie("my-cookie", "123") return response @get( "/", response_cookies=[ Cookie( key="my-cookie", description="my-cookie documentations", documentation_only=True, ) ], ) def test_method() -> None: return None with create_test_client(test_method, after_request=after_request) as client: response = client.get("/") assert response.headers["Set-Cookie"] == "my-cookie=123; Path=/; SameSite=lax" assert len(response.headers.get_list("Set-Cookie")) == 1 def test_response_cookie_is_always_set() -> None: # https://github.com/litestar-org/litestar/issues/888 @get(path="/set-cookie") def set_cookie_handler() -> Response[None]: return Response( content=None, cookies=[ Cookie( key="test", value=str(uuid4()), expires=10, ) ], ) with create_test_client([set_cookie_handler]) as client: response = client.get("/set-cookie") assert response.status_code == HTTP_200_OK assert response.cookies.get("test") cookie = response.cookies.get("test") client.cookies.clear() response = client.get("/set-cookie") assert response.status_code == HTTP_200_OK assert response.cookies.get("test") assert cookie != response.cookies.get("test") litestar-2.16.0/tests/unit/test_response/test_response_headers.py000066400000000000000000000144411500564371300254170ustar00rootroot00000000000000from typing import Dict import pytest from litestar import Controller, HttpMethod, Litestar, Router, get, post from litestar.datastructures import CacheControlHeader, ETag, ResponseHeader from litestar.datastructures.headers import Header from litestar.status_codes import HTTP_201_CREATED from litestar.testing import TestClient, create_test_client def test_response_headers() -> None: router_first = ResponseHeader(name="second", value="1") router_second = ResponseHeader(name="third", value="2") controller_first = ResponseHeader(name="first", value="3") controller_second = ResponseHeader(name="second", value="4") app_first = ResponseHeader(name="first", value="5") app_second = ResponseHeader(name="fourth", value="6") local_first = ResponseHeader(name="first", value="7") test_path = "/test" class MyController(Controller): path = test_path response_headers = [controller_first, controller_second] @get(path="/{path_param:str}", response_headers=[local_first]) def test_method(self) -> None: pass first_router = Router(path="/users", response_headers=[router_first, router_second], route_handlers=[MyController]) second_router = Router( path="/external", response_headers=[ResponseHeader(name="external", value="nope")], route_handlers=[] ) app = Litestar( openapi_config=None, response_headers=[app_first, app_second], route_handlers=[first_router, second_router], ) route_handler, _ = app.routes[0].route_handler_map[HttpMethod.GET] # type: ignore[union-attr] resolved_headers = {header.name: header for header in route_handler.resolve_response_headers()} assert resolved_headers["first"].value == local_first.value assert resolved_headers["second"].value == controller_second.value assert resolved_headers["third"].value == router_second.value assert resolved_headers["fourth"].value == app_second.value assert "external" not in resolved_headers def test_response_headers_mapping() -> None: @get(response_headers={"foo": "bar"}) def handler_one() -> None: pass @get(response_headers=[ResponseHeader(name="foo", value="bar")]) def handler_two() -> None: pass assert handler_one.resolve_response_headers() == handler_two.resolve_response_headers() def test_response_headers_mapping_unresolved() -> None: # this should never happen, as there's no way to create this situation which type-checks. # we test for it nevertheless @get() def handler_one() -> None: pass handler_one.response_headers = {"foo": "bar"} # type: ignore[assignment] assert handler_one.resolve_response_headers() == frozenset([ResponseHeader(name="foo", value="bar")]) def test_response_headers_rendering() -> None: @post( path="/test", tags=["search"], response_headers=[ResponseHeader(name="test-header", value="test value", description="test")], ) def my_handler(data: Dict[str, str]) -> Dict[str, str]: return data with create_test_client(my_handler) as client: response = client.post("/test", json={"hello": "world"}) assert response.status_code == HTTP_201_CREATED assert response.headers.get("test-header") == "test value" @pytest.mark.parametrize( "config_kwarg,app_header,controller_header,handler_header", [ ( "etag", ETag(value="1"), ETag(value="2"), ETag(value="3"), ), ( "cache_control", CacheControlHeader(max_age=1), CacheControlHeader(max_age=2), CacheControlHeader(max_age=3), ), ], ) def test_explicit_response_headers( config_kwarg: str, app_header: Header, controller_header: Header, handler_header: Header ) -> None: class MyController(Controller): @get( path="/handler-override", **{config_kwarg: handler_header}, # type: ignore[arg-type] ) def controller_override(self) -> None: pass @get(path="/controller") def controller_handler(self) -> None: pass setattr(MyController, config_kwarg, controller_header) @get(path="/app") def app_handler() -> None: pass app = Litestar( route_handlers=[MyController, app_handler], **{config_kwarg: app_header}, # type: ignore[arg-type] ) with TestClient(app=app) as client: for path, expected_value in { "handler-override": handler_header, "controller": controller_header, "app": app_header, }.items(): response = client.get(path) assert response.headers[expected_value.HEADER_NAME] == expected_value.to_header() @pytest.mark.parametrize( "config_kwarg,header", [ ("cache_control", CacheControlHeader(no_cache=True, documentation_only=True)), ("etag", ETag(value="1", documentation_only=True)), ], ) def test_explicit_headers_documentation_only(config_kwarg: str, header: Header) -> None: @get( path="/test", **{config_kwarg: header}, # type: ignore[arg-type] ) def my_handler() -> None: pass with create_test_client(my_handler) as client: response = client.get("/test") assert header.HEADER_NAME not in response.headers @pytest.mark.parametrize( "config_kwarg,response_header,header", [ ( "cache_control", ResponseHeader(name=CacheControlHeader.HEADER_NAME, value="no-store"), CacheControlHeader(no_cache=True), ), ("etag", ResponseHeader(name=ETag.HEADER_NAME, value="1"), ETag(value="2")), ], ) def test_explicit_headers_override_response_headers( config_kwarg: str, response_header: ResponseHeader, header: Header ) -> None: @get( path="/test", response_headers=[response_header], **{config_kwarg: header}, # type: ignore[arg-type] ) def my_handler() -> None: pass app = Litestar(route_handlers=[my_handler]) route_handler, _ = app.routes[0].route_handler_map[HttpMethod.GET] # type: ignore[union-attr] resolved_headers = {header.name: header for header in route_handler.resolve_response_headers()} assert resolved_headers[header.HEADER_NAME].value == header.to_header() litestar-2.16.0/tests/unit/test_response/test_response_to_asgi_response.py000066400000000000000000000377201500564371300273540ustar00rootroot00000000000000from __future__ import annotations from inspect import iscoroutine from json import loads from pathlib import Path from time import sleep from typing import TYPE_CHECKING, Any, Generator, Iterator, cast import msgspec import pytest from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse from starlette.responses import Response as StarletteResponse from litestar import HttpMethod, Litestar, MediaType, Request, Response, get, route from litestar._signature import SignatureModel from litestar.background_tasks import BackgroundTask from litestar.contrib.jinja import JinjaTemplateEngine from litestar.datastructures import Cookie, ResponseHeader from litestar.response import ServerSentEvent from litestar.response.base import ASGIResponse from litestar.response.file import ASGIFileResponse, File from litestar.response.redirect import Redirect from litestar.response.streaming import ASGIStreamingResponse, Stream from litestar.response.template import Template from litestar.status_codes import HTTP_200_OK, HTTP_308_PERMANENT_REDIRECT from litestar.template.config import TemplateConfig from litestar.testing import RequestFactory, create_test_client from litestar.types import StreamType from litestar.utils import AsyncIteratorWrapper from litestar.utils.signature import ParsedSignature from tests.models import DataclassPerson, DataclassPersonFactory if TYPE_CHECKING: from typing import AsyncGenerator from litestar.routes import HTTPRoute def my_generator() -> Generator[str, None, None]: for count in range(1, 11): yield str(count) return async def my_async_generator() -> AsyncGenerator[str, None]: for count in range(1, 11): yield str(count) return class MySyncIterator: def __init__(self) -> None: self.delay = 0.01 self.i = 1 self.to = 10 def __iter__(self) -> Iterator[str]: return self def __next__(self) -> str: i = self.i if i > self.to: raise StopIteration self.i += 1 if i: sleep(self.delay) return str(i) class MyAsyncIterator(AsyncIteratorWrapper[str]): def __init__(self) -> None: super().__init__(iterator=MySyncIterator()) async def test_to_response_async_await(anyio_backend: str) -> None: @route(http_method=HttpMethod.POST, path="/person") async def handler(data: DataclassPerson) -> DataclassPerson: assert isinstance(data, DataclassPerson) return data person_instance = DataclassPersonFactory.build() handler._signature_model = SignatureModel.create( dependency_name_set=set(), fn=handler.fn, data_dto=None, parsed_signature=ParsedSignature.from_fn(handler.fn, {}), type_decoders=[], ) response = await handler.to_response( data=handler.fn(data=person_instance), app=Litestar(route_handlers=[handler]), request=RequestFactory().get(route_handler=handler), ) assert loads(response.body) == msgspec.to_builtins(person_instance) # type: ignore[attr-defined] async def test_to_response_returning_litestar_response() -> None: @get(path="/test") def handler() -> Response: return Response(media_type=MediaType.TEXT, content="ok") with create_test_client(handler) as client: http_route: HTTPRoute = client.app.routes[0] route_handler = http_route.route_handlers[0] response = await route_handler.to_response( data=route_handler.fn(), app=client.app, request=RequestFactory().get() ) assert isinstance(response, ASGIResponse) @pytest.mark.parametrize( "expected_response", [ StarletteResponse(status_code=HTTP_200_OK, content=b"abc"), PlainTextResponse(content="abc"), HTMLResponse(content="
None: @get(path="/test") def handler() -> StarletteResponse: return expected_response with create_test_client(handler) as client: http_route: HTTPRoute = client.app.routes[0] route_handler = http_route.route_handlers[0] response = await route_handler.to_response( data=route_handler.fn(), app=client.app, request=RequestFactory().get() ) assert isinstance(response, StarletteResponse) assert response is expected_response async def test_to_response_returning_redirect_response(anyio_backend: str) -> None: background_task = BackgroundTask(lambda: "") @get( path="/test", status_code=301, response_headers=[ResponseHeader(name="local-header", value="123")], response_cookies=[Cookie(key="redirect-cookie", value="aaa"), Cookie(key="general-cookie", value="xxx")], ) def handler() -> Redirect: return Redirect( path="/somewhere-else", headers={"response-header": "abc"}, cookies=[Cookie(key="redirect-cookie", value="xyz")], background=background_task, ) with create_test_client(handler) as client: route: HTTPRoute = client.app.routes[0] route_handler = route.route_handlers[0] response = await route_handler.to_response( data=route_handler.fn(), app=client.app, request=RequestFactory().get() ) encoded_headers = response.encode_headers() # type: ignore[attr-defined] assert isinstance(response, ASGIResponse) assert (b"location", b"/somewhere-else") in encoded_headers assert (b"local-header", b"123") in encoded_headers assert (b"response-header", b"abc") in encoded_headers assert (b"set-cookie", b"general-cookie=xxx; Path=/; SameSite=lax") in encoded_headers assert (b"set-cookie", b"redirect-cookie=xyz; Path=/; SameSite=lax") in encoded_headers assert response.background == background_task def test_to_response_returning_redirect_response_from_redirect() -> None: @get(path="/proxy") def proxy_handler() -> dict: return {"message": "redirected by before request hook"} def before_request_hook_handler(_: Request) -> Redirect: return Redirect(path="/proxy", status_code=HTTP_308_PERMANENT_REDIRECT) @get(path="/test", before_request=before_request_hook_handler) def redirect_handler() -> None: raise AssertionError("this endpoint should not be reached") with create_test_client(route_handlers=[redirect_handler, proxy_handler]) as client: response = client.get("/test") assert response.status_code == HTTP_200_OK assert response.json() == {"message": "redirected by before request hook"} async def test_to_response_returning_file_response(anyio_backend: str) -> None: current_file_path = Path(__file__).resolve() filename = Path(__file__).name background_task = BackgroundTask(lambda: "") @get( path="/test", response_headers=[ResponseHeader(name="local-header", value="123")], response_cookies=[Cookie(key="redirect-cookie", value="aaa"), Cookie(key="general-cookie", value="xxx")], ) def handler() -> File: return File( path=current_file_path, filename=filename, headers={"response-header": "abc"}, cookies=[Cookie(key="file-cookie", value="xyz")], background=background_task, ) with create_test_client(handler) as client: route: HTTPRoute = client.app.routes[0] route_handler = route.route_handlers[0] response = await route_handler.to_response( data=route_handler.fn(), app=client.app, request=RequestFactory().get() ) assert isinstance(response, ASGIFileResponse) assert response.file_info if iscoroutine(response.file_info): await response.file_info encoded_headers = response.encode_headers() assert (b"local-header", b"123") in encoded_headers assert (b"response-header", b"abc") in encoded_headers assert (b"set-cookie", b"file-cookie=xyz; Path=/; SameSite=lax") in encoded_headers assert (b"set-cookie", b"general-cookie=xxx; Path=/; SameSite=lax") in encoded_headers assert (b"set-cookie", b"redirect-cookie=aaa; Path=/; SameSite=lax") in encoded_headers assert response.background == background_task @pytest.mark.parametrize( "iterator, should_raise", [ [my_generator(), False], [my_async_generator(), False], [MySyncIterator(), False], [MyAsyncIterator(), False], [my_generator, False], [my_async_generator, False], [MyAsyncIterator, False], [MySyncIterator, False], [[1, 2, 3, 4], False], ["abc", False], [b"abc", False], [{"key": 1}, False], [[{"key": 1}], False], [1, True], [None, True], ], ) async def test_to_response_streaming_response(iterator: Any, should_raise: bool, anyio_backend: str) -> None: background_task = BackgroundTask(lambda: "") @get( path="/test", response_headers=[ResponseHeader(name="local-header", value="123")], response_cookies=[Cookie(key="redirect-cookie", value="aaa"), Cookie(key="general-cookie", value="xxx")], ) def handler() -> Stream: return Stream( iterator, headers={"response-header": "abc"}, cookies=[Cookie(key="streaming-cookie", value="xyz")], background=background_task, ) with create_test_client(handler) as client: route: HTTPRoute = client.app.routes[0] route_handler = route.route_handlers[0] if should_raise: with pytest.raises(TypeError): await route_handler.to_response(data=route_handler.fn(), app=client.app, request=RequestFactory().get()) else: response = await route_handler.to_response( data=route_handler.fn(), app=client.app, request=RequestFactory().get() ) assert isinstance(response, ASGIStreamingResponse) encoded_headers = response.encode_headers() assert (b"local-header", b"123") in encoded_headers assert (b"response-header", b"abc") in encoded_headers assert (b"set-cookie", b"general-cookie=xxx; Path=/; SameSite=lax") in encoded_headers assert (b"set-cookie", b"redirect-cookie=aaa; Path=/; SameSite=lax") in encoded_headers assert (b"set-cookie", b"streaming-cookie=xyz; Path=/; SameSite=lax") in encoded_headers assert response.background == background_task async def test_to_response_template_response(anyio_backend: str, tmp_path: Path) -> None: background_task = BackgroundTask(lambda: "") p = tmp_path / "test.template" p.write_text("

hello world

") @get( path="/test", response_headers=[ResponseHeader(name="local-header", value="123")], response_cookies=[Cookie(key="redirect-cookie", value="aaa"), Cookie(key="general-cookie", value="xxx")], ) def handler() -> Template: return Template( template_name="test.template", context={}, headers={"response-header": "abc"}, cookies=[Cookie(key="template-cookie", value="xyz")], background=background_task, ) app = Litestar( route_handlers=[], template_config=TemplateConfig( engine=JinjaTemplateEngine, directory=tmp_path, ), ) with create_test_client( handler, template_config=TemplateConfig(engine=JinjaTemplateEngine, directory=tmp_path) ) as client: route: HTTPRoute = client.app.routes[0] route_handler = route.route_handlers[0] response = await route_handler.to_response( data=route_handler.fn(), app=client.app, request=RequestFactory(app=app).get() ) assert isinstance(response, ASGIResponse) encoded_headers = response.encode_headers() assert (b"local-header", b"123") in encoded_headers assert (b"response-header", b"abc") in encoded_headers assert (b"set-cookie", b"general-cookie=xxx; Path=/; SameSite=lax") in encoded_headers assert (b"set-cookie", b"template-cookie=xyz; Path=/; SameSite=lax") in encoded_headers assert response.background == background_task @pytest.mark.parametrize( "content", [ my_generator(), my_async_generator(), MySyncIterator(), MyAsyncIterator(), my_generator, my_async_generator, MyAsyncIterator, MySyncIterator, [1, 2, 3, 4], "abc", b"abc", {"key": 1}, [{"key": 1}], ], ) async def test_to_response_sse_events(content: str | bytes | StreamType[str | bytes]) -> None: background_task = BackgroundTask(lambda: "") @get( path="/test", ) def handler() -> ServerSentEvent: return ServerSentEvent( content=content, headers={"response-header": "abc"}, cookies=[Cookie(key="streaming-cookie", value="xyz")], background=background_task, comment_message="my comment message\r\nwith some\nmixed line breaks", event_id="123", event_type="special", ) with create_test_client(handler) as client: route: HTTPRoute = client.app.routes[0] route_handler = route.route_handlers[0] response = await route_handler.to_response( data=route_handler.fn(), app=client.app, request=RequestFactory().get() ) encoded_headers = response.encode_headers() # type: ignore[attr-defined] assert isinstance(response, ASGIStreamingResponse) assert ((b"cache-control", b"no-cache")) in encoded_headers assert (b"x-accel-buffering", b"no") in encoded_headers assert (b"connection", b"keep-alive") in encoded_headers assert (b"content-type", b"text/event-stream; charset=utf-8") in encoded_headers assert (b"response-header", b"abc") in encoded_headers assert (b"set-cookie", b"streaming-cookie=xyz; Path=/; SameSite=lax") in encoded_headers assert response.background == background_task @pytest.mark.parametrize( "content", [ my_generator(), my_async_generator(), my_generator, my_async_generator, MySyncIterator(), MyAsyncIterator(), MyAsyncIterator, MySyncIterator, ], ) async def test_sse_events_content(content: str | bytes | StreamType[str | bytes]) -> None: @get( path="/test", ) def handler() -> ServerSentEvent: return ServerSentEvent( content=content, comment_message="my comment message\r\nwith some\nmixed line breaks", event_id="123", event_type="special", ) events: list[bytes] = [] with create_test_client(handler) as client: route: HTTPRoute = client.app.routes[0] route_handler = route.route_handlers[0] response = await route_handler.to_response( data=route_handler.fn(), app=client.app, request=RequestFactory().get() ) assert isinstance(response, ASGIStreamingResponse) async for value in response.iterator: events.append(cast("bytes", value)) assert events == [ b": my comment message\r\n", b": with some\r\n", b": mixed line breaks\r\n", b"id: 123\r\n", b"event: special\r\n", b"id: 123\r\nevent: special\r\ndata: 1\r\n\r\n", b"id: 123\r\nevent: special\r\ndata: 2\r\n\r\n", b"id: 123\r\nevent: special\r\ndata: 3\r\n\r\n", b"id: 123\r\nevent: special\r\ndata: 4\r\n\r\n", b"id: 123\r\nevent: special\r\ndata: 5\r\n\r\n", b"id: 123\r\nevent: special\r\ndata: 6\r\n\r\n", b"id: 123\r\nevent: special\r\ndata: 7\r\n\r\n", b"id: 123\r\nevent: special\r\ndata: 8\r\n\r\n", b"id: 123\r\nevent: special\r\ndata: 9\r\n\r\n", b"id: 123\r\nevent: special\r\ndata: 10\r\n\r\n", ] litestar-2.16.0/tests/unit/test_response/test_serialization.py000066400000000000000000000066351500564371300247510ustar00rootroot00000000000000import enum from pathlib import Path, PurePath, PureWindowsPath from typing import Any, Callable, cast import msgspec import pytest from pytest import FixtureRequest from litestar import MediaType, Response from litestar.exceptions import ImproperlyConfiguredException from litestar.serialization import get_serializer from tests.models import DataclassPersonFactory, MsgSpecStructPerson person = DataclassPersonFactory.build() class _TestEnum(enum.Enum): A = "alpha" B = "beta" @pytest.fixture(params=[MediaType.JSON, MediaType.MESSAGEPACK]) def media_type(request: FixtureRequest) -> MediaType: return cast(MediaType, request.param) DecodeMediaType = Callable[[Any], Any] @pytest.fixture() def decode_media_type(media_type: MediaType) -> DecodeMediaType: if media_type == MediaType.JSON: return msgspec.json.decode return msgspec.msgpack.decode def test_dataclass(media_type: MediaType, decode_media_type: DecodeMediaType) -> None: encoded = Response(None).render(person, media_type=media_type) assert decode_media_type(encoded) == msgspec.to_builtins(person) def test_struct(media_type: MediaType, decode_media_type: DecodeMediaType) -> None: encoded = Response(None).render(MsgSpecStructPerson(**msgspec.to_builtins(person)), media_type=media_type) assert decode_media_type(encoded) == msgspec.to_builtins(person) @pytest.mark.parametrize("content", [{"value": 1}, [{"value": 1}]]) def test_dict(media_type: MediaType, decode_media_type: DecodeMediaType, content: Any) -> None: encoded = Response(None).render(content, media_type=media_type) assert decode_media_type(encoded) == content def test_enum(media_type: MediaType, decode_media_type: DecodeMediaType) -> None: encoded = Response(None).render({"value": _TestEnum.A}, media_type=media_type) assert decode_media_type(encoded) == {"value": _TestEnum.A.value} @pytest.mark.parametrize("path", [PurePath("/path/to/file"), Path("/path/to/file")]) def test_path(media_type: MediaType, decode_media_type: DecodeMediaType, path: Path) -> None: encoded = Response(None).render({"value": path}, media_type=media_type) expected = r"\path\to\file" if isinstance(path, PureWindowsPath) else "/path/to/file" assert decode_media_type(encoded) == {"value": expected} @pytest.mark.parametrize( "content, response_type, media_type", [["abcdefg", str, MediaType.TEXT], ["
", str, MediaType.HTML]] ) def test_response_serialization_text_types(content: Any, response_type: Any, media_type: MediaType) -> None: assert Response(None).render(content, media_type=media_type, enc_hook=get_serializer({})) == content.encode("utf-8") @pytest.mark.parametrize( "content, response_type, media_type, should_raise", [ ["abcdefg", str, "text/custom", False], ["", str, "application/unknown", False], [b"", bytes, "application/unknown", False], [{"key": "value"}, dict, "application/unknown", True], ], ) def test_response_validation_of_unknown_media_types( content: Any, response_type: Any, media_type: MediaType, should_raise: bool ) -> None: response = Response(None) if should_raise: with pytest.raises(ImproperlyConfiguredException): response.render(content, media_type=media_type) else: rendered = response.render(content, media_type=media_type) assert rendered == (content if isinstance(content, bytes) else content.encode("utf-8")) litestar-2.16.0/tests/unit/test_response/test_sse.py000066400000000000000000000110601500564371300226520ustar00rootroot00000000000000from typing import AsyncIterator, Iterator, List import anyio import pytest from httpx_sse import ServerSentEvent as HTTPXServerSentEvent from httpx_sse import aconnect_sse from litestar import get from litestar.exceptions import ImproperlyConfiguredException from litestar.response import ServerSentEvent from litestar.response.sse import ServerSentEventMessage from litestar.testing import create_async_test_client from litestar.types import SSEData async def test_sse_steaming_response() -> None: @get( path="/test", ) def handler() -> ServerSentEvent: def numbers(minimum: int, maximum: int) -> Iterator[str]: for i in range(minimum, maximum + 1): yield str(i) generator = numbers(1, 5) return ServerSentEvent(content=generator, event_id="123", event_type="special", retry_duration=1000) async with create_async_test_client(handler) as client: async with aconnect_sse(client, "GET", f"{client.base_url}/test") as event_source: events = [sse async for sse in event_source.aiter_sse()] assert len(events) == 5 for idx, sse in enumerate(events, start=1): assert sse.event == "special" assert sse.data == str(idx) assert sse.id == "123" assert sse.retry == 1000 @pytest.mark.parametrize( "input,expected_events", [ ("string", [HTTPXServerSentEvent(event="special", data=str(i), id="123", retry=1000) for i in range(1, 6)]), ("bytes", [HTTPXServerSentEvent(event="special", data=str(i), id="123", retry=1000) for i in range(1, 6)]), ("integer", [HTTPXServerSentEvent(event="special", data=str(i), id="123", retry=1000) for i in range(1, 6)]), ("dict1", [HTTPXServerSentEvent(event="special", data=str(i), id="123", retry=1000) for i in range(1, 6)]), ( "dict2", [ HTTPXServerSentEvent( event=e, data=str(i) if e == "event1" else str(2 * i), id="123", retry=1000 if e == "event1" else 10 ) for i in range(1, 6) for e in ["event1", "event2"] ], ), ("obj", [HTTPXServerSentEvent(event="special", data=str(i), id="123", retry=1000) for i in range(1, 6)]), ("empty", [HTTPXServerSentEvent(event="something-empty", id="123", retry=1000) for i in range(1, 6)]), ("comment", [HTTPXServerSentEvent(event="something-with-comment", id="123", retry=1000) for i in range(1, 6)]), ], ) async def test_various_sse_inputs(input: str, expected_events: List[HTTPXServerSentEvent]) -> None: @get("/testme") async def handler() -> ServerSentEvent: async def numbers() -> AsyncIterator[SSEData]: for i in range(1, 6): await anyio.sleep(0.001) if input == "integer": yield i elif input == "string": yield str(i) elif input == "bytes": yield str(i).encode("utf-8") elif input == "dict1": yield {"data": i, "event": "special", "retry": 1000} elif input == "dict2": yield {"data": i, "event": "event1", "retry": 1000} yield {"data": 2 * i, "event": "event2", "retry": 10} elif input == "obj": yield ServerSentEventMessage(data=i, event="special", retry=1000) elif input == "empty": yield ServerSentEventMessage(event="something-empty", retry=1000) elif input == "comment": yield ServerSentEventMessage(event="something-with-comment", retry=1000, comment="some comment") return ServerSentEvent(numbers(), event_type="special", event_id="123", retry_duration=1000) async with create_async_test_client(handler) as client: async with aconnect_sse(client, "GET", f"{client.base_url}/testme") as event_source: events = [sse async for sse in event_source.aiter_sse()] assert len(events) == len(expected_events) for i in range(len(expected_events)): assert events[i].event == expected_events[i].event assert events[i].data == expected_events[i].data assert events[i].id == expected_events[i].id assert events[i].retry == expected_events[i].retry def test_invalid_content_type_raises() -> None: with pytest.raises(ImproperlyConfiguredException): ServerSentEvent(content=object()) # type: ignore[arg-type] litestar-2.16.0/tests/unit/test_response/test_streaming_response.py000066400000000000000000000141471500564371300260000ustar00rootroot00000000000000"""A large part of the tests in this file were adapted from: https://github.com/encode/starlette/blob/master/tests/test_responses.py And are meant to ensure our compatibility with their API. """ from itertools import cycle from typing import TYPE_CHECKING, AsyncIterator, Iterator import anyio from litestar.background_tasks import BackgroundTask from litestar.response.streaming import ASGIStreamingResponse from litestar.testing import TestClient if TYPE_CHECKING: from litestar.types import Message, Receive, Scope, Send def test_streaming_response_unknown_size() -> None: app = ASGIStreamingResponse(iterator=iter(["hello", "world"])) client = TestClient(app) response = client.get("/") assert "content-length" not in response.headers def test_streaming_response_known_size() -> None: app = ASGIStreamingResponse(iterator=iter(["hello", "world"]), headers={"content-length": "10"}) client = TestClient(app) response = client.get("/") assert response.headers["content-length"] == "10" async def test_streaming_response_stops_if_receiving_http_disconnect_with_async_iterator(anyio_backend: str) -> None: streamed = 0 disconnected = anyio.Event() async def receive_disconnect() -> dict: await disconnected.wait() return {"type": "http.disconnect"} async def send(message: "Message") -> None: nonlocal streamed if message["type"] == "http.response.body": streamed += len(message.get("body", b"")) # Simulate disconnection after download has started if streamed >= 16: disconnected.set() async def stream_indefinitely() -> AsyncIterator[bytes]: while True: # Need a sleep for the event loop to switch to another task await anyio.sleep(0) yield b"chunk " response = ASGIStreamingResponse(iterator=stream_indefinitely()) with anyio.move_on_after(1) as cancel_scope: await response({}, receive_disconnect, send) # type: ignore[arg-type] assert not cancel_scope.cancel_called, "Content streaming should stop itself." async def test_streaming_response_stops_if_receiving_http_disconnect_with_sync_iterator(anyio_backend: str) -> None: streamed = 0 disconnected = anyio.Event() async def receive_disconnect() -> dict: await disconnected.wait() return {"type": "http.disconnect"} async def send(message: "Message") -> None: nonlocal streamed if message["type"] == "http.response.body": streamed += len(message.get("body", b"")) # Simulate disconnection after download has started if streamed >= 16: disconnected.set() response = ASGIStreamingResponse(iterator=cycle(["1", "2", "3"])) with anyio.move_on_after(1) as cancel_scope: await response({}, receive_disconnect, send) # type: ignore[arg-type] assert not cancel_scope.cancel_called, "Content streaming should stop itself." def test_streaming_response() -> None: filled_by_bg_task = "" async def app(scope: "Scope", receive: "Receive", send: "Send") -> None: async def numbers(minimum: int, maximum: int) -> AsyncIterator[str]: for i in range(minimum, maximum + 1): yield str(i) if i != maximum: yield ", " await anyio.sleep(0) async def numbers_for_cleanup(start: int = 1, stop: int = 5) -> None: nonlocal filled_by_bg_task async for thing in numbers(start, stop): filled_by_bg_task += thing cleanup_task = BackgroundTask(numbers_for_cleanup, start=6, stop=9) generator = numbers(1, 5) response = ASGIStreamingResponse(iterator=generator, media_type="text/plain", background=cleanup_task) await response(scope, receive, send) assert not filled_by_bg_task client = TestClient(app) response = client.get("/") assert response.text == "1, 2, 3, 4, 5" assert filled_by_bg_task == "6, 7, 8, 9" def test_streaming_response_custom_iterator() -> None: async def app(scope: "Scope", receive: "Receive", send: "Send") -> None: class CustomAsyncIterator: def __init__(self) -> None: self._called = 0 def __aiter__(self) -> "CustomAsyncIterator": return self async def __anext__(self) -> str: if self._called == 5: raise StopAsyncIteration() self._called += 1 return str(self._called) response = ASGIStreamingResponse(iterator=CustomAsyncIterator(), media_type="text/plain") await response(scope, receive, send) client = TestClient(app) response = client.get("/") assert response.text == "12345" def test_streaming_response_custom_iterable() -> None: async def app(scope: "Scope", receive: "Receive", send: "Send") -> None: class CustomAsyncIterable: async def __aiter__(self) -> AsyncIterator[str]: for i in range(5): yield str(i + 1) response = ASGIStreamingResponse(iterator=CustomAsyncIterable(), media_type="text/plain") await response(scope, receive, send) client = TestClient(app) response = client.get("/") assert response.text == "12345" def test_sync_streaming_response() -> None: async def app(scope: "Scope", receive: "Receive", send: "Send") -> None: def numbers(minimum: int, maximum: int) -> Iterator[str]: for i in range(minimum, maximum + 1): yield str(i) if i != maximum: yield ", " generator = numbers(1, 5) response = ASGIStreamingResponse(iterator=generator, media_type="text/plain") await response(scope, receive, send) client = TestClient(app) response = client.get("/") assert response.text == "1, 2, 3, 4, 5" def test_asgi_response_encoded_headers() -> None: response = ASGIStreamingResponse(encoded_headers=[(b"foo", b"bar")], iterator="") assert response.encode_headers() == [(b"foo", b"bar"), (b"content-type", b"application/json")] litestar-2.16.0/tests/unit/test_response/test_type_decoders.py000066400000000000000000000067551500564371300247300ustar00rootroot00000000000000from typing import Any, Literal, Type, Union from unittest import mock import pytest from litestar import Controller, Litestar, Router, get from litestar.datastructures.url import URL from litestar.enums import HttpMethod from litestar.handlers.http_handlers.base import HTTPRouteHandler from litestar.handlers.websocket_handlers.listener import ( WebsocketListener, WebsocketListenerRouteHandler, websocket_listener, ) from litestar.types.composite_types import TypeDecodersSequence handler_decoder, router_decoder, controller_decoder, app_decoder = 4 * [(lambda t: t is URL, lambda t, v: URL(v))] @pytest.fixture(scope="module") def controller() -> Type[Controller]: class MyController(Controller): path = "/controller" type_decoders = [controller_decoder] @get("/http", type_decoders=[handler_decoder]) def http(self) -> Any: ... @websocket_listener("/ws", type_decoders=[handler_decoder]) async def handler(self, data: str) -> None: ... return MyController @pytest.fixture(scope="module") def websocket_listener_handler() -> Type[WebsocketListener]: class WebSocketHandler(WebsocketListener): path = "/ws-listener" type_decoders = [handler_decoder] def on_receive(self, data: str) -> None: # pyright: ignore [reportIncompatibleMethodOverride] ... return WebSocketHandler @pytest.fixture(scope="module") def http_handler() -> HTTPRouteHandler: @get("/http", type_decoders=[handler_decoder]) def http() -> Any: ... return http @pytest.fixture(scope="module") def websocket_handler() -> WebsocketListenerRouteHandler: @websocket_listener("/ws", type_decoders=[handler_decoder]) async def websocket(data: str) -> None: ... return websocket @pytest.fixture(scope="module") def router( controller: Type[Controller], websocket_listener_handler: Type[WebsocketListenerRouteHandler], http_handler: Type[HTTPRouteHandler], websocket_handler: Type[WebsocketListenerRouteHandler], ) -> Router: return Router( "/router", type_decoders=[router_decoder], route_handlers=[controller, websocket_listener_handler, http_handler, websocket_handler], ) @pytest.fixture(scope="module") @mock.patch("litestar.app.Litestar._get_default_plugins", mock.Mock(return_value=[])) def app(router: Router) -> Litestar: return Litestar([router], type_decoders=[app_decoder]) @pytest.mark.parametrize( "path, method, type_decoders", ( ("/router/controller/http", HttpMethod.GET, [app_decoder, router_decoder, controller_decoder, handler_decoder]), ("/router/controller/ws", "websocket", [app_decoder, router_decoder, controller_decoder, handler_decoder]), ("/router/http", HttpMethod.GET, [app_decoder, router_decoder, handler_decoder]), ("/router/ws", "websocket", [app_decoder, router_decoder, handler_decoder]), ("/router/ws-listener", "websocket", [app_decoder, router_decoder, handler_decoder]), ), ids=( "Controller http endpoint type decoders", "Controller ws endpoint type decoders", "Router http endpoint type decoders", "Router ws endpoint type decoders", "Router ws listener type decoders", ), ) def test_resolve_type_decoders( path: str, method: Union[HttpMethod, Literal["websocket"]], type_decoders: TypeDecodersSequence, app: Litestar ) -> None: handler = app.route_handler_method_map[path][method] assert handler.resolve_type_decoders() == type_decoders litestar-2.16.0/tests/unit/test_response/test_type_encoders.py000066400000000000000000000034711500564371300247320ustar00rootroot00000000000000from typing import TYPE_CHECKING, Any, Tuple from litestar import Controller, HttpMethod, Litestar, Response, Router, get from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.types import Serializer def create_mock_encoder(name: str) -> Tuple[type, "Serializer"]: mock_type = type(name, (type,), {}) def mock_encoder(obj: Any) -> Any: return name return mock_type, mock_encoder handler_type, handler_encoder = create_mock_encoder("HandlerType") router_type, router_encoder = create_mock_encoder("RouterType") controller_type, controller_encoder = create_mock_encoder("ControllerType") app_type, app_encoder = create_mock_encoder("AppType") def test_resolve_type_encoders() -> None: class MyController(Controller): type_encoders = {controller_type: controller_encoder} @get("/", type_encoders={handler_type: handler_encoder}) def handler(self) -> Any: ... router = Router("/router", type_encoders={router_type: router_encoder}, route_handlers=[MyController]) app = Litestar([router], type_encoders={app_type: app_encoder}) route_handler = app.routes[0].route_handler_map[HttpMethod.GET][0] # type: ignore[union-attr] encoders = route_handler.resolve_type_encoders() assert encoders.get(handler_type) == handler_encoder assert encoders.get(controller_type) == controller_encoder assert encoders.get(router_type) == router_encoder assert encoders.get(app_type) == app_encoder def test_type_encoders_response_override() -> None: class Foo: pass @get("/", type_encoders={Foo: lambda f: "foo"}) def handler() -> Response: return Response({"obj": Foo()}, type_encoders={Foo: lambda f: "FOO"}) with create_test_client([handler]) as client: assert client.get("/").json() == {"obj": "FOO"} litestar-2.16.0/tests/unit/test_response_class_resolution.py000066400000000000000000000044101500564371300244720ustar00rootroot00000000000000from typing import Optional, Type import pytest from litestar import Controller, HttpMethod, Litestar, Response, Router, get from litestar.handlers.http_handlers.base import HTTPRouteHandler RouterResponse: Type[Response] = type("RouterResponse", (Response,), {}) ControllerResponse: Type[Response] = type("ControllerResponse", (Response,), {}) AppResponse: Type[Response] = type("AppResponse", (Response,), {}) HandlerResponse: Type[Response] = type("HandlerResponse", (Response,), {}) @pytest.mark.parametrize( "handler_response_class, controller_response_class, router_response_class, app_response_class, expected", ( (HandlerResponse, ControllerResponse, RouterResponse, AppResponse, HandlerResponse), (None, ControllerResponse, RouterResponse, AppResponse, ControllerResponse), (None, None, RouterResponse, AppResponse, RouterResponse), (None, None, None, AppResponse, AppResponse), (None, None, None, None, Response), ), ids=( "Custom class for all layers", "Custom class for all above handler layer", "Custom class for all above controller layer", "Custom class for all above router layer", "No custom class for layers", ), ) def test_response_class_resolution_of_layers( handler_response_class: Optional[Type[Response]], controller_response_class: Optional[Type[Response]], router_response_class: Optional[Type[Response]], app_response_class: Optional[Type[Response]], expected: Type[Response], ) -> None: class MyController(Controller): @get() def handler(self) -> None: pass if controller_response_class: MyController.response_class = ControllerResponse router = Router(path="/", route_handlers=[MyController]) if router_response_class: router.response_class = router_response_class app = Litestar(route_handlers=[router]) if app_response_class: app.response_class = app_response_class route_handler: HTTPRouteHandler = app.route_handler_method_map["/"][HttpMethod.GET] # type: ignore[assignment] if handler_response_class: route_handler.response_class = handler_response_class response_class = route_handler.resolve_response_class() assert response_class is expected litestar-2.16.0/tests/unit/test_security/000077500000000000000000000000001500564371300204625ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_security/__init__.py000066400000000000000000000000001500564371300225610ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_security/test_jwt/000077500000000000000000000000001500564371300223255ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_security/test_jwt/__init__.py000066400000000000000000000000001500564371300244240ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_security/test_jwt/test_auth.py000066400000000000000000000767641500564371300247230ustar00rootroot00000000000000import dataclasses import secrets import string from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple from uuid import uuid4 import jwt import msgspec import pytest from hypothesis import given, settings from hypothesis.strategies import dictionaries, integers, none, one_of, sampled_from, text, timedeltas from typing_extensions import TypeAlias from litestar import Litestar, Request, Response, get from litestar.security.jwt import JWTAuth, JWTCookieAuth, OAuth2PasswordBearerAuth, Token from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_401_UNAUTHORIZED from litestar.stores.memory import MemoryStore from litestar.testing import TestClient, create_test_client from tests.models import User, UserFactory if TYPE_CHECKING: from litestar.connection import ASGIConnection @pytest.fixture(scope="module") def mock_db() -> MemoryStore: return MemoryStore() @given( algorithm=sampled_from( [ "HS256", "HS384", "HS512", ] ), auth_header=sampled_from(["Authorization", "X-API-Key"]), default_token_expiration=timedeltas(min_value=timedelta(seconds=30), max_value=timedelta(weeks=1)), token_secret=text(min_size=10), response_status_code=integers(min_value=200, max_value=201), token_expiration=timedeltas(min_value=timedelta(seconds=30), max_value=timedelta(weeks=1)), token_issuer=one_of(none(), text(max_size=256)), token_audience=one_of(none(), text(max_size=256, alphabet=string.ascii_letters)), token_unique_jwt_id=one_of(none(), text(max_size=256)), token_extras=one_of(none(), dictionaries(text(max_size=256), text(max_size=256))), ) @settings(deadline=None) async def test_jwt_auth( mock_db: "MemoryStore", algorithm: str, auth_header: str, default_token_expiration: timedelta, token_secret: str, response_status_code: int, token_expiration: timedelta, token_issuer: Optional[str], token_audience: Optional[str], token_unique_jwt_id: Optional[str], token_extras: Optional[Dict[str, Any]], ) -> None: mock_block_list: Dict[str, str] = {} user = UserFactory.build() await mock_db.set(str(user.id), user, 120) # type: ignore[arg-type] async def retrieve_user_handler(token: Token, _: "ASGIConnection") -> Any: return await mock_db.get(token.sub) async def revoked_token_handler(token: Token, _: "ASGIConnection") -> bool: if token.jti: return mock_block_list.get(token.jti) == "revoked" return False jwt_auth = JWTAuth[Any]( algorithm=algorithm, auth_header=auth_header, default_token_expiration=default_token_expiration, token_secret=token_secret, retrieve_user_handler=retrieve_user_handler, revoked_token_handler=revoked_token_handler, ) @get("/my-endpoint", middleware=[jwt_auth.middleware]) def my_handler(request: Request["User", Token, Any]) -> None: assert request.user assert msgspec.to_builtins(request.user) == msgspec.to_builtins(user) assert request.auth.sub == str(user.id) @get("/login") def login_handler() -> Response["User"]: return jwt_auth.login( identifier=str(user.id), response_body=user, response_status_code=response_status_code, token_expiration=token_expiration, token_issuer=token_issuer, token_audience=token_audience, token_unique_jwt_id=token_unique_jwt_id, token_extras=token_extras, ) @get("/logout", middleware=[jwt_auth.middleware]) def logout_handler(request: Request["User", Token, Any]) -> Dict[str, str]: jti = request.auth.jti if jti: mock_block_list[jti] = "revoked" return {"message": "logged out successfully"} return {"message": f"can't logout, jti is {jti}"} with create_test_client(route_handlers=[my_handler, login_handler, logout_handler]) as client: response = client.get("/login") assert response.status_code == response_status_code _, _, encoded_token = response.headers.get(auth_header).partition(" ") assert encoded_token decoded_token = Token.decode(encoded_token=encoded_token, secret=token_secret, algorithm=algorithm) assert decoded_token.sub == str(user.id) assert decoded_token.iss == token_issuer assert decoded_token.aud == token_audience assert decoded_token.jti == token_unique_jwt_id if token_extras is not None: for key, value in token_extras.items(): assert decoded_token.extras[key] == value response = client.get("/my-endpoint") assert response.status_code == HTTP_401_UNAUTHORIZED response = client.get("/my-endpoint", headers={auth_header: jwt_auth.format_auth_header(encoded_token)}) assert response.status_code == HTTP_200_OK response = client.get("/logout", headers={auth_header: jwt_auth.format_auth_header(encoded_token)}) if decoded_token.jti: assert response.json()["message"] == "logged out successfully" response = client.get("/my-endpoint", headers={auth_header: jwt_auth.format_auth_header(encoded_token)}) assert response.status_code == HTTP_401_UNAUTHORIZED else: assert response.json()["message"] == f"can't logout, jti is {decoded_token.jti}" response = client.get("/my-endpoint", headers={auth_header: encoded_token}) assert response.status_code == HTTP_401_UNAUTHORIZED response = client.get("/my-endpoint", headers={auth_header: uuid4().hex}) assert response.status_code == HTTP_401_UNAUTHORIZED fake_token = Token( sub=uuid4().hex, iss=token_issuer, aud=token_audience, jti=token_unique_jwt_id, exp=(datetime.now(timezone.utc) + token_expiration), ).encode(secret=token_secret, algorithm=algorithm) response = client.get("/my-endpoint", headers={auth_header: jwt_auth.format_auth_header(fake_token)}) assert response.status_code == HTTP_401_UNAUTHORIZED @pytest.mark.parametrize("auth_cls", [JWTAuth, JWTCookieAuth, OAuth2PasswordBearerAuth]) async def test_jwt_auth_custom_token_cls(auth_cls: Any) -> None: @dataclasses.dataclass class CustomToken(Token): random_field: int = 1 async def retrieve_user_handler(token: CustomToken, _: "ASGIConnection") -> Any: return object() token_secret = secrets.token_hex() if auth_cls is OAuth2PasswordBearerAuth: jwt_auth = auth_cls( token_secret=token_secret, retrieve_user_handler=retrieve_user_handler, token_cls=CustomToken, token_url="http://testserver.local", ) else: jwt_auth = auth_cls[Any]( token_secret=token_secret, retrieve_user_handler=retrieve_user_handler, token_cls=CustomToken, ) @get("/", middleware=[jwt_auth.middleware]) def handler(request: Request[Any, CustomToken, Any]) -> Dict[str, Any]: return { "is_token_cls": isinstance(request.auth, CustomToken), "token": dataclasses.asdict(request.auth), } header = jwt_auth.format_auth_header( jwt_auth.create_token( "foo", token_extras={"foo": "bar"}, # pass a string here as value to ensure things get converted properly random_field="2", ), ) with create_test_client(route_handlers=[handler]) as client: response = client.get("/", headers={"Authorization": header}) assert response.status_code == 200 response_data = response.json() assert response_data["is_token_cls"] is True assert response_data["token"]["extras"] == {"foo": "bar"} assert response_data["token"]["random_field"] == 2 @given( algorithm=sampled_from( [ "HS256", "HS384", "HS512", ] ), auth_header=sampled_from(["Authorization", "X-API-Key"]), auth_cookie=sampled_from(["token", "accessToken"]), default_token_expiration=timedeltas(min_value=timedelta(seconds=30), max_value=timedelta(weeks=1)), token_secret=text(min_size=10), response_status_code=integers(min_value=200, max_value=201), token_expiration=timedeltas(min_value=timedelta(seconds=30), max_value=timedelta(weeks=1)), token_issuer=one_of(none(), text(max_size=256)), token_audience=one_of(none(), text(max_size=256, alphabet=string.ascii_letters)), token_unique_jwt_id=one_of(none(), text(max_size=256)), token_extras=one_of(none(), dictionaries(text(max_size=256), text(max_size=256))), ) @settings(deadline=None) async def test_jwt_cookie_auth( mock_db: "MemoryStore", algorithm: str, auth_header: str, auth_cookie: str, default_token_expiration: timedelta, token_secret: str, response_status_code: int, token_expiration: timedelta, token_issuer: Optional[str], token_audience: Optional[str], token_unique_jwt_id: Optional[str], token_extras: Optional[Dict[str, Any]], ) -> None: mock_block_list: Dict[str, str] = {} user = UserFactory.build() await mock_db.set(str(user.id), user, 120) # type: ignore[arg-type] async def retrieve_user_handler(token: Token, connection: Any) -> Any: assert connection return await mock_db.get(token.sub) async def revoked_token_handler(token: Token, _: Any) -> bool: if token.jti: return mock_block_list.get(token.jti) == "revoked" return False jwt_auth = JWTCookieAuth( algorithm=algorithm, key=auth_cookie, auth_header=auth_header, default_token_expiration=default_token_expiration, retrieve_user_handler=retrieve_user_handler, # type: ignore[var-annotated] revoked_token_handler=revoked_token_handler, token_secret=token_secret, ) @get("/my-endpoint", middleware=[jwt_auth.middleware]) def my_handler(request: Request["User", Token, Any]) -> None: assert request.user assert msgspec.to_builtins(request.user) == msgspec.to_builtins(user) assert request.auth.sub == str(user.id) @get("/login") def login_handler() -> Response["User"]: return jwt_auth.login( identifier=str(user.id), response_body=user, response_status_code=response_status_code, token_expiration=token_expiration, token_issuer=token_issuer, token_audience=token_audience, token_unique_jwt_id=token_unique_jwt_id, token_extras=token_extras, ) @get("/logout", middleware=[jwt_auth.middleware]) def logout_handler(request: Request["User", Token, Any]) -> Dict[str, str]: jti = request.auth.jti if jti: mock_block_list[jti] = "revoked" return {"message": "logged out successfully"} return {"message": f"can't logout, jti is {jti}"} with create_test_client(route_handlers=[my_handler, login_handler, logout_handler]) as client: response = client.get("/login") assert response.status_code == response_status_code _, _, encoded_token = response.headers.get(auth_header).partition(" ") assert encoded_token decoded_token = Token.decode(encoded_token=encoded_token, secret=token_secret, algorithm=algorithm) assert decoded_token.sub == str(user.id) assert decoded_token.iss == token_issuer assert decoded_token.aud == token_audience assert decoded_token.jti == token_unique_jwt_id if token_extras is not None: for key, value in token_extras.items(): assert decoded_token.extras[key] == value client.cookies.clear() response = client.get("/my-endpoint") assert response.status_code == HTTP_401_UNAUTHORIZED client.cookies.clear() response = client.get("/my-endpoint", headers={auth_header: jwt_auth.format_auth_header(encoded_token)}) assert response.status_code == HTTP_200_OK client.cookies = {auth_cookie: jwt_auth.format_auth_header(encoded_token)} # type: ignore[assignment] response = client.get( "/my-endpoint", ) assert response.status_code == HTTP_200_OK client.cookies.clear() response = client.get("/my-endpoint", headers={auth_header: encoded_token}) assert response.status_code == HTTP_401_UNAUTHORIZED client.cookies.clear() response = client.get("/my-endpoint", headers={auth_cookie: encoded_token}) assert response.status_code == HTTP_401_UNAUTHORIZED client.cookies.clear() response = client.get("/my-endpoint", headers={auth_header: jwt_auth.format_auth_header(uuid4().hex)}) assert response.status_code == HTTP_401_UNAUTHORIZED client.cookies = {auth_cookie: jwt_auth.format_auth_header(uuid4().hex)} # type: ignore[assignment] response = client.get("/my-endpoint") assert response.status_code == HTTP_401_UNAUTHORIZED client.cookies = {auth_cookie: uuid4().hex} # type: ignore[assignment] response = client.get("/my-endpoint") assert response.status_code == HTTP_401_UNAUTHORIZED fake_token = Token( sub=uuid4().hex, iss=token_issuer, aud=token_audience, jti=token_unique_jwt_id, exp=(datetime.now(timezone.utc) + token_expiration), ).encode(secret=token_secret, algorithm=algorithm) client.cookies.clear() response = client.get("/my-endpoint", headers={auth_header: jwt_auth.format_auth_header(fake_token)}) assert response.status_code == HTTP_401_UNAUTHORIZED client.cookies = {auth_cookie: jwt_auth.format_auth_header(fake_token)} # type: ignore[assignment] response = client.get("/my-endpoint") assert response.status_code == HTTP_401_UNAUTHORIZED client.cookies.clear() client.cookies = {auth_cookie: jwt_auth.format_auth_header(encoded_token)} # type: ignore[assignment] response = client.get("/my-endpoint") assert response.status_code == HTTP_200_OK response = client.get("/logout") if decoded_token.jti: assert response.json()["message"] == "logged out successfully" response = client.get("/my-endpoint") assert response.status_code == HTTP_401_UNAUTHORIZED else: assert response.json()["message"] == f"can't logout, jti is {decoded_token.jti}" async def test_path_exclusion() -> None: async def retrieve_user_handler(_: Token, __: "ASGIConnection") -> None: return None jwt_auth = JWTAuth[Any]( token_secret="abc123", retrieve_user_handler=retrieve_user_handler, exclude=["north", "south"], ) @get("/north/{value:int}") def north_handler(value: int) -> Dict[str, int]: return {"value": value} @get("/south") def south_handler() -> None: return None @get("/west") def west_handler() -> None: return None with create_test_client( route_handlers=[north_handler, south_handler, west_handler], on_app_init=[jwt_auth.on_app_init] ) as client: response = client.get("/north/1") assert response.status_code == HTTP_200_OK response = client.get("/south") assert response.status_code == HTTP_200_OK response = client.get("/west") assert response.status_code == HTTP_401_UNAUTHORIZED def test_jwt_auth_openapi() -> None: jwt_auth = JWTAuth[Any](token_secret="abc123", retrieve_user_handler=lambda _: None) # type: ignore[arg-type, misc] assert jwt_auth.openapi_components.to_schema() == { "schemas": {}, "securitySchemes": { "BearerToken": { "type": "http", "description": "JWT api-key authentication and authorization.", "name": "Authorization", "scheme": "Bearer", "bearerFormat": "JWT", } }, } assert jwt_auth.security_requirement == {"BearerToken": []} app = Litestar(on_app_init=[jwt_auth.on_app_init]) assert app.openapi_schema assert app.openapi_schema.to_schema() == { "openapi": "3.1.0", "info": {"title": "Litestar API", "version": "1.0.0"}, "servers": [{"url": "/"}], "paths": {}, "components": { "schemas": {}, "securitySchemes": { "BearerToken": { "type": "http", "description": "JWT api-key authentication and authorization.", "name": "Authorization", "scheme": "Bearer", "bearerFormat": "JWT", } }, }, "security": [{"BearerToken": []}], } async def test_oauth2_password_bearer_auth_openapi(mock_db: "MemoryStore") -> None: user = UserFactory.build() await mock_db.set(str(user.id), user, 120) # type: ignore[arg-type] async def retrieve_user_handler(token: Token, connection: Any) -> Any: assert connection return await mock_db.get(token.sub) jwt_auth = OAuth2PasswordBearerAuth( token_url="/login", token_secret="abc123", retrieve_user_handler=retrieve_user_handler, # type: ignore[var-annotated] ) @get("/login") def login_handler() -> Response["User"]: return jwt_auth.login(identifier=str(user.id)) @get("/login_custom") def login_custom_handler() -> Response["User"]: return jwt_auth.login(identifier=str(user.id), response_body=user) with create_test_client(route_handlers=[login_custom_handler, login_handler]) as client: response = client.get("/login") response_custom = client.get("/login_custom") assert "access_token" in response.content.decode() assert response.content != response_custom.content assert jwt_auth.openapi_components.to_schema() == { "schemas": {}, "securitySchemes": { "BearerToken": { "type": "oauth2", "description": "OAUTH2 password bearer authentication and authorization.", "name": "Authorization", "in": "header", "scheme": "Bearer", "bearerFormat": "JWT", "flows": {"password": {"tokenUrl": "/login"}}, } }, } assert jwt_auth.security_requirement == {"BearerToken": []} app = Litestar(on_app_init=[jwt_auth.on_app_init]) assert app.openapi_schema.to_schema() == { "openapi": "3.1.0", "info": {"title": "Litestar API", "version": "1.0.0"}, "servers": [{"url": "/"}], "paths": {}, "components": { "schemas": {}, "securitySchemes": { "BearerToken": { "type": "oauth2", "description": "OAUTH2 password bearer authentication and authorization.", "name": "Authorization", "in": "header", "scheme": "Bearer", "bearerFormat": "JWT", "flows": {"password": {"tokenUrl": "/login"}}, } }, }, "security": [{"BearerToken": []}], } def test_type_encoders() -> None: # see: https://github.com/litestar-org/litestar/issues/1136 class CustomUser: def __init__(self, id: str) -> None: self.id = id async def retrieve_user_handler(token: Token, connection: "ASGIConnection[Any, Any, Any, Any]") -> CustomUser: return CustomUser(id=token.sub) jwt_cookie_auth = JWTCookieAuth[User]( retrieve_user_handler=retrieve_user_handler, token_secret="abc1234", exclude=["/"], type_encoders={CustomUser: lambda u: {"id": u.id}}, ) @get() def handler() -> Response[User]: data = CustomUser(id="1") return jwt_cookie_auth.login(identifier=str(data.id), response_body=data) with create_test_client([handler]) as client: response = client.get("/") assert response.status_code == HTTP_201_CREATED async def retrieve_user_handler(token: Token, connection: "ASGIConnection[Any, Any, Any, Any]") -> Any: return User(name="moishe", id=uuid4()) @pytest.mark.parametrize( "config", ( JWTAuth[User]( retrieve_user_handler=retrieve_user_handler, token_secret="abc1234", exclude=["/"], ), JWTCookieAuth[User]( retrieve_user_handler=retrieve_user_handler, token_secret="abc1234", exclude=["/"], ), OAuth2PasswordBearerAuth( token_url="/", exclude=["/"], token_secret="abc123", retrieve_user_handler=retrieve_user_handler ), ), ) def test_returns_token_in_response_when_configured(config: JWTAuth) -> None: @get() def handler() -> Response[User]: return config.login(identifier="123", send_token_as_response_body=True) with create_test_client([handler]) as client: response = client.get("/") assert response.status_code == HTTP_201_CREATED assert isinstance(response.json(), dict) and response.json() @pytest.mark.parametrize( "config", ( JWTAuth[User]( retrieve_user_handler=retrieve_user_handler, token_secret="abc1234", exclude=["/"], ), JWTCookieAuth[User]( retrieve_user_handler=retrieve_user_handler, token_secret="abc1234", exclude=["/"], ), OAuth2PasswordBearerAuth( token_url="/", exclude=["/"], token_secret="abc123", retrieve_user_handler=retrieve_user_handler ), ), ) def test_returns_none_when_response_body_is_none(config: JWTAuth) -> None: @get() def handler() -> Response[User]: return config.login(identifier="123", send_token_as_response_body=True, response_body=None) with create_test_client([handler]) as client: response = client.get("/") assert response.status_code == HTTP_201_CREATED assert response.json() is None async def test_jwt_auth_validation_error_returns_not_authorized() -> None: # if the value of a field has an invalid type, msgspec will raise a 'ValidationError'. # this should still result in a '401' status response async def retrieve_user_handler(token: Token, _: "ASGIConnection") -> Any: return object() token_secret = secrets.token_hex() jwt_auth = JWTAuth[Any]( token_secret=token_secret, retrieve_user_handler=retrieve_user_handler, ) @get("/", middleware=[jwt_auth.middleware]) def handler() -> None: return None header = jwt_auth.format_auth_header( jwt.encode( { "sub": "foo", "exp": (datetime.now() + timedelta(days=1)).timestamp(), "iat": datetime.now().timestamp(), "iss": {"foo": "bar"}, }, key=token_secret, ), ) with create_test_client(route_handlers=[handler]) as client: response = client.get("/", headers={"Authorization": header}) assert response.status_code == 401 @pytest.mark.parametrize( "accepted_issuers, signing_issuer, expected_status_code", [ (["issuer_a"], "issuer_a", 200), (["issuer_a", "issuer_b"], "issuer_a", 200), (["issuer_a", "issuer_b"], "issuer_b", 200), (["issuer_b"], "issuer_a", 401), ], ) @pytest.mark.parametrize("auth_cls", [JWTAuth, JWTCookieAuth, OAuth2PasswordBearerAuth]) async def test_jwt_auth_verify_issuer( auth_cls: Any, accepted_issuers: List[str], signing_issuer: str, expected_status_code: int, ) -> None: async def retrieve_user_handler(token: Token, _: "ASGIConnection") -> Any: return object() token_secret = secrets.token_hex() if auth_cls is OAuth2PasswordBearerAuth: jwt_auth = auth_cls( token_secret=token_secret, retrieve_user_handler=retrieve_user_handler, token_url="http://testserver.local", accepted_issuers=accepted_issuers, ) else: jwt_auth = auth_cls[Any]( token_secret=token_secret, retrieve_user_handler=retrieve_user_handler, accepted_issuers=accepted_issuers, ) @get("/", middleware=[jwt_auth.middleware]) def handler() -> None: return None header = jwt_auth.format_auth_header( jwt_auth.create_token( identifier="foo", token_issuer=signing_issuer, ), ) with create_test_client(route_handlers=[handler]) as client: response = client.get("/", headers={"Authorization": header}) assert response.status_code == expected_status_code @pytest.mark.parametrize( "accepted_audiences, token_audience, expected_status_code", [ (["audience_a"], "audience_a", 200), (["audience_a", "audience_b"], "audience_a", 200), (["audience_a", "audience_b"], "audience_b", 200), (["audience_b"], "audience_a", 401), ], ) @pytest.mark.parametrize("auth_cls", [JWTAuth, JWTCookieAuth, OAuth2PasswordBearerAuth]) async def test_jwt_auth_verify_audience( auth_cls: Any, accepted_audiences: List[str], token_audience: str, expected_status_code: int, ) -> None: async def retrieve_user_handler(token: Token, _: "ASGIConnection") -> Any: return object() token_secret = secrets.token_hex() if auth_cls is OAuth2PasswordBearerAuth: jwt_auth = auth_cls( token_secret=token_secret, retrieve_user_handler=retrieve_user_handler, token_url="http://testserver.local", accepted_audiences=accepted_audiences, ) else: jwt_auth = auth_cls[Any]( token_secret=token_secret, retrieve_user_handler=retrieve_user_handler, accepted_audiences=accepted_audiences, ) @get("/", middleware=[jwt_auth.middleware]) def handler() -> None: return None header = jwt_auth.format_auth_header( jwt_auth.create_token( identifier="foo", token_audience=token_audience, ), ) with create_test_client(route_handlers=[handler]) as client: response = client.get("/", headers={"Authorization": header}) assert response.status_code == expected_status_code CreateJWTApp: TypeAlias = Callable[..., Tuple[JWTAuth, TestClient]] @pytest.fixture() def create_jwt_app(auth_cls: Any, request: pytest.FixtureRequest) -> CreateJWTApp: def create(**kwargs: Any) -> Tuple[JWTAuth, TestClient]: async def retrieve_user_handler(token: Token, _: "ASGIConnection") -> Any: return object() if auth_cls is OAuth2PasswordBearerAuth: jwt_auth = auth_cls( token_secret=secrets.token_hex(), retrieve_user_handler=retrieve_user_handler, token_url="http://testserver.local", **kwargs, ) else: jwt_auth = auth_cls[Any]( token_secret=secrets.token_hex(), retrieve_user_handler=retrieve_user_handler, **kwargs ) @get("/", middleware=[jwt_auth.middleware]) def handler() -> None: return None client = create_test_client(route_handlers=[handler]).__enter__() request.addfinalizer(client.__exit__) return jwt_auth, client return create @pytest.fixture(params=[JWTAuth, JWTCookieAuth, OAuth2PasswordBearerAuth]) def auth_cls(request: pytest.FixtureRequest) -> Any: return request.param @pytest.mark.parametrize( "accepted_audiences, token_audience, expected_status_code", [ (["audience_a"], "audience_a", 200), ("audience_a", "audience_a", 200), (["audience_a"], ["audience_a", "audience_b"], 401), (["audience_b"], "audience_a", 401), ], ) async def test_jwt_auth_strict_audience( accepted_audiences: List[str], token_audience: str, expected_status_code: int, create_jwt_app: CreateJWTApp, ) -> None: jwt_auth, client = create_jwt_app(strict_audience=True, accepted_audiences=accepted_audiences) header = jwt_auth.format_auth_header( jwt_auth.create_token( identifier="foo", token_audience=token_audience, ), ) response = client.get("/", headers={"Authorization": header}) assert response.status_code == expected_status_code @pytest.mark.parametrize( "require_claims, token_claims, expected_status_code", [ (["aud"], {"token_audience": "foo"}, 200), (["aud"], {}, 401), ([], {}, 200), ], ) async def test_jwt_auth_require_claims( require_claims: List[str], token_claims: Dict[str, str], expected_status_code: int, create_jwt_app: CreateJWTApp, ) -> None: jwt_auth, client = create_jwt_app(require_claims=require_claims) header = jwt_auth.format_auth_header( jwt_auth.create_token( identifier="foo", **token_claims, # type: ignore[arg-type] ), ) response = client.get("/", headers={"Authorization": header}) assert response.status_code == expected_status_code @pytest.mark.parametrize( "token_expiration, verify_expiry, expected_status_code", [ pytest.param((datetime.now(tz=timezone.utc) + timedelta(days=1)).timestamp(), True, 200, id="valid-verify"), pytest.param((datetime.now(tz=timezone.utc) + timedelta(days=1)).timestamp(), False, 200, id="valid-no_verify"), pytest.param( (datetime.now(tz=timezone.utc) - timedelta(days=1)).timestamp(), False, 200, id="invalid-no_verify" ), pytest.param((datetime.now(tz=timezone.utc) - timedelta(days=1)).timestamp(), True, 401, id="invalid-verify"), ], ) async def test_jwt_auth_verify_exp( token_expiration: datetime, verify_expiry: bool, expected_status_code: int, create_jwt_app: CreateJWTApp, ) -> None: @dataclasses.dataclass class CustomToken(Token): def __post_init__(self) -> None: pass jwt_auth, client = create_jwt_app(verify_expiry=verify_expiry, token_cls=CustomToken) header = jwt_auth.format_auth_header( CustomToken( sub="foo", exp=token_expiration, ).encode(jwt_auth.token_secret, jwt_auth.algorithm), ) response = client.get("/", headers={"Authorization": header}) assert response.status_code == expected_status_code @pytest.mark.parametrize( "token_nbf, verify_not_before, expected_status_code", [ pytest.param((datetime.now(tz=timezone.utc) - timedelta(days=1)).timestamp(), True, 200, id="valid-verify"), pytest.param((datetime.now(tz=timezone.utc) - timedelta(days=1)).timestamp(), False, 200, id="valid-no_verify"), pytest.param( (datetime.now(tz=timezone.utc) + timedelta(days=1)).timestamp(), False, 200, id="invalid-no_verify" ), pytest.param((datetime.now(tz=timezone.utc) + timedelta(days=1)).timestamp(), True, 401, id="invalid-verify"), ], ) async def test_jwt_auth_verify_nbf( token_nbf: datetime, verify_not_before: bool, expected_status_code: int, create_jwt_app: CreateJWTApp, ) -> None: @dataclasses.dataclass() class CustomToken(Token): nbf: Optional[float] = None jwt_auth, client = create_jwt_app(verify_not_before=verify_not_before, token_cls=CustomToken) header = jwt_auth.format_auth_header(jwt_auth.create_token("foo", nbf=token_nbf)) response = client.get("/", headers={"Authorization": header}) assert response.status_code == expected_status_code litestar-2.16.0/tests/unit/test_security/test_jwt/test_integration.py000066400000000000000000000026321500564371300262640ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from os import environ from typing import Any from uuid import UUID from litestar import Request, Response, get, post from litestar.connection import ASGIConnection from litestar.security.jwt import JWTAuth, Token from litestar.status_codes import HTTP_204_NO_CONTENT from litestar.testing import create_test_client @dataclass class User: id: UUID name: str email: str MOCK_DB: dict[str, User] = {} async def retrieve_user_handler(token: Token, connection: ASGIConnection[Any, Any, Any, Any]) -> User | None: return MOCK_DB.get(token.sub) jwt_auth = JWTAuth[User]( retrieve_user_handler=retrieve_user_handler, token_secret=environ.get("JWT_SECRET", "abcd123"), exclude=["/login", "/schema"], ) @post("/login") async def login_handler(data: User) -> Response[User]: MOCK_DB[str(data.id)] = data return jwt_auth.login(identifier=str(data.id), response_body=data) @get("/some-path", sync_to_thread=False) def some_route_handler(request: Request[User, Token, Any]) -> Any: assert isinstance(request.user, User) def test_options_request_with_jwt() -> None: with create_test_client( route_handlers=[login_handler, some_route_handler], on_app_init=[jwt_auth.on_app_init], ) as client: response = client.options("/some-path") assert response.status_code == HTTP_204_NO_CONTENT litestar-2.16.0/tests/unit/test_security/test_jwt/test_token.py000066400000000000000000000165271500564371300250710ustar00rootroot00000000000000from __future__ import annotations import dataclasses import secrets import sys from dataclasses import asdict from datetime import datetime, timedelta, timezone from typing import Any, Sequence from uuid import uuid4 import jwt import pytest from hypothesis import given from hypothesis.strategies import datetimes from litestar.exceptions import ImproperlyConfiguredException, NotAuthorizedException from litestar.security.jwt import Token from litestar.security.jwt.token import JWTDecodeOptions @pytest.mark.parametrize("algorithm", ["HS256", "HS384", "HS512"]) @pytest.mark.parametrize("token_issuer", [None, "e3d7d10edbbc28bfebd8861d39ae7587acde1e1fcefe2cbbec686d235d68f475"]) @pytest.mark.parametrize("token_audience", [None, "627224198b4245ed91cf8353e4ccdf1650728c7ee92748f55fe1e9a9c4d961df"]) @pytest.mark.parametrize( "token_unique_jwt_id", [None, "10f5c6967783ddd6bb0c4e8262d7097caeae64705e45f83275e3c32eee5d30f2"] ) @pytest.mark.parametrize("token_extras", [None, {"email": "test@test.com"}]) def test_token( algorithm: str, token_issuer: str | None, token_audience: str | None, token_unique_jwt_id: str | None, token_extras: dict[str, Any] | None, ) -> None: token_secret = secrets.token_hex() token = Token( sub=secrets.token_hex(), exp=(datetime.now(timezone.utc) + timedelta(minutes=30)), aud=token_audience, iss=token_issuer, jti=token_unique_jwt_id, extras=token_extras or {}, ) encoded_token = token.encode(secret=token_secret, algorithm=algorithm) decoded_token = token.decode(encoded_token=encoded_token, secret=token_secret, algorithm=algorithm) assert asdict(token) == asdict(decoded_token) @pytest.mark.parametrize( "algorithm, secret", [ ( "nope", "1", ), ( "HS256", "", ), ( None, None, ), ( "HS256", None, ), ( "", None, ), ( "", "", ), ( "", "1", ), ], ) def test_encode_validation(algorithm: str, secret: str) -> None: with pytest.raises(ImproperlyConfiguredException): Token( sub="123", exp=(datetime.now(timezone.utc) + timedelta(seconds=30)), ).encode(algorithm="nope", secret=secret) def test_decode_validation() -> None: token = Token( sub="123", exp=(datetime.now(timezone.utc) + timedelta(seconds=30)), ) algorithm = "HS256" secret = uuid4().hex encoded_token = token.encode(algorithm=algorithm, secret=secret) token.decode(encoded_token=encoded_token, algorithm=algorithm, secret=secret) with pytest.raises(NotAuthorizedException): token.decode(encoded_token=secret, algorithm=algorithm, secret=secret) with pytest.raises(NotAuthorizedException): token.decode(encoded_token=encoded_token, algorithm="nope", secret=secret) with pytest.raises(NotAuthorizedException): token.decode(encoded_token=encoded_token, algorithm=algorithm, secret=uuid4().hex) @given(exp=datetimes(min_value=datetime(1970, 1, 1), max_value=datetime.now() - timedelta(seconds=1))) def test_exp_validation(exp: datetime) -> None: if sys.platform == "win32" and exp == datetime(1970, 1, 1): # this does not work on windows. see https://bugs.python.org/issue29097 pytest.skip("Skipping because .timestamp is weird on windows sometimes") with pytest.raises(ImproperlyConfiguredException): Token( sub="123", exp=exp, iat=(datetime.now() - timedelta(seconds=30)), ) @given(iat=datetimes(min_value=datetime.now() + timedelta(days=1))) def test_iat_validation(iat: datetime) -> None: if sys.platform == "win32" and iat >= datetime(3000, 1, 1, 0, 0): # this does not work on windows. see https://bugs.python.org/issue29097 pytest.skip("Skipping because .timestamp is weird on windows sometimes") with pytest.raises(ImproperlyConfiguredException): Token( sub="123", iat=iat, exp=(iat + timedelta(seconds=120)), ) def test_sub_validation() -> None: with pytest.raises(ImproperlyConfiguredException): Token( sub="", iat=(datetime.now() - timedelta(seconds=30)), exp=(datetime.now() + timedelta(seconds=120)), ) def test_extra_fields() -> None: raw_token = { "sub": secrets.token_hex(), "iat": datetime.now(timezone.utc), "azp": "extra value", "email": "thetest@test.com", "exp": (datetime.now(timezone.utc) + timedelta(seconds=30)), } token_secret = secrets.token_hex() encoded_token = jwt.encode(payload=raw_token, key=token_secret, algorithm="HS256") token = Token.decode(encoded_token=encoded_token, secret=token_secret, algorithm="HS256") assert "azp" in token.extras assert "email" in token.extras raw_token = { "sub": secrets.token_hex(), "iat": datetime.now(timezone.utc), "exp": (datetime.now(timezone.utc) + timedelta(seconds=30)), } token_secret = secrets.token_hex() encoded_token = jwt.encode(payload=raw_token, key=token_secret, algorithm="HS256") token = Token.decode(encoded_token=encoded_token, secret=token_secret, algorithm="HS256") assert token.extras == {} @pytest.mark.parametrize("audience", [None, ["foo", "bar"]]) def test_strict_aud_with_multiple_audiences_raises(audience: str | list[str]) -> None: with pytest.raises(ValueError, match="When using 'strict_audience=True'"): Token.decode( "", secret="", algorithm="HS256", audience=audience, strict_audience=True, ) @pytest.mark.parametrize("audience", ["foo", ["foo", "bar"]]) def test_strict_aud_with_one_element_sequence(audience: str | list[str]) -> None: # when validating with strict audience, PyJWT requires that the 'audience' parameter # is passed as a string - one element lists are not allowed. Since we allow these # generally, we convert them to a string in this case secret = secrets.token_hex() encoded = Token(exp=datetime.now() + timedelta(days=1), sub="foo", aud="foo").encode(secret, "HS256") Token.decode( encoded, secret=secret, algorithm="HS256", audience=["foo"], strict_audience=True, ) def test_custom_decode_payload() -> None: @dataclasses.dataclass class CustomToken(Token): @classmethod def decode_payload( cls, encoded_token: str, secret: str, algorithms: list[str], issuer: list[str] | None = None, audience: str | Sequence[str] | None = None, options: JWTDecodeOptions | None = None, ) -> Any: payload = super().decode_payload( encoded_token=encoded_token, secret=secret, algorithms=algorithms, ) payload["sub"] = "some-random-value" return payload _secret = secrets.token_hex() encoded = CustomToken(exp=datetime.now() + timedelta(days=1), sub="foo").encode(_secret, "HS256") assert CustomToken.decode(encoded, secret=_secret, algorithm="HS256").sub == "some-random-value" litestar-2.16.0/tests/unit/test_security/test_security.py000066400000000000000000000157751500564371300237610ustar00rootroot00000000000000from typing import TYPE_CHECKING, Any, Dict, Optional import pytest from litestar import get from litestar.di import Provide from litestar.middleware.session.server_side import ( ServerSideSessionBackend, ServerSideSessionConfig, ) from litestar.openapi.config import OpenAPIConfig from litestar.openapi.spec import Components, SecurityScheme from litestar.security.session_auth import SessionAuth from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.connection import ASGIConnection from litestar.handlers.base import BaseRouteHandler def retrieve_user_handler(_: Dict[str, Any], __: "ASGIConnection") -> Any: pass def test_abstract_security_config_sets_guards(session_backend_config_memory: ServerSideSessionConfig) -> None: async def guard(_: "ASGIConnection", __: "BaseRouteHandler") -> None: pass security_config = SessionAuth[Any, ServerSideSessionBackend]( retrieve_user_handler=retrieve_user_handler, session_backend_config=session_backend_config_memory, guards=[guard], ) with create_test_client([], on_app_init=[security_config.on_app_init]) as client: assert client.app.guards def test_abstract_security_config_sets_dependencies(session_backend_config_memory: ServerSideSessionConfig) -> None: security_config = SessionAuth[Any, ServerSideSessionBackend]( retrieve_user_handler=retrieve_user_handler, session_backend_config=session_backend_config_memory, dependencies={"value": Provide(lambda: 13, sync_to_thread=False)}, ) with create_test_client([], on_app_init=[security_config.on_app_init]) as client: assert client.app.dependencies.get("value") @pytest.mark.filterwarnings("ignore:Middleware 'SessionAuthMiddleware' exclude pattern") def test_abstract_security_config_registers_route_handlers( session_backend_config_memory: ServerSideSessionConfig, ) -> None: @get("/") def handler() -> dict: return {"hello": "world"} security_config = SessionAuth[Any, ServerSideSessionBackend]( retrieve_user_handler=retrieve_user_handler, exclude=["/"], session_backend_config=session_backend_config_memory, route_handlers=[handler], ) with create_test_client([], on_app_init=[security_config.on_app_init]) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.json() == {"hello": "world"} @pytest.mark.parametrize( "openapi_config, expected", ( (None, None), ( OpenAPIConfig(title="Litestar API", version="1.0.0"), { "schemas": {}, "securitySchemes": { "sessionCookie": { "type": "apiKey", "description": "Session cookie authentication.", "name": "session", "in": "cookie", } }, }, ), ( OpenAPIConfig( title="Litestar API", version="1.0.0", components=[ Components( security_schemes={ "app": SecurityScheme( type="http", name="test", security_scheme_in="cookie", # pyright: ignore description="test.", ) } ) ], ), { "schemas": {}, "securitySchemes": { "app": {"type": "http", "description": "test.", "name": "test", "in": "cookie"}, "sessionCookie": { "type": "apiKey", "description": "Session cookie authentication.", "name": "session", "in": "cookie", }, }, }, ), ( OpenAPIConfig( title="Litestar API", version="1.0.0", components=Components( security_schemes={ "app": SecurityScheme( type="http", name="test", security_scheme_in="cookie", description="test.", ) } ), ), { "schemas": {}, "securitySchemes": { "sessionCookie": { "type": "apiKey", "description": "Session cookie authentication.", "name": "session", "in": "cookie", }, "app": {"type": "http", "description": "test.", "name": "test", "in": "cookie"}, }, }, ), ), ) def test_abstract_security_config_setting_openapi_components( openapi_config: Optional["OpenAPIConfig"], expected: dict, session_backend_config_memory: ServerSideSessionConfig ) -> None: security_config = SessionAuth[Any, ServerSideSessionBackend]( retrieve_user_handler=retrieve_user_handler, exclude=["/"], session_backend_config=session_backend_config_memory ) with create_test_client([], on_app_init=[security_config.on_app_init], openapi_config=openapi_config) as client: if openapi_config is not None: assert client.app.openapi_schema assert client.app.openapi_config assert client.app.openapi_config.components assert client.app.openapi_config.components.to_schema() == expected else: assert not client.app.openapi_config @pytest.mark.parametrize( "openapi_config, expected", ( (None, None), (OpenAPIConfig(title="Litestar API", version="1.0.0", security=None), [{"sessionCookie": []}]), ( OpenAPIConfig(title="Litestar API", version="1.0.0", security=[{"app": ["a", "b", "c"]}]), [{"app": ["a", "b", "c"]}, {"sessionCookie": []}], ), ), ) def test_abstract_security_config_setting_openapi_security_requirements( openapi_config: Optional[OpenAPIConfig], expected: list, session_backend_config_memory: ServerSideSessionConfig ) -> None: security_config = SessionAuth[Any, ServerSideSessionBackend]( retrieve_user_handler=retrieve_user_handler, exclude=["/"], session_backend_config=session_backend_config_memory ) with create_test_client([], on_app_init=[security_config.on_app_init], openapi_config=openapi_config) as client: if openapi_config is not None: assert client.app.openapi_config assert client.app.openapi_config.security assert client.app.openapi_config.security == expected else: assert not client.app.openapi_config litestar-2.16.0/tests/unit/test_security/test_session_auth.py000066400000000000000000000072201500564371300246000ustar00rootroot00000000000000from typing import TYPE_CHECKING, Any, Dict, Optional from uuid import uuid4 import msgspec from starlette.status import ( HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT, HTTP_401_UNAUTHORIZED, ) from litestar import Litestar, Request, delete, get, post from litestar.middleware.session.server_side import ( ServerSideSessionBackend, ServerSideSessionConfig, ) from litestar.security.session_auth import SessionAuth from litestar.testing import create_test_client from tests.models import User, UserFactory if TYPE_CHECKING: from litestar.connection import ASGIConnection user_instance = UserFactory.build() def retrieve_user_handler(session_data: Dict[str, Any], _: "ASGIConnection") -> Optional[User]: if session_data["id"] == str(user_instance.id): return User(**session_data) return None def test_authentication(session_backend_config_memory: ServerSideSessionConfig) -> None: session_auth = SessionAuth[Any, ServerSideSessionBackend]( retrieve_user_handler=retrieve_user_handler, exclude=["login"], session_backend_config=session_backend_config_memory, ) @post("/login") def login_handler(request: "Request[Any, Any, Any]", data: User) -> None: request.set_session(msgspec.to_builtins(data)) @delete("/user/{user_id:str}") def delete_user_handler(request: "Request[User, Any, Any]") -> None: request.clear_session() @get("/user/{user_id:str}") def get_user_handler(request: "Request[User, Any, Any]") -> User: return request.user with create_test_client( route_handlers=[login_handler, delete_user_handler, get_user_handler], on_app_init=[session_auth.on_app_init], ) as client: response = client.get(f"user/{user_instance.id}") assert response.status_code == HTTP_401_UNAUTHORIZED, response.json() response = client.post("/login", json={"id": str(user_instance.id), "name": user_instance.name}) assert response.status_code == HTTP_201_CREATED, response.json() response = client.get(f"user/{user_instance.id}") assert response.status_code == HTTP_200_OK, response.json() response = client.delete(f"user/{user_instance.id}") assert response.status_code == HTTP_204_NO_CONTENT, response.json() response = client.get(f"user/{user_instance.id}") assert response.status_code == HTTP_401_UNAUTHORIZED, response.json() response = client.post("/login", json={"id": str(uuid4()), "name": user_instance.name}) assert response.status_code == HTTP_201_CREATED, response.json() response = client.get(f"user/{user_instance.id}") assert response.status_code == HTTP_401_UNAUTHORIZED, response.json() def test_session_auth_openapi(session_backend_config_memory: "ServerSideSessionConfig") -> None: session_auth = SessionAuth[Any, ServerSideSessionBackend]( retrieve_user_handler=retrieve_user_handler, session_backend_config=session_backend_config_memory, ) app = Litestar(on_app_init=[session_auth.on_app_init]) assert app.openapi_schema.to_schema() == { "openapi": "3.1.0", "info": {"title": "Litestar API", "version": "1.0.0"}, "servers": [{"url": "/"}], "paths": {}, "components": { "schemas": {}, "securitySchemes": { "sessionCookie": { "type": "apiKey", "description": "Session cookie authentication.", "name": session_backend_config_memory.key, "in": "cookie", } }, }, "security": [{"sessionCookie": []}], } litestar-2.16.0/tests/unit/test_signature/000077500000000000000000000000001500564371300206145ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_signature/__init__.py000066400000000000000000000000001500564371300227130ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_signature/test_parsing.py000066400000000000000000000153601500564371300236750ustar00rootroot00000000000000from dataclasses import dataclass from types import ModuleType from typing import Any, Callable, Iterable, List, Optional, Sequence, Union from unittest.mock import MagicMock import msgspec import pytest from typing_extensions import Annotated from litestar import get from litestar._signature import SignatureModel from litestar.dto import DataclassDTO from litestar.params import Body, Parameter from litestar.status_codes import HTTP_200_OK, HTTP_204_NO_CONTENT from litestar.testing import TestClient, create_test_client from litestar.types import Empty from litestar.utils.signature import ParsedSignature def test_create_function_signature_model_parameter_parsing() -> None: @get() def my_fn(a: int, b: str, c: Optional[bytes], d: bytes = b"123", e: Optional[dict] = None) -> None: pass model = SignatureModel.create( dependency_name_set=set(), fn=my_fn.fn, data_dto=None, parsed_signature=ParsedSignature.from_fn(my_fn.fn, {}), type_decoders=[], ) fields = model._fields assert fields["a"].annotation is int assert not fields["a"].is_optional assert fields["b"].annotation is str assert not fields["b"].is_optional assert fields["c"].annotation is Optional[bytes] assert fields["c"].is_optional assert fields["c"].default is Empty assert fields["d"].annotation is bytes assert fields["d"].default == b"123" assert fields["e"].annotation == Optional[dict] assert fields["e"].is_optional assert fields["e"].default is None def test_create_function_signature_model_ignore_return_annotation() -> None: @get(path="/health", status_code=HTTP_204_NO_CONTENT) async def health_check() -> None: return None signature_model_type = SignatureModel.create( dependency_name_set=set(), fn=health_check.fn, data_dto=None, parsed_signature=ParsedSignature.from_fn(health_check.fn, {}), type_decoders=[], ) assert signature_model_type().to_dict() == {} def test_signature_model_resolves_forward_ref_annotations(create_module: Callable[[str], ModuleType]) -> None: module = create_module( """ from __future__ import annotations from msgspec import Struct from litestar import Litestar, get from litestar.di import Provide class Test(Struct): hello: str async def get_dep() -> Test: return Test(hello="world") @get("/", dependencies={"test": Provide(get_dep)}) def hello_world(test: Test) -> Test: return test app = Litestar(route_handlers=[hello_world], openapi_config=None) """ ) with TestClient(app=module.app) as client: response = client.get("/") assert response.status_code == 200 assert response.json() == {"hello": "world"} @pytest.mark.parametrize(("query", "exp"), [("?a=1&a=2&a=3", [1, 2, 3]), ("", None)]) def test_parse_optional_sequence_from_connection_kwargs(query: str, exp: Any) -> None: @get("/") def test(a: Optional[List[int]] = Parameter(query="a", default=None, required=False)) -> Optional[List[int]]: return a with create_test_client(route_handlers=[test]) as client: response = client.get(f"/{query}") assert response.status_code == HTTP_200_OK, response.json() assert response.json() == exp def test_field_definition_is_non_string_iterable() -> None: def fn(a: Iterable[int], b: Optional[Iterable[int]]) -> None: pass model = SignatureModel.create( dependency_name_set=set(), fn=fn, data_dto=None, parsed_signature=ParsedSignature.from_fn(fn, {}), type_decoders=[], ) assert model._fields["a"].is_non_string_iterable assert model._fields["b"].is_non_string_iterable def test_field_definition_is_non_string_sequence() -> None: def fn(a: Sequence[int], b: Optional[Sequence[int]]) -> None: pass model = SignatureModel.create( dependency_name_set=set(), fn=fn, data_dto=None, parsed_signature=ParsedSignature.from_fn(fn, signature_namespace={}), type_decoders=[], ) assert model._fields["a"].is_non_string_sequence assert model._fields["b"].is_non_string_sequence @pytest.mark.parametrize("query,expected", [("1", True), ("true", True), ("0", False), ("false", False)]) def test_query_param_bool(query: str, expected: bool) -> None: mock = MagicMock() @get("/") def handler(param: bool) -> None: mock(param) with create_test_client(route_handlers=[handler]) as client: response = client.get(f"/?param={query}") assert response.status_code == HTTP_200_OK, response.json() mock.assert_called_once_with(expected) def test_union_constraint_handling() -> None: mock = MagicMock() @get("/") def handler(param: Annotated[Union[str, List[str]], Body(max_length=3, max_items=3)]) -> None: mock(param) with create_test_client([handler]) as client: response = client.get("/?param=foo") assert response.status_code == 200 mock.assert_called_once_with("foo") @pytest.mark.parametrize(("with_optional",), [(True,), (False,)]) def test_collection_union_struct_fields(with_optional: bool) -> None: """Test consistent behavior between optional and non-optional collection unions. Issue: https://github.com/litestar-org/litestar/issues/2600 identified that where a union of collection types was optional, it would result in a 400 error when the handler was called, whereas a non-optional union would result in a 500 error. This test ensures that both optional and non-optional unions of collection types result in the same error. """ annotation = Union[List[str], List[int]] if with_optional: annotation = Optional[annotation] # type: ignore[misc] @get("/", signature_namespace={"annotation": annotation}) def handler(param: annotation) -> None: # pyright: ignore return None with create_test_client([handler]) as client: response = client.get("/?param=foo¶m=bar¶m=123") assert response.status_code == 500 assert "TypeError: Type unions may not contain more than one array-like" in response.text def test_dto_data_typed_as_any() -> None: """DTOs already validate the payload, we don't need the signature model to do it too. https://github.com/litestar-org/litestar/issues/2149 """ @dataclass class Test: a: str dto = DataclassDTO[Test] def fn(data: Test) -> None: pass model = SignatureModel.create( dependency_name_set=set(), fn=fn, data_dto=dto, parsed_signature=ParsedSignature.from_fn(fn, signature_namespace={"Test": Test}), type_decoders=[], ) (field,) = msgspec.structs.fields(model) assert field.name == "data" assert field.type is Any litestar-2.16.0/tests/unit/test_signature/test_validation.py000066400000000000000000000257311500564371300243670ustar00rootroot00000000000000from dataclasses import dataclass from typing import Generic, List, Optional, TypeVar import pytest from attr import define from typing_extensions import Annotated, TypedDict from litestar import get, post from litestar._signature import SignatureModel from litestar.di import Provide from litestar.enums import ParamType from litestar.exceptions import ImproperlyConfiguredException, ValidationException from litestar.params import Dependency, Parameter from litestar.status_codes import HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR from litestar.testing import RequestFactory, create_test_client from litestar.utils.signature import ParsedSignature def test_parses_values_from_connection_kwargs_raises() -> None: def fn(a: int) -> None: pass model = SignatureModel.create( dependency_name_set=set(), fn=fn, data_dto=None, parsed_signature=ParsedSignature.from_fn(fn, {}), type_decoders=[], ) with pytest.raises(ValidationException): model.parse_values_from_connection_kwargs(connection=RequestFactory().get(), kwargs={"a": "not an int"}) def test_create_signature_validation() -> None: @get() def my_fn(typed: int, untyped) -> None: # type: ignore[no-untyped-def] pass with pytest.raises(ImproperlyConfiguredException): SignatureModel.create( dependency_name_set=set(), fn=my_fn.fn, data_dto=None, parsed_signature=ParsedSignature.from_fn(my_fn.fn, {}), type_decoders=[], ) def test_dependency_validation_failure_raises_500() -> None: dependencies = {"dep": Provide(lambda: "thirteen", sync_to_thread=False)} @get("/") def test(dep: int, param: int, optional_dep: Optional[int] = Dependency()) -> None: ... with create_test_client( route_handlers=[test], dependencies=dependencies, debug=False, ) as client: response = client.get("/?param=13") assert response.json() == {"detail": "Internal Server Error", "status_code": HTTP_500_INTERNAL_SERVER_ERROR} def test_validation_failure_raises_400() -> None: dependencies = {"dep": Provide(lambda: 13, sync_to_thread=False)} @get("/") def test(dep: int, param: int, optional_dep: Optional[int] = Dependency()) -> None: ... with create_test_client(route_handlers=[test], dependencies=dependencies) as client: response = client.get("/?param=thirteen") assert response.json() == { "detail": "Validation failed for GET /?param=thirteen", "extra": [{"key": "param", "message": "Expected `int`, got `str`", "source": "query"}], "status_code": 400, } def test_invalid_path_parameter() -> None: @get("/{param:int}") def test(param: Annotated[int, Parameter(le=10)]) -> None: ... with create_test_client(route_handlers=[test]) as client: response = client.get("/11") assert response.json() == { "detail": "Validation failed for GET /11", "extra": [{"key": "param", "message": "Expected `int` <= 10", "source": "path"}], "status_code": 400, } def test_client_backend_error_precedence_over_server_error() -> None: dependencies = { "dep": Provide(lambda: "thirteen", sync_to_thread=False), "optional_dep": Provide(lambda: "thirty-one", sync_to_thread=False), } @get("/") def test(dep: int, param: int, optional_dep: Optional[int] = Dependency()) -> None: ... with create_test_client(route_handlers=[test], dependencies=dependencies) as client: response = client.get("/?param=thirteen") assert response.json() == { "detail": "Validation failed for GET /?param=thirteen", "extra": [{"key": "param", "message": "Expected `int`, got `str`", "source": "query"}], "status_code": 400, } def test_validation_error_exception_key() -> None: from dataclasses import dataclass @dataclass class OtherChild: val: List[int] @dataclass class Child: val: int other_val: int @dataclass class Parent: child: Child other_child: OtherChild @get("/") def handler(data: Parent) -> None: pass model = SignatureModel.create( dependency_name_set=set(), fn=handler, data_dto=None, parsed_signature=ParsedSignature.from_fn(handler.fn, {}), type_decoders=[], ) with pytest.raises(ValidationException) as exc_info: model.parse_values_from_connection_kwargs( connection=RequestFactory().get(route_handler=handler), kwargs={"data": {"child": {}, "other_child": {}}} ) assert isinstance(exc_info.value.extra, list) assert exc_info.value.extra[0]["key"] == "child" def test_invalid_input_attrs() -> None: @define class OtherChild: val: List[int] @define class Child: val: int other_val: int @define class Parent: child: Child other_child: OtherChild @post("/") def test( data: Parent, int_param: int, int_header: int = Parameter(header="X-SOME-INT"), int_cookie: int = Parameter(cookie="int-cookie"), ) -> None: ... with create_test_client(route_handlers=[test]) as client: client.cookies.update({"int-cookie": "cookie"}) response = client.post( "/", json={"child": {"val": "a", "other_val": "b"}, "other_child": {"val": [1, "c"]}}, params={"int_param": "param"}, headers={"X-SOME-INT": "header"}, ) assert response.status_code == HTTP_400_BAD_REQUEST data = response.json() assert data assert data["extra"] == [ {"message": "Expected `int`, got `str`", "key": "child.val", "source": "body"}, {"message": "Expected `int`, got `str`", "key": "int_param", "source": "query"}, {"message": "Expected `int`, got `str`", "key": "int_header", "source": "header"}, {"message": "Expected `int`, got `str`", "key": "int_cookie", "source": "cookie"}, ] def test_invalid_input_dataclass() -> None: @dataclass class OtherChild: val: List[int] @dataclass class Child: val: int other_val: int @dataclass class Parent: child: Child other_child: OtherChild @post("/") def test( data: Parent, int_param: int, length_param: str = Parameter(min_length=2), int_header: int = Parameter(header="X-SOME-INT"), int_cookie: int = Parameter(cookie="int-cookie"), ) -> None: ... with create_test_client(route_handlers=[test]) as client: client.cookies.update({"int-cookie": "cookie"}) response = client.post( "/", json={"child": {"val": "a", "other_val": "b"}, "other_child": {"val": [1, "c"]}}, params={"int_param": "param", "length_param": "d"}, headers={"X-SOME-INT": "header"}, ) assert response.status_code == HTTP_400_BAD_REQUEST data = response.json() assert data assert data["extra"] == [ {"message": "Expected `int`, got `str`", "key": "child.val", "source": "body"}, {"message": "Expected `int`, got `str`", "key": "int_param", "source": "query"}, {"message": "Expected `str` of length >= 2", "key": "length_param", "source": "query"}, {"message": "Expected `int`, got `str`", "key": "int_header", "source": "header"}, {"message": "Expected `int`, got `str`", "key": "int_cookie", "source": "cookie"}, ] def test_invalid_input_typed_dict() -> None: class OtherChild(TypedDict): val: List[int] class Child(TypedDict): val: int other_val: int class Parent(TypedDict): child: Child other_child: OtherChild @post("/") def test( data: Parent, int_param: int, length_param: str = Parameter(min_length=2), int_header: int = Parameter(header="X-SOME-INT"), int_cookie: int = Parameter(cookie="int-cookie"), ) -> None: ... with create_test_client(route_handlers=[test]) as client: client.cookies.update({"int-cookie": "cookie"}) response = client.post( "/", json={"child": {"val": "a", "other_val": "b"}, "other_child": {"val": [1, "c"]}}, params={"int_param": "param", "length_param": "d"}, headers={"X-SOME-INT": "header"}, ) assert response.status_code == HTTP_400_BAD_REQUEST data = response.json() assert data assert data["extra"] == [ {"message": "Expected `int`, got `str`", "key": "child.val", "source": "body"}, {"message": "Expected `int`, got `str`", "key": "int_param", "source": "query"}, {"message": "Expected `str` of length >= 2", "key": "length_param", "source": "query"}, {"message": "Expected `int`, got `str`", "key": "int_header", "source": "header"}, {"message": "Expected `int`, got `str`", "key": "int_cookie", "source": "cookie"}, ] def test_parse_values_from_connection_kwargs_with_multiple_errors() -> None: def fn(a: Annotated[int, Parameter(gt=5)], b: Annotated[int, Parameter(lt=5)]) -> None: pass model = SignatureModel.create( dependency_name_set=set(), fn=fn, data_dto=None, parsed_signature=ParsedSignature.from_fn(fn, {}), type_decoders=[], ) with pytest.raises(ValidationException) as exc: model.parse_values_from_connection_kwargs(connection=RequestFactory().get(), kwargs={"a": 0, "b": 9}) assert exc.value.extra == [ {"message": "Expected `int` >= 6", "key": "a", "source": ParamType.QUERY}, {"message": "Expected `int` <= 4", "key": "b", "source": ParamType.QUERY}, ] def test_validate_subscribed_generics() -> None: T = TypeVar("T") class Foo(Generic[T]): pass @get("/") async def something(foo: Foo[str] = Foo()) -> None: return None with create_test_client([something]) as client: assert client.get("/").status_code == 200 def test_separate_model_namespace() -> None: # https://github.com/litestar-org/litestar/issues/3593 async def provide_connection() -> str: return "connection" @get("/connection", dependencies={"connection": provide_connection}) async def get_connection(connection: str) -> str: return connection async def provide_deserializer() -> str: return "deserializer" @get("/deserializer", dependencies={"deserializer": provide_deserializer}) async def get_deserializer(deserializer: int) -> str: return deserializer # type: ignore[return-value] with create_test_client([get_connection, get_deserializer], raise_server_exceptions=True, debug=True) as client: assert client.get("/connection").text == "connection" res = client.get("/deserializer") assert res.status_code == 500 assert "Expected `int`, got `str` - at `$.deserializer`" in res.text litestar-2.16.0/tests/unit/test_static_files/000077500000000000000000000000001500564371300212645ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_static_files/__init__.py000066400000000000000000000000001500564371300233630ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_static_files/conftest.py000066400000000000000000000021121500564371300234570ustar00rootroot00000000000000from __future__ import annotations from dataclasses import asdict from typing import Callable import pytest from _pytest.fixtures import FixtureRequest from fsspec.implementations.local import LocalFileSystem from typing_extensions import TypeAlias from litestar import Router from litestar.file_system import BaseLocalFileSystem from litestar.static_files import StaticFilesConfig, create_static_files_router from litestar.types import FileSystemProtocol MakeConfig: TypeAlias = "Callable[[StaticFilesConfig], tuple[list[StaticFilesConfig], list[Router]]]" @pytest.fixture(params=["config", "handlers"]) def make_config(request: FixtureRequest) -> MakeConfig: def make(config: StaticFilesConfig) -> tuple[list[StaticFilesConfig], list[Router]]: if request.param == "config": return [config], [] return [], [create_static_files_router(**asdict(config))] return make @pytest.fixture(params=[BaseLocalFileSystem(), LocalFileSystem()]) def file_system(request: FixtureRequest) -> FileSystemProtocol: return request.param # type: ignore[no-any-return] litestar-2.16.0/tests/unit/test_static_files/test_create_static_router.py000066400000000000000000000062401500564371300271110ustar00rootroot00000000000000from pathlib import Path from typing import Any, Optional import pytest from litestar import Litestar, Request, Response, Router from litestar.connection import ASGIConnection from litestar.datastructures import CacheControlHeader from litestar.exceptions import ValidationException from litestar.handlers import BaseRouteHandler from litestar.static_files import create_static_files_router from litestar.status_codes import HTTP_200_OK from litestar.testing.helpers import create_test_client def test_route_reverse() -> None: app = Litestar( route_handlers=[create_static_files_router(path="/static", directories=["something"], name="static")] ) assert app.route_reverse("static", file_path="foo.py") == "/static/foo.py" def test_pass_options() -> None: def guard(connection: ASGIConnection, handler: BaseRouteHandler) -> None: pass def handle(request: Request, exception: Any) -> Response: return Response(b"") async def after_request(response: Response) -> Response: return Response(b"") async def after_response(request: Request) -> None: pass async def before_request(request: Request) -> Any: pass exception_handlers = {ValidationException: handle} opts = {"foo": "bar"} cache_control = CacheControlHeader() security = [{"foo": ["bar"]}] tags = ["static", "random"] router = create_static_files_router( path="/", directories=["something"], guards=[guard], exception_handlers=exception_handlers, # type: ignore[arg-type] opt=opts, after_request=after_request, after_response=after_response, before_request=before_request, cache_control=cache_control, include_in_schema=False, security=security, tags=tags, ) assert router.guards == [guard] assert router.exception_handlers == exception_handlers assert router.opt == opts assert router.after_request is after_request assert router.after_response is after_response assert router.before_request is before_request assert router.cache_control is cache_control assert router.include_in_schema is False assert router.security == security assert router.tags == tags def test_custom_router_class() -> None: class MyRouter(Router): pass router = create_static_files_router("/", directories=["some"], router_class=MyRouter) assert isinstance(router, MyRouter) @pytest.mark.parametrize("cache_control", (None, CacheControlHeader(max_age=3600))) def test_cache_control(tmp_path: Path, cache_control: Optional[CacheControlHeader]) -> None: static_dir = tmp_path / "foo" static_dir.mkdir() static_dir.joinpath("test.txt").write_text("hello") router = create_static_files_router("/static", [static_dir], name="static", cache_control=cache_control) with create_test_client([router]) as client: response = client.get("static/test.txt") assert response.status_code == HTTP_200_OK if cache_control is not None: assert response.headers["cache-control"] == cache_control.to_header() else: assert "cache-control" not in response.headers litestar-2.16.0/tests/unit/test_static_files/test_file_serving_resolution.py000066400000000000000000000310041500564371300276320ustar00rootroot00000000000000from __future__ import annotations import gzip import mimetypes from pathlib import Path from typing import TYPE_CHECKING, Callable import brotli import pytest from typing_extensions import TypeAlias from litestar import MediaType, Router, get from litestar.static_files import StaticFiles, StaticFilesConfig, create_static_files_router from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from tests.unit.test_static_files.conftest import MakeConfig if TYPE_CHECKING: from litestar.types import FileSystemProtocol def test_default_static_files_config(tmpdir: Path, make_config: MakeConfig) -> None: path = tmpdir / "test.txt" path.write_text("content", "utf-8") static_files_config, router = make_config(StaticFilesConfig(path="/static", directories=[tmpdir])) with create_test_client(router, static_files_config=static_files_config) as client: response = client.get("/static/test.txt") assert response.status_code == HTTP_200_OK, response.text assert response.text == "content" @pytest.fixture() def setup_dirs(tmpdir: Path) -> tuple[Path, Path]: paths = [] for i in range(1, 3): root = tmpdir / str(i) root.mkdir() file_path = root / f"test_{i}.txt" file_path.write_text(f"content{i}", "utf-8") paths.append(root) return paths[0], paths[1] MakeConfigs: TypeAlias = ( "Callable[[StaticFilesConfig, StaticFilesConfig], tuple[list[StaticFilesConfig], list[Router]]]" ) @pytest.fixture() def make_configs(make_config: MakeConfig) -> MakeConfigs: def make( first_config: StaticFilesConfig, second_config: StaticFilesConfig ) -> tuple[list[StaticFilesConfig], list[Router]]: configs_1, routers_1 = make_config(first_config) configs_2, routers_2 = make_config(second_config) return [*configs_1, *configs_2], [*routers_1, *routers_2] return make def test_multiple_static_files_configs(setup_dirs: tuple[Path, Path], make_configs: MakeConfigs) -> None: root1, root2 = setup_dirs configs, handlers = make_configs( StaticFilesConfig(path="/static_first", directories=[root1]), # pyright: ignore StaticFilesConfig(path="/static_second", directories=[root2]), # pyright: ignore ) with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/static_first/test_1.txt") assert response.status_code == HTTP_200_OK assert response.text == "content1" response = client.get("/static_second/test_2.txt") assert response.status_code == HTTP_200_OK assert response.text == "content2" def test_static_files_configs_with_mixed_file_systems( file_system: FileSystemProtocol, setup_dirs: tuple[Path, Path], make_configs: MakeConfigs ) -> None: root1, root2 = setup_dirs configs, handlers = make_configs( StaticFilesConfig(path="/static_first", directories=[root1], file_system=file_system), # pyright: ignore StaticFilesConfig(path="/static_second", directories=[root2]), # pyright: ignore ) with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/static_first/test_1.txt") assert response.status_code == HTTP_200_OK assert response.text == "content1" response = client.get("/static_second/test_2.txt") assert response.status_code == HTTP_200_OK assert response.text == "content2" def test_static_files_config_with_multiple_directories( file_system: FileSystemProtocol, setup_dirs: tuple[Path, Path], make_config: MakeConfig ) -> None: root1, root2 = setup_dirs configs, handlers = make_config( StaticFilesConfig(path="/static", directories=[root1, root2], file_system=file_system) ) with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/static/test_1.txt") assert response.status_code == HTTP_200_OK assert response.text == "content1" response = client.get("/static/test_2.txt") assert response.status_code == HTTP_200_OK assert response.text == "content2" def test_staticfiles_for_slash_path_regular_mode(tmpdir: Path, make_config: MakeConfig) -> None: path = tmpdir / "text.txt" path.write_text("content", "utf-8") configs, handlers = make_config(StaticFilesConfig(path="/", directories=[tmpdir])) with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/text.txt") assert response.status_code == HTTP_200_OK assert response.text == "content" def test_staticfiles_for_slash_path_html_mode(tmpdir: Path, make_config: MakeConfig) -> None: path = tmpdir / "index.html" path.write_text("", "utf-8") configs, handlers = make_config(StaticFilesConfig(path="/", directories=[tmpdir], html_mode=True)) with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "" def test_sub_path_under_static_path(tmpdir: Path, make_config: MakeConfig) -> None: path = tmpdir / "test.txt" path.write_text("content", "utf-8") @get("/static/sub/{f:str}", media_type=MediaType.TEXT) def handler(f: str) -> str: return f configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[tmpdir])) handlers.append(handler) # type: ignore[arg-type] with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/static/test.txt") assert response.status_code == HTTP_200_OK response = client.get("/static/sub/abc") assert response.status_code == HTTP_200_OK def test_static_substring_of_self(tmpdir: Path, make_config: MakeConfig) -> None: path = tmpdir.mkdir("static_part").mkdir("static") / "test.txt" # type: ignore[arg-type, func-returns-value] path.write_text("content", "utf-8") configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[tmpdir])) with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/static/static_part/static/test.txt") assert response.status_code == HTTP_200_OK assert response.text == "content" @pytest.mark.parametrize("extension", ["css", "js", "html", "json"]) def test_static_files_response_mimetype(tmpdir: Path, extension: str, make_config: MakeConfig) -> None: fn = f"test.{extension}" path = tmpdir / fn path.write_text("content", "utf-8") configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[tmpdir])) expected_mime_type = mimetypes.guess_type(fn)[0] with create_test_client(handlers, static_files_config=configs) as client: response = client.get(f"/static/{fn}") assert expected_mime_type assert response.status_code == HTTP_200_OK assert response.headers["content-type"].startswith(expected_mime_type) @pytest.mark.parametrize("extension", ["gz", "br"]) def test_static_files_response_encoding(tmp_path: Path, extension: str, make_config: MakeConfig) -> None: fn = f"test.js.{extension}" path = tmp_path / fn compressed_data = None if extension == "br": compressed_data = brotli.compress(b"content") elif extension == "gz": compressed_data = gzip.compress(b"content") path.write_bytes(compressed_data) # type: ignore[arg-type] expected_encoding_type = mimetypes.guess_type(fn)[1] configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[tmp_path])) with create_test_client(handlers, static_files_config=configs) as client: response = client.get(f"/static/{fn}") assert expected_encoding_type assert response.status_code == HTTP_200_OK assert response.headers["content-encoding"].startswith(expected_encoding_type) @pytest.mark.parametrize("send_as_attachment,disposition", [(True, "attachment"), (False, "inline")]) def test_static_files_content_disposition( tmpdir: Path, send_as_attachment: bool, disposition: str, make_config: MakeConfig ) -> None: path = tmpdir.mkdir("static_part").mkdir("static") / "test.txt" # type: ignore[arg-type, func-returns-value] path.write_text("content", "utf-8") configs, handlers = make_config( StaticFilesConfig(path="/static", directories=[tmpdir], send_as_attachment=send_as_attachment) ) with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/static/static_part/static/test.txt") assert response.status_code == HTTP_200_OK assert response.headers["content-disposition"].startswith(disposition) def test_service_from_relative_path_using_string(tmpdir: Path, make_config: MakeConfig) -> None: sub_dir = Path(tmpdir.mkdir("low")).resolve() # type: ignore[arg-type, func-returns-value] path = tmpdir / "test.txt" path.write_text("content", "utf-8") configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[f"{sub_dir}/.."])) with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/static/test.txt") assert response.status_code == HTTP_200_OK assert response.text == "content" def test_service_from_relative_path_using_path(tmpdir: Path, make_config: MakeConfig) -> None: sub_dir = Path(tmpdir.mkdir("low")).resolve() # type: ignore[arg-type, func-returns-value] path = tmpdir / "test.txt" path.write_text("content", "utf-8") configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[Path(f"{sub_dir}/..")])) with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/static/test.txt") assert response.status_code == HTTP_200_OK assert response.text == "content" def test_service_from_base_path_using_string(tmpdir: Path) -> None: sub_dir = Path(tmpdir.mkdir("low")).resolve() # type: ignore[arg-type, func-returns-value] path = tmpdir / "test.txt" path.write_text("content", "utf-8") @get("/", media_type=MediaType.TEXT) def index_handler() -> str: return "index" @get("/sub") def sub_handler() -> dict: return {"hello": "world"} static_files_config = StaticFilesConfig(path="/", directories=[f"{sub_dir}/.."]) with create_test_client([index_handler, sub_handler], static_files_config=[static_files_config]) as client: response = client.get("/test.txt") assert response.status_code == HTTP_200_OK assert response.text == "content" response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "index" response = client.get("/sub") assert response.status_code == HTTP_200_OK assert response.json() == {"hello": "world"} @pytest.mark.parametrize("resolve", [True, False]) def test_resolve_symlinks(tmp_path: Path, resolve: bool) -> None: source_dir = tmp_path / "foo" source_dir.mkdir() linked_dir = tmp_path / "bar" linked_dir.symlink_to(source_dir, target_is_directory=True) source_dir.joinpath("test.txt").write_text("hello") router = create_static_files_router(path="/", directories=[linked_dir], resolve_symlinks=resolve) with create_test_client(router) as client: if not resolve: linked_dir.unlink() assert client.get("/test.txt").status_code == 404 else: assert client.get("/test.txt").status_code == 200 async def test_staticfiles_get_fs_info_no_access_to_non_static_directory( tmp_path: Path, file_system: FileSystemProtocol, ) -> None: assets = tmp_path / "assets" assets.mkdir() index = tmp_path / "index.html" index.write_text("content", "utf-8") static_files = StaticFiles(is_html_mode=False, directories=[assets], file_system=file_system) path, info = await static_files.get_fs_info([assets], "../index.html") assert path is None assert info is None async def test_staticfiles_get_fs_info_no_access_to_non_static_file_with_prefix( tmp_path: Path, file_system: FileSystemProtocol, ) -> None: static = tmp_path / "static" static.mkdir() private_file = tmp_path / "staticsecrets.env" private_file.write_text("content", "utf-8") static_files = StaticFiles(is_html_mode=False, directories=[static], file_system=file_system) path, info = await static_files.get_fs_info([static], "../staticsecrets.env") assert path is None assert info is None litestar-2.16.0/tests/unit/test_static_files/test_html_mode.py000066400000000000000000000046211500564371300246500ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from litestar.static_files import StaticFilesConfig from litestar.status_codes import HTTP_200_OK, HTTP_404_NOT_FOUND from litestar.testing import create_test_client from tests.unit.test_static_files.conftest import MakeConfig if TYPE_CHECKING: from pathlib import Path from litestar.types import FileSystemProtocol def test_staticfiles_is_html_mode(tmpdir: Path, file_system: FileSystemProtocol, make_config: MakeConfig) -> None: path = tmpdir / "index.html" path.write_text("content", "utf-8") static_files_config, handlers = make_config( StaticFilesConfig(path="/static", directories=[tmpdir], html_mode=True, file_system=file_system) ) with create_test_client(handlers, static_files_config=static_files_config) as client: response = client.get("/static") assert response.status_code == HTTP_200_OK assert response.text == "content" assert response.headers["content-type"] == "text/html; charset=utf-8" assert response.headers["content-disposition"].startswith("inline") def test_staticfiles_is_html_mode_serves_404_when_present( tmpdir: Path, file_system: FileSystemProtocol, make_config: MakeConfig ) -> None: path = tmpdir / "404.html" path.write_text("not found", "utf-8") static_files_config, handlers = make_config( StaticFilesConfig(path="/static", directories=[tmpdir], html_mode=True, file_system=file_system) ) with create_test_client(handlers, static_files_config=static_files_config) as client: response = client.get("/static") assert response.status_code == HTTP_404_NOT_FOUND assert response.text == "not found" assert response.headers["content-type"] == "text/html; charset=utf-8" def test_staticfiles_is_html_mode_raises_exception_when_no_404_html_is_present( tmpdir: Path, file_system: FileSystemProtocol, make_config: MakeConfig ) -> None: static_files_config, handlers = make_config( StaticFilesConfig(path="/static", directories=[tmpdir], html_mode=True, file_system=file_system) ) with create_test_client(handlers, static_files_config=static_files_config) as client: response = client.get("/static") assert response.status_code == HTTP_404_NOT_FOUND assert response.json() == {"status_code": 404, "detail": "no file or directory match the path . was found"} litestar-2.16.0/tests/unit/test_static_files/test_static_files_validation.py000066400000000000000000000134211500564371300275610ustar00rootroot00000000000000import asyncio from pathlib import Path from typing import TYPE_CHECKING, Any, List, cast import pytest from litestar import HttpMethod, Litestar, MediaType, get from litestar.exceptions import ImproperlyConfiguredException from litestar.static_files import StaticFilesConfig, create_static_files_router from litestar.status_codes import HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_405_METHOD_NOT_ALLOWED from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.static_files import StaticFiles @pytest.mark.parametrize("directories", [[], [""]]) @pytest.mark.parametrize("func", [StaticFilesConfig, create_static_files_router]) def test_config_validation_of_directories(func: Any, directories: List[str]) -> None: with pytest.raises(ImproperlyConfiguredException): func(path="/static", directories=directories) @pytest.mark.parametrize("func", [StaticFilesConfig, create_static_files_router]) def test_config_validation_of_path(tmpdir: "Path", func: Any) -> None: path = tmpdir / "text.txt" path.write_text("content", "utf-8") with pytest.raises(ImproperlyConfiguredException): func(path="", directories=[tmpdir]) with pytest.raises(ImproperlyConfiguredException): func(path="/{param:int}", directories=[tmpdir]) @pytest.mark.parametrize("func", [StaticFilesConfig, create_static_files_router]) def test_config_validation_of_file_system(tmpdir: "Path", func: Any) -> None: class FSWithoutOpen: def info(self) -> None: return with pytest.raises(ImproperlyConfiguredException): func(path="/static", directories=[tmpdir], file_system=FSWithoutOpen()) class FSWithoutInfo: def open(self) -> None: return with pytest.raises(ImproperlyConfiguredException): func(path="/static", directories=[tmpdir], file_system=FSWithoutInfo()) class ImplementedFS: def info(self) -> None: return def open(self) -> None: return assert func(path="/static", directories=[tmpdir], file_system=ImplementedFS()) def test_runtime_validation_of_static_path_and_path_parameter(tmpdir: "Path") -> None: path = tmpdir / "test.txt" path.write_text("content", "utf-8") @get("/static/{f:str}", media_type=MediaType.TEXT) def handler(f: str) -> str: return f with pytest.raises(ImproperlyConfiguredException): Litestar( route_handlers=[handler], static_files_config=[StaticFilesConfig(path="/static", directories=[tmpdir])] ) @pytest.mark.parametrize( "method, expected", ( (HttpMethod.GET, HTTP_200_OK), (HttpMethod.HEAD, HTTP_200_OK), (HttpMethod.PUT, HTTP_405_METHOD_NOT_ALLOWED), (HttpMethod.PATCH, HTTP_405_METHOD_NOT_ALLOWED), (HttpMethod.POST, HTTP_405_METHOD_NOT_ALLOWED), (HttpMethod.DELETE, HTTP_405_METHOD_NOT_ALLOWED), (HttpMethod.OPTIONS, HTTP_405_METHOD_NOT_ALLOWED), ), ) def test_runtime_validation_of_request_method_legacy_config(tmpdir: "Path", method: HttpMethod, expected: int) -> None: path = tmpdir / "test.txt" path.write_text("content", "utf-8") with create_test_client( [], static_files_config=[StaticFilesConfig(path="/static", directories=[tmpdir])] ) as client: response = client.request(method, "/static/test.txt") assert response.status_code == expected @pytest.mark.parametrize( "method, expected", ( (HttpMethod.GET, HTTP_200_OK), (HttpMethod.HEAD, HTTP_200_OK), (HttpMethod.OPTIONS, HTTP_204_NO_CONTENT), (HttpMethod.PUT, HTTP_405_METHOD_NOT_ALLOWED), (HttpMethod.PATCH, HTTP_405_METHOD_NOT_ALLOWED), (HttpMethod.POST, HTTP_405_METHOD_NOT_ALLOWED), (HttpMethod.DELETE, HTTP_405_METHOD_NOT_ALLOWED), ), ) def test_runtime_validation_of_request_method_create_handler(tmpdir: "Path", method: HttpMethod, expected: int) -> None: path = tmpdir / "test.txt" path.write_text("content", "utf-8") with create_test_client(create_static_files_router(path="/static", directories=[tmpdir])) as client: response = client.request(method, "/static/test.txt") assert response.status_code == expected def test_config_validation_of_path_prevents_directory_traversal(tmpdir: "Path") -> None: # Setup: Create a 'secret.txt' outside the static directory to simulate sensitive file secret_path = Path(tmpdir) / "../secret.txt" secret_path.write_text("This is a secret file.", encoding="utf-8") # Setup: Create 'test.txt' inside the static directory test_file_path = Path(tmpdir) / "test.txt" test_file_path.write_text("This is a test file.", encoding="utf-8") # Get StaticFiles handler config = StaticFilesConfig(path="/static", directories=[tmpdir]) asgi_router = config.to_static_files_app() static_files_handler = cast("StaticFiles", asgi_router.fn) # Resolve file path with the StaticFiles handler string_path = Path("../secret.txt").as_posix() coroutine = static_files_handler.get_fs_info(directories=static_files_handler.directories, file_path=string_path) resolved_path, fs_info = asyncio.run(coroutine) assert resolved_path is None # Because the resolved path is outside the static directory assert fs_info is None # Because the file doesn't exist, so there is no info # Resolve file path with the StaticFiles handler string_path = Path("test.txt").as_posix() coroutine = static_files_handler.get_fs_info(directories=static_files_handler.directories, file_path=string_path) resolved_path, fs_info = asyncio.run(coroutine) expected_resolved_path = tmpdir / "test.txt" assert resolved_path == expected_resolved_path # Because the resolved path is inside the static directory assert fs_info is not None # Because the file exists, so there is info litestar-2.16.0/tests/unit/test_stores.py000066400000000000000000000440371500564371300205140ustar00rootroot00000000000000from __future__ import annotations import asyncio import math import shutil import string from datetime import timedelta from pathlib import Path from typing import TYPE_CHECKING, cast from unittest.mock import MagicMock, Mock, patch import pytest from _pytest.fixtures import FixtureRequest from pytest_mock import MockerFixture from time_machine import Coordinates from litestar.exceptions import ImproperlyConfiguredException from litestar.stores.file import FileStore from litestar.stores.memory import MemoryStore from litestar.stores.redis import RedisStore from litestar.stores.registry import StoreRegistry from litestar.stores.valkey import ValkeyStore if TYPE_CHECKING: from redis.asyncio import Redis from valkey.asyncio import Valkey from litestar.stores.base import NamespacedStore, Store @pytest.fixture() def mock_redis() -> None: patch("litestar.Store.redis_backend.Redis") @pytest.fixture() def mock_valkey() -> None: patch("litestar.Store.valkey_backend.Valkey") async def test_get(store: Store) -> None: key = "key" value = b"value" assert await store.get("foo") is None await store.set(key, value, 60) stored_value = await store.get(key) assert stored_value == value async def test_set(store: Store) -> None: values: dict[str, bytes | str] = {"key_1": b"value_1", "key_2": "value_2"} for key, value in values.items(): await store.set(key, value) for key, value in values.items(): stored_value = await store.get(key) assert stored_value == value if isinstance(value, bytes) else value.encode("utf-8") @pytest.mark.parametrize("key", [*list(string.punctuation), "foo\xc3\xbc", ".."]) async def test_set_special_chars_key(store: Store, key: str) -> None: # ensure that stores handle special chars correctly value = b"value" await store.set(key, value) assert await store.get(key) == value async def test_expires(store: Store, frozen_datetime: Coordinates) -> None: await store.set("foo", b"bar", expires_in=1) frozen_datetime.shift(2) if isinstance(store, RedisStore): # shifting time does not affect the Redis instance # this is done to emulate auto-expiration await store._redis.expire(f"{store.namespace}:foo", 0) if isinstance(store, ValkeyStore): await store._valkey.expire(f"{store.namespace}:foo", 0) stored_value = await store.get("foo") assert stored_value is None @pytest.mark.flaky(reruns=5) @pytest.mark.parametrize("renew_for", [10, timedelta(seconds=10)]) async def test_get_and_renew(store: Store, renew_for: int | timedelta, frozen_datetime: Coordinates) -> None: if isinstance(store, (RedisStore, ValkeyStore)): pytest.skip() await store.set("foo", b"bar", expires_in=1) await store.get("foo", renew_for=renew_for) frozen_datetime.shift(2) stored_value = await store.get("foo") assert stored_value is not None @pytest.mark.flaky(reruns=5) @pytest.mark.parametrize("renew_for", [10, timedelta(seconds=10)]) @pytest.mark.xdist_group("redis") async def test_get_and_renew_redis(redis_store: RedisStore, renew_for: int | timedelta) -> None: # we can't sleep() in frozen datetime, and frozen datetime doesn't affect the redis # instance, so we test this separately await redis_store.set("foo", b"bar", expires_in=1) await redis_store.get("foo", renew_for=renew_for) await asyncio.sleep(1.1) stored_value = await redis_store.get("foo") assert stored_value is not None @pytest.mark.flaky(reruns=5) @pytest.mark.parametrize("renew_for", [10, timedelta(seconds=10)]) @pytest.mark.xdist_group("valkey") async def test_get_and_renew_valkey(valkey_store: ValkeyStore, renew_for: int | timedelta) -> None: # we can't sleep() in frozen datetime, and frozen datetime doesn't affect the redis # instance, so we test this separately await valkey_store.set("foo", b"bar", expires_in=1) await valkey_store.get("foo", renew_for=renew_for) await asyncio.sleep(1.1) stored_value = await valkey_store.get("foo") assert stored_value is not None async def test_delete(store: Store) -> None: key = "key" await store.set(key, b"value", 60) await store.delete(key) value = await store.get(key) assert value is None async def test_delete_empty(store: Store) -> None: # assert that this does not raise an exception await store.delete("foo") async def test_exists(store: Store) -> None: assert await store.exists("foo") is False await store.set("foo", b"bar") assert await store.exists("foo") is True async def test_expires_in_not_set(store: Store) -> None: assert await store.expires_in("foo") is None await store.set("foo", b"bar") assert await store.expires_in("foo") == -1 async def test_delete_all(store: Store) -> None: keys = [] for i in range(10): key = f"key-{i}" keys.append(key) await store.set(key, b"value", expires_in=10 if i % 2 else None) await store.delete_all() for key in keys: assert await store.get(key) is None async def test_expires_in(store: Store, frozen_datetime: Coordinates) -> None: if not isinstance(store, (RedisStore, ValkeyStore)): pytest.xfail("bug in FileStore and MemoryStore") assert await store.expires_in("foo") is None await store.set("foo", "bar") assert await store.expires_in("foo") == -1 await store.set("foo", "bar", expires_in=10) expiration = await store.expires_in("foo") assert expiration is not None assert math.ceil(expiration / 10) == 1 if isinstance(store, RedisStore): await store._redis.expire(f"{store.namespace}:foo", 0) elif isinstance(store, ValkeyStore): await store._valkey.expire(f"{store.namespace}:foo", 0) expiration = await store.expires_in("foo") assert expiration is None @patch("litestar.stores.redis.Redis") @patch("litestar.stores.redis.ConnectionPool.from_url") def test_redis_with_client_default(connection_pool_from_url_mock: Mock, mock_redis: Mock) -> None: backend = RedisStore.with_client() connection_pool_from_url_mock.assert_called_once_with( url="redis://localhost:6379", db=None, port=None, username=None, password=None, decode_responses=False ) mock_redis.assert_called_once_with(connection_pool=connection_pool_from_url_mock.return_value) assert backend._redis is mock_redis.return_value @patch("litestar.stores.valkey.Valkey") @patch("litestar.stores.valkey.ConnectionPool.from_url") def test_valkey_with_client_default(connection_pool_from_url_mock: Mock, mock_valkey: Mock) -> None: backend = ValkeyStore.with_client() connection_pool_from_url_mock.assert_called_once_with( url="valkey://localhost:6379", db=None, port=None, username=None, password=None, decode_responses=False ) mock_valkey.assert_called_once_with(connection_pool=connection_pool_from_url_mock.return_value) assert backend._valkey is mock_valkey.return_value @patch("litestar.stores.redis.Redis") @patch("litestar.stores.redis.ConnectionPool.from_url") def test_redis_with_non_default(connection_pool_from_url_mock: Mock, mock_redis: Mock) -> None: url = "redis://localhost" db = 2 port = 1234 username = "user" password = "password" backend = RedisStore.with_client(url=url, db=db, port=port, username=username, password=password) connection_pool_from_url_mock.assert_called_once_with( url=url, db=db, port=port, username=username, password=password, decode_responses=False ) mock_redis.assert_called_once_with(connection_pool=connection_pool_from_url_mock.return_value) assert backend._redis is mock_redis.return_value @patch("litestar.stores.valkey.Valkey") @patch("litestar.stores.valkey.ConnectionPool.from_url") def test_valkey_with_non_default(connection_pool_from_url_mock: Mock, mock_valkey: Mock) -> None: url = "valkey://localhost" db = 2 port = 1234 username = "user" password = "password" backend = ValkeyStore.with_client(url=url, db=db, port=port, username=username, password=password) connection_pool_from_url_mock.assert_called_once_with( url=url, db=db, port=port, username=username, password=password, decode_responses=False ) mock_valkey.assert_called_once_with(connection_pool=connection_pool_from_url_mock.return_value) assert backend._valkey is mock_valkey.return_value @pytest.mark.xdist_group("redis") async def test_redis_delete_all(redis_store: RedisStore) -> None: await redis_store._redis.set("test_key", b"test_value") keys = [] for i in range(10): key = f"key-{i}" keys.append(key) await redis_store.set(key, b"value", expires_in=10 if i % 2 else None) await redis_store.delete_all() for key in keys: assert await redis_store.get(key) is None stored_value = await redis_store._redis.get("test_key") assert stored_value == b"test_value" # check it doesn't delete other values @pytest.mark.xdist_group("valkey") async def test_valkey_delete_all(valkey_store: ValkeyStore) -> None: await valkey_store._valkey.set("test_key", b"test_value") keys = [] for i in range(10): key = f"key-{i}" keys.append(key) await valkey_store.set(key, b"value", expires_in=10 if i % 2 else None) await valkey_store.delete_all() for key in keys: assert await valkey_store.get(key) is None stored_value = await valkey_store._valkey.get("test_key") assert stored_value == b"test_value" # check it doesn't delete other values @pytest.mark.xdist_group("redis") async def test_redis_delete_all_no_namespace_raises(redis_client: Redis) -> None: redis_store = RedisStore(redis=redis_client, namespace=None) with pytest.raises(ImproperlyConfiguredException): await redis_store.delete_all() @pytest.mark.xdist_group("valkey") async def test_valkey_delete_all_no_namespace_raises(valkey_client: Valkey) -> None: valkey_store = ValkeyStore(valkey=valkey_client, namespace=None) with pytest.raises(ImproperlyConfiguredException): await valkey_store.delete_all() @pytest.mark.xdist_group("redis") def test_redis_namespaced_key(redis_store: RedisStore) -> None: assert redis_store.namespace == "LITESTAR" assert redis_store._make_key("foo") == "LITESTAR:foo" @pytest.mark.xdist_group("valkey") def test_valkey_namespaced_key(valkey_store: ValkeyStore) -> None: assert valkey_store.namespace == "LITESTAR" assert valkey_store._make_key("foo") == "LITESTAR:foo" @pytest.mark.xdist_group("redis") def test_redis_with_namespace(redis_store: RedisStore) -> None: namespaced_test = redis_store.with_namespace("TEST") namespaced_test_foo = namespaced_test.with_namespace("FOO") assert namespaced_test.namespace == "LITESTAR_TEST" assert namespaced_test_foo.namespace == "LITESTAR_TEST_FOO" assert namespaced_test._redis is redis_store._redis @pytest.mark.xdist_group("valkey") def test_valkey_with_namespace(valkey_store: ValkeyStore) -> None: namespaced_test = valkey_store.with_namespace("TEST") namespaced_test_foo = namespaced_test.with_namespace("FOO") assert namespaced_test.namespace == "LITESTAR_TEST" assert namespaced_test_foo.namespace == "LITESTAR_TEST_FOO" assert namespaced_test._valkey is valkey_store._valkey @pytest.mark.xdist_group("redis") def test_redis_namespace_explicit_none(redis_client: Redis) -> None: assert RedisStore.with_client(url="redis://127.0.0.1", namespace=None).namespace is None assert RedisStore(redis=redis_client, namespace=None).namespace is None @pytest.mark.xdist_group("valkey") def test_valkey_namespace_explicit_none(valkey_client: Valkey) -> None: assert ValkeyStore.with_client(url="redis://127.0.0.1", namespace=None).namespace is None assert ValkeyStore(valkey=valkey_client, namespace=None).namespace is None async def test_file_init_directory(file_store: FileStore) -> None: shutil.rmtree(file_store.path) await file_store.set("foo", b"bar") async def test_file_init_subdirectory(file_store_create_directories: FileStore) -> None: file_store = file_store_create_directories async with file_store: await file_store.set("foo", b"bar") async def test_file_init_subdirectory_negative(file_store_create_directories_flag_false: FileStore) -> None: file_store = file_store_create_directories_flag_false async with file_store: with pytest.raises(FileNotFoundError): await file_store.set("foo", b"bar") async def test_file_path(file_store: FileStore) -> None: await file_store.set("foo", b"bar") assert await (file_store.path / "foo").exists() def test_file_with_namespace(file_store: FileStore) -> None: namespaced = file_store.with_namespace("foo") assert namespaced.path == file_store.path / "foo" @pytest.mark.parametrize("invalid_char", string.punctuation) def test_file_with_namespace_invalid_namespace_char(file_store: FileStore, invalid_char: str) -> None: with pytest.raises(ValueError): file_store.with_namespace(f"foo{invalid_char}") @pytest.fixture( params=[ pytest.param("redis_store", marks=pytest.mark.xdist_group("redis")), pytest.param("valkey_store", marks=pytest.mark.xdist_group("valkey")), "file_store", ] ) def namespaced_store(request: FixtureRequest) -> NamespacedStore: return cast("NamespacedStore", request.getfixturevalue(request.param)) async def test_namespaced_store_get_set(namespaced_store: NamespacedStore) -> None: foo_namespaced = namespaced_store.with_namespace("foo") await namespaced_store.set("bar", b"litestar namespace") await foo_namespaced.set("bar", b"foo namespace") assert await namespaced_store.get("bar") == b"litestar namespace" assert await foo_namespaced.get("bar") == b"foo namespace" async def test_namespaced_store_does_not_propagate_up(namespaced_store: NamespacedStore) -> None: foo_namespace = namespaced_store.with_namespace("FOO") await foo_namespace.set("foo", b"foo-value") await namespaced_store.set("bar", b"bar-value") await foo_namespace.delete_all() assert await foo_namespace.get("foo") is None assert await namespaced_store.get("bar") == b"bar-value" async def test_namespaced_store_delete_all_propagates_down(namespaced_store: NamespacedStore) -> None: foo_namespace = namespaced_store.with_namespace("FOO") await foo_namespace.set("foo", b"foo-value") await namespaced_store.set("bar", b"bar-value") await namespaced_store.delete_all() assert await foo_namespace.get("foo") is None assert await namespaced_store.get("bar") is None @pytest.mark.parametrize("store_fixture", ["memory_store", "file_store"]) async def test_memory_delete_expired(store_fixture: str, request: FixtureRequest, frozen_datetime: Coordinates) -> None: store = request.getfixturevalue(store_fixture) expect_expired: list[str] = [] expect_not_expired: list[str] = [] for i in range(10): key = f"key-{i}" expires_in = 0.001 if i % 2 == 0 else None await store.set(key, b"value", expires_in=expires_in) (expect_expired if expires_in else expect_not_expired).append(key) frozen_datetime.shift(1) await store.delete_expired() for key in expect_expired: assert await store.get(key) is None for key in expect_not_expired: assert await store.get(key) is not None def test_registry_get(memory_store: MemoryStore) -> None: default_factory = MagicMock() default_factory.return_value = memory_store registry = StoreRegistry(default_factory=default_factory) default_factory.reset_mock() assert registry.get("foo") is memory_store assert registry.get("foo") is memory_store assert "foo" in registry._stores default_factory.assert_called_once_with("foo") def test_registry_register(memory_store: MemoryStore) -> None: registry = StoreRegistry() registry.register("foo", memory_store) assert registry.get("foo") is memory_store def test_registry_register_exist_raises(memory_store: MemoryStore) -> None: registry = StoreRegistry({"foo": memory_store}) with pytest.raises(ValueError): registry.register("foo", memory_store) def test_registry_register_exist_override(memory_store: MemoryStore) -> None: registry = StoreRegistry({"foo": memory_store}) registry.register("foo", memory_store, allow_override=True) assert registry.get("foo") is memory_store async def test_file_store_handle_rename_fail(file_store: FileStore, mocker: MockerFixture) -> None: mocker.patch("litestar.stores.file.os.replace", side_effect=OSError) mock_unlink = mocker.patch("litestar.stores.file.os.unlink") await file_store.set("foo", "bar") mock_unlink.assert_called_once() assert Path(mock_unlink.call_args_list[0].args[0]).with_suffix("") == file_store.path.joinpath("foo") @pytest.mark.xdist_group("redis") async def test_redis_store_with_client_shutdown(redis_service: None) -> None: redis_store = RedisStore.with_client(url="redis://localhost:6397") assert await redis_store._redis.ping() # remove the private shutdown and the assert below fails # the check on connection is a mimic of https://github.com/redis/redis-py/blob/d529c2ad8d2cf4dcfb41bfd93ea68cfefd81aa66/tests/test_asyncio/test_connection_pool.py#L35-L39 await redis_store._shutdown() assert not any( x.is_connected for x in redis_store._redis.connection_pool._available_connections + list(redis_store._redis.connection_pool._in_use_connections) ) @pytest.mark.xdist_group("valkey") async def test_valkey_store_with_client_shutdown(valkey_service: None) -> None: valkey_store = ValkeyStore.with_client(url="valkey://localhost:6381") assert await valkey_store._valkey.ping() # remove the private shutdown and the assert below fails # the check on connection is a mimic of https://github.com/redis/redis-py/blob/d529c2ad8d2cf4dcfb41bfd93ea68cfefd81aa66/tests/test_asyncio/test_connection_pool.py#L35-L39 await valkey_store._shutdown() assert not any( x.is_connected for x in valkey_store._valkey.connection_pool._available_connections + list(valkey_store._valkey.connection_pool._in_use_connections) ) litestar-2.16.0/tests/unit/test_template/000077500000000000000000000000001500564371300204265ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_template/__init__.py000066400000000000000000000000001500564371300225250ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_template/test_built_in.py000066400000000000000000000144501500564371300236500ustar00rootroot00000000000000from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, Type, Union import pytest from jinja2 import DictLoader, Environment from mako.lookup import TemplateLookup # type: ignore[import-untyped] from litestar import get from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.response.template import Template from litestar.template.config import TemplateConfig from litestar.testing import create_test_client if TYPE_CHECKING: from litestar.handlers.http_handlers import HTTPRouteHandler @dataclass class EngineTest: engine_class: Optional[Type[Union[JinjaTemplateEngine, MakoTemplateEngine, MiniJinjaTemplateEngine]]] index_template: str nested_template: str instantiated: bool instance: Optional[Union[JinjaTemplateEngine, MakoTemplateEngine, MiniJinjaTemplateEngine]] mako_template_lookup = TemplateLookup() mako_template_lookup.put_string("index.html", "Injected? ${test}") mako_template_lookup.put_string("nested-dir/nested.html", "Does nested dirs work? ${test}") mako_template_lookup.put_string("no_context.html", "This works!") @pytest.fixture( params=[ EngineTest( engine_class=JinjaTemplateEngine, index_template="Injected? {{test}}", nested_template="Does nested dirs work? {{test}}", instantiated=False, instance=None, ), EngineTest( engine_class=MakoTemplateEngine, index_template="Injected? ${test}", nested_template="Does nested dirs work? ${test}", instantiated=False, instance=None, ), EngineTest( engine_class=MiniJinjaTemplateEngine, index_template="Injected? {{test}}", nested_template="Does nested dirs work? {{test}}", instantiated=False, instance=None, ), EngineTest( engine_class=None, index_template="Injected? {{test}}", nested_template="Does nested dirs work? {{test}}", instantiated=True, instance=JinjaTemplateEngine.from_environment( Environment( loader=DictLoader( { "index.html": "Injected? {{test}}", "nested-dir/nested.html": "Does nested dirs work? {{test}}", "no_context.html": "This works!", } ) ) ), ), EngineTest( engine_class=None, index_template="Injected? ${test}", nested_template="Does nested dirs work? ${test}", instantiated=True, instance=MakoTemplateEngine.from_template_lookup(mako_template_lookup), ), ] ) def engine_test(request: Any) -> EngineTest: return request.param # type:ignore[no-any-return] @pytest.fixture() def index_handler(engine_test: EngineTest, tmp_path: Path) -> "HTTPRouteHandler": Path(tmp_path / "index.html").write_text(engine_test.index_template) @get(path="/") def index_handler() -> Template: return Template(template_name="index.html", context={"test": "yep"}) return index_handler @pytest.fixture() def nested_path_handler(engine_test: EngineTest, tmp_path: Path) -> "HTTPRouteHandler": nested_path = tmp_path / "nested-dir" nested_path.mkdir() Path(nested_path / "nested.html").write_text(engine_test.nested_template) @get(path="/nested") def nested_path_handler() -> Template: return Template(template_name="nested-dir/nested.html", context={"test": "yep"}) return nested_path_handler @pytest.fixture() def template_config(engine_test: EngineTest, tmp_path: Path) -> TemplateConfig: if engine_test.instantiated: return TemplateConfig(instance=engine_test.instance) return TemplateConfig(engine=engine_test.engine_class, directory=tmp_path) def test_template(index_handler: "HTTPRouteHandler", template_config: TemplateConfig) -> None: with create_test_client(route_handlers=[index_handler], template_config=template_config) as client: response = client.request("GET", "/") assert response.status_code == 200, response.text assert response.text == "Injected? yep" assert response.headers["Content-Type"] == "text/html; charset=utf-8" def test_nested_tmp_pathectory(nested_path_handler: "HTTPRouteHandler", template_config: TemplateConfig) -> None: with create_test_client(route_handlers=[nested_path_handler], template_config=template_config) as client: response = client.request("GET", "/nested") assert response.status_code == 200, response.text assert response.text == "Does nested dirs work? yep" assert response.headers["Content-Type"] == "text/html; charset=utf-8" def test_raise_for_invalid_template_name(template_config: TemplateConfig) -> None: @get(path="/") def invalid_template_name_handler() -> Template: return Template(template_name="invalid.html", context={"test": "yep"}) with create_test_client( route_handlers=[invalid_template_name_handler], template_config=template_config, debug=False ) as client: response = client.request("GET", "/") assert response.status_code == 500 assert response.json() == {"detail": "Internal Server Error", "status_code": 500} def test_no_context(tmp_path: Path, template_config: TemplateConfig) -> None: Path(tmp_path / "no_context.html").write_text("This works!") @get(path="/") def index() -> Template: return Template(template_name="no_context.html") with create_test_client(route_handlers=[index], template_config=template_config) as client: index_response = client.request("GET", "/") assert index_response.status_code == 200 assert index_response.text == "This works!" assert index_response.headers["Content-Type"] == "text/html; charset=utf-8" litestar-2.16.0/tests/unit/test_template/test_builtin_functions.py000066400000000000000000000265371500564371300256120ustar00rootroot00000000000000import sys from pathlib import Path from typing import Optional import pytest from litestar import get from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.response.template import Template from litestar.static_files.config import StaticFilesConfig from litestar.status_codes import HTTP_200_OK, HTTP_500_INTERNAL_SERVER_ERROR from litestar.template.config import TemplateConfig from litestar.testing import create_test_client @pytest.mark.xfail(sys.platform == "win32", reason="For some reason this is flaky on windows", strict=False) def test_jinja_url_for(tmp_path: Path) -> None: template_config = TemplateConfig(engine=JinjaTemplateEngine, directory=tmp_path) @get(path="/") def tpl_renderer() -> Template: return Template(template_name="tpl.html") @get(path="/simple", name="simple") def simple_handler() -> None: pass @get(path="/complex/{int_param:int}/{time_param:time}", name="complex") def complex_handler() -> None: pass with create_test_client( route_handlers=[simple_handler, complex_handler, tpl_renderer], template_config=template_config ) as client: Path(tmp_path / "tpl.html").write_text("{{ url_for('simple') }}") response = client.get("/") assert response.status_code == 200 assert response.text == "/simple" with create_test_client( route_handlers=[simple_handler, complex_handler, tpl_renderer], template_config=template_config ) as client: Path(tmp_path / "tpl.html").write_text("{{ url_for('complex', int_param=100, time_param='18:00') }}") response = client.get("/") assert response.status_code == 200 assert response.text == "/complex/100/18:00" with create_test_client( route_handlers=[simple_handler, complex_handler, tpl_renderer], template_config=template_config ) as client: # missing route params should cause 500 err Path(tmp_path / "tpl.html").write_text("{{ url_for('complex') }}") response = client.get("/") assert response.status_code == 500 with create_test_client( route_handlers=[simple_handler, complex_handler, tpl_renderer], template_config=template_config ) as client: # wrong param type should also cause 500 error Path(tmp_path / "tpl.html").write_text("{{ url_for('complex', int_param='100', time_param='18:00') }}") response = client.get("/") assert response.status_code == 500 with create_test_client( route_handlers=[simple_handler, complex_handler, tpl_renderer], template_config=template_config ) as client: Path(tmp_path / "tpl.html").write_text("{{ url_for('non-existent-route') }}") response = client.get("/") assert response.status_code == 500 # TODO: use some other flaky test technique, probably re-running flaky tests? @pytest.mark.xfail(sys.platform == "win32", reason="For some reason this is flaky on windows", strict=False) def test_jinja_url_for_static_asset(tmp_path: Path) -> None: template_config = TemplateConfig(engine=JinjaTemplateEngine, directory=tmp_path) @get(path="/", name="tpl_renderer") def tpl_renderer() -> Template: return Template(template_name="tpl.html") with create_test_client( route_handlers=[tpl_renderer], template_config=template_config, static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")], ) as client: Path(tmp_path / "tpl.html").write_text("{{ url_for_static_asset('css', 'main/main.css') }}") response = client.get("/") assert response.status_code == 200 assert response.text == "/static/css/main/main.css" with create_test_client( route_handlers=[tpl_renderer], template_config=template_config, static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")], ) as client: Path(tmp_path / "tpl.html").write_text("{{ url_for_static_asset('non-existent', 'main.css') }}") response = client.get("/") assert response.status_code == 500 with create_test_client( route_handlers=[tpl_renderer], template_config=template_config, static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")], ) as client: Path(tmp_path / "tpl.html").write_text("{{ url_for_static_asset('tpl_renderer', 'main.css') }}") response = client.get("/") assert response.status_code == 500 @pytest.mark.parametrize( "builtin, expected_status, expected_text", ( ("${url_for_static_asset('css', 'main/main.css')}", HTTP_200_OK, "/static/css/main/main.css"), ("${url_for_static_asset('non-existent', 'main.css')}", HTTP_500_INTERNAL_SERVER_ERROR, None), ("${url_for_static_asset('tpl_renderer', 'main.css')}", HTTP_500_INTERNAL_SERVER_ERROR, None), ), ) def test_mako_url_for_static_asset( tmp_path: Path, builtin: str, expected_status: int, expected_text: Optional[str] ) -> None: template_config = TemplateConfig(engine=MakoTemplateEngine, directory=tmp_path) @get(path="/", name="tpl_renderer") def tpl_renderer() -> Template: return Template(template_name="tpl.html") with create_test_client( route_handlers=[tpl_renderer], template_config=template_config, static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")], ) as client: Path(tmp_path / "tpl.html").write_text(builtin) response = client.get("/") assert response.status_code == expected_status if expected_text: assert response.text == expected_text @pytest.mark.parametrize( "builtin, expected_status, expected_text", ( ("${url_for('simple')}", HTTP_200_OK, "/simple"), ("${url_for('complex', int_param=100, time_param='18:00')}", HTTP_200_OK, None), ("${url_for('complex')}", HTTP_500_INTERNAL_SERVER_ERROR, None), ("${url_for('non-existent-route')}", HTTP_500_INTERNAL_SERVER_ERROR, None), ), ) def test_mako_url_for(tmp_path: Path, builtin: str, expected_status: int, expected_text: Optional[str]) -> None: template_config = TemplateConfig(engine=MakoTemplateEngine, directory=tmp_path) @get(path="/") def tpl_renderer() -> Template: return Template(template_name="tpl.html") @get(path="/simple", name="simple") def simple_handler() -> None: pass @get(path="/complex/{int_param:int}/{time_param:time}", name="complex") def complex_handler() -> None: pass with create_test_client( route_handlers=[simple_handler, complex_handler, tpl_renderer], template_config=template_config ) as client: # missing route params should cause 500 err Path(tmp_path / "tpl.html").write_text(builtin) response = client.get("/") assert response.status_code == expected_status if expected_text: assert response.text == expected_text @pytest.mark.xfail(sys.platform == "win32", reason="For some reason this is flaky on windows", strict=False) def test_minijinja_url_for(tmp_path: Path) -> None: template_config = TemplateConfig(engine=MiniJinjaTemplateEngine, directory=tmp_path) @get(path="/{path:path}") def tpl_renderer(path: Path) -> Template: return Template(template_name=path.name) @get(path="/simple", name="simple") def simple_handler() -> None: pass @get(path="/complex/{int_param:int}/{time_param:time}", name="complex") def complex_handler() -> None: pass with create_test_client( route_handlers=[simple_handler, complex_handler, tpl_renderer], template_config=template_config ) as client: Path(tmp_path / "simple.html").write_text("{{ url_for('simple') }}") response = client.get("/simple.html") assert response.status_code == 200 assert response.text == "/simple" with create_test_client( route_handlers=[simple_handler, complex_handler, tpl_renderer], template_config=template_config ) as client: Path(tmp_path / "complex_args_kwargs.html").write_text( "{{ url_for('complex', int_param=100, time_param='18:00') }}" ) response = client.get("/complex_args_kwargs.html") assert response.status_code == 200 assert response.text == "/complex/100/18:00" with create_test_client( route_handlers=[simple_handler, complex_handler, tpl_renderer], template_config=template_config ) as client: # missing route params should cause 500 err Path(tmp_path / "complex.html").write_text("{{ url_for('complex') }}") response = client.get("/complex.html") assert response.status_code == 500 with create_test_client( route_handlers=[simple_handler, complex_handler, tpl_renderer], template_config=template_config ) as client: # wrong param type should also cause 500 error Path(tmp_path / "complex_wrong_type.html").write_text( "{{ url_for('complex', int_param='100', time_param='18:00') }}" ) response = client.get("/complex_wrong_type.html") assert response.status_code == 500 with create_test_client( route_handlers=[simple_handler, complex_handler, tpl_renderer], template_config=template_config ) as client: Path(tmp_path / "non_existent.html").write_text("{{ url_for('non-existent-route') }}") response = client.get("/non_existent.html") assert response.status_code == 500 @pytest.mark.xfail(sys.platform == "win32", reason="For some reason this is flaky on windows", strict=False) def test_minijinja_url_for_static_asset(tmp_path: Path) -> None: template_config = TemplateConfig(engine=MiniJinjaTemplateEngine, directory=tmp_path) @get(path="/{path:path}", name="tpl_renderer") def tpl_renderer(path: Path) -> Template: return Template(template_name=path.name) with create_test_client( route_handlers=[tpl_renderer], template_config=template_config, static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")], ) as client: Path(tmp_path / "working.html").write_text("{{ url_for_static_asset('css', 'main/main.css') }}") response = client.get("/working.html") assert response.status_code == 200 assert response.text == "/static/css/main/main.css" with create_test_client( route_handlers=[tpl_renderer], template_config=template_config, static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")], ) as client: Path(tmp_path / "non_existent.html").write_text("{{ url_for_static_asset('non-existent', 'main.css') }}") response = client.get("/non_existent.html") assert response.status_code == 500 with create_test_client( route_handlers=[tpl_renderer], template_config=template_config, static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")], ) as client: Path(tmp_path / "self.html").write_text("{{ url_for_static_asset('tpl_renderer', 'main.css') }}") response = client.get("/self.html") assert response.status_code == 500 litestar-2.16.0/tests/unit/test_template/test_config.py000066400000000000000000000006471500564371300233130ustar00rootroot00000000000000from typing import TYPE_CHECKING from litestar.contrib.jinja import JinjaTemplateEngine from litestar.template.config import TemplateConfig if TYPE_CHECKING: from pathlib import Path def test_pytest_config_caches_engine_instance(tmp_path: "Path") -> None: config = TemplateConfig( directory=tmp_path, engine=JinjaTemplateEngine, ) assert config.engine_instance is config.engine_instance litestar-2.16.0/tests/unit/test_template/test_context.py000066400000000000000000000024731500564371300235310ustar00rootroot00000000000000from pathlib import Path from typing import Any import pytest from litestar import MediaType, get from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.response.template import Template from litestar.template.config import TemplateConfig from litestar.testing import create_test_client @pytest.mark.parametrize( "engine, template, expected", ( (JinjaTemplateEngine, 'path: {{ request.scope["path"] }}', "path: /"), (MakoTemplateEngine, 'path: ${request.scope["path"]}', "path: /"), (MiniJinjaTemplateEngine, 'path: {{ request.scope["path"] }}', "path: /"), ), ) def test_request_is_set_in_context(engine: Any, template: str, expected: str, tmp_path: Path) -> None: Path(tmp_path / "abc.html").write_text(template) @get(path="/", media_type=MediaType.HTML) def handler() -> Template: return Template(template_name="abc.html", context={"request": {"scope": {"path": "nope"}}}) with create_test_client( route_handlers=[handler], template_config=TemplateConfig( directory=tmp_path, engine=engine, ), ) as client: response = client.get("/") assert response.text == expected litestar-2.16.0/tests/unit/test_template/test_csrf_token.py000066400000000000000000000051121500564371300241730ustar00rootroot00000000000000import html from pathlib import Path from typing import Any import pytest from litestar import MediaType, get from litestar.config.csrf import CSRFConfig from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.middleware.csrf import generate_csrf_token from litestar.response.template import Template from litestar.template.config import TemplateConfig from litestar.testing import create_test_client from litestar.types import Scope from litestar.utils.empty import value_or_default from litestar.utils.scope.state import ScopeState @pytest.mark.parametrize( "engine, template", ( (JinjaTemplateEngine, "{{csrf_token()}}"), (MakoTemplateEngine, "${csrf_token()}"), (MiniJinjaTemplateEngine, "{{csrf_token()}}"), ), ) def test_csrf_token(engine: Any, template: str, tmp_path: Path) -> None: Path(tmp_path / "abc.html").write_text(template) @get(path="/", media_type=MediaType.HTML) def handler() -> Template: return Template(template_name="abc.html") csrf_config = CSRFConfig(secret="yaba daba do") with create_test_client( route_handlers=[handler], template_config=TemplateConfig( directory=tmp_path, engine=engine, ), csrf_config=csrf_config, ) as client: response = client.get("/") assert len(response.text) == len(generate_csrf_token(csrf_config.secret)) @pytest.mark.parametrize( "engine, template", ( (JinjaTemplateEngine, "{{csrf_input}}"), (MakoTemplateEngine, "${csrf_input}"), (MiniJinjaTemplateEngine, "{{csrf_input}}"), ), ) def test_csrf_input(engine: Any, template: str, tmp_path: Path) -> None: Path(tmp_path / "abc.html").write_text(template) token = {"value": ""} @get(path="/", media_type=MediaType.HTML) def handler(scope: Scope) -> Template: connection_state = ScopeState.from_scope(scope) token["value"] = value_or_default(connection_state.csrf_token, "") return Template(template_name="abc.html") csrf_config = CSRFConfig(secret="yaba daba do") with create_test_client( route_handlers=[handler], template_config=TemplateConfig( directory=tmp_path, engine=engine, ), csrf_config=csrf_config, ) as client: response = client.get("/") assert token["value"] assert html.unescape(response.text) == f'' litestar-2.16.0/tests/unit/test_template/test_template.py000066400000000000000000000167111500564371300236600ustar00rootroot00000000000000from __future__ import annotations import sys from pathlib import Path from typing import TYPE_CHECKING import pytest from litestar import Litestar, MediaType, get from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.exceptions import ImproperlyConfiguredException from litestar.response.template import Template from litestar.template import TemplateEngineProtocol from litestar.template.config import TemplateConfig from litestar.testing import create_test_client if TYPE_CHECKING: from litestar import Request def test_handler_raise_for_no_template_engine() -> None: @get(path="/") def invalid_path() -> Template: return Template(template_name="index.html", context={"ye": "yeeee"}) with create_test_client(route_handlers=[invalid_path], debug=False) as client: response = client.request("GET", "/") assert response.status_code == 500 assert response.json() == {"detail": "Internal Server Error", "status_code": 500} def test_engine_passed_to_callback(tmp_path: Path) -> None: received_engine: JinjaTemplateEngine | None = None def callback(engine: TemplateEngineProtocol) -> None: nonlocal received_engine assert isinstance(engine, JinjaTemplateEngine), "Engine must be a JinjaTemplateEngine" received_engine = engine app = Litestar( route_handlers=[], template_config=TemplateConfig( directory=tmp_path, engine=JinjaTemplateEngine, engine_callback=callback, ), ) assert received_engine is not None assert received_engine is app.template_engine @pytest.mark.parametrize("engine", (JinjaTemplateEngine, MakoTemplateEngine, MiniJinjaTemplateEngine)) def test_engine_instance(engine: type[TemplateEngineProtocol], tmp_path: Path) -> None: engine_instance = engine(directory=tmp_path, engine_instance=None) if isinstance(engine_instance, JinjaTemplateEngine): assert engine_instance.engine.autoescape is True if isinstance(engine_instance, MakoTemplateEngine): assert engine_instance.engine.template_args["default_filters"] == ["h"] config = TemplateConfig(engine=engine_instance) assert config.engine_instance is engine_instance @pytest.mark.parametrize("engine", (JinjaTemplateEngine, MakoTemplateEngine, MiniJinjaTemplateEngine)) def test_directory_validation(engine: type[TemplateEngineProtocol], tmp_path: Path) -> None: with pytest.raises(ImproperlyConfiguredException): TemplateConfig(engine=engine) @pytest.mark.parametrize("engine", (JinjaTemplateEngine, MakoTemplateEngine, MiniJinjaTemplateEngine)) def test_instance_and_directory_validation(engine: type[TemplateEngineProtocol], tmp_path: Path) -> None: with pytest.raises(ImproperlyConfiguredException): TemplateConfig(engine=engine, instance=engine(directory=tmp_path, engine_instance=None)) @pytest.mark.parametrize("media_type", [MediaType.HTML, MediaType.TEXT, "text/arbitrary"]) def test_media_type(media_type: MediaType | str, tmp_path: Path) -> None: (tmp_path / "hello.tpl").write_text("hello") @get("/", media_type=media_type) def index() -> Template: return Template(template_name="hello.tpl") with create_test_client( [index], template_config=TemplateConfig(directory=tmp_path, engine=JinjaTemplateEngine) ) as client: res = client.get("/") assert res.status_code == 200 assert res.headers["content-type"].startswith( media_type if isinstance(media_type, str) else media_type.value, # type: ignore[union-attr] ) @pytest.mark.parametrize( "extension,expected_type", [ (".json", MediaType.JSON.value), (".html", MediaType.HTML.value), (".html.other", MediaType.HTML.value), (".css", MediaType.CSS.value), (".xml", MediaType.XML.value), (".xml.other", MediaType.XML.value), (".txt", MediaType.TEXT.value), (".unknown", MediaType.TEXT.value), ("", MediaType.TEXT.value), ], ) @pytest.mark.skipif(sys.platform == "win32", reason="mimetypes.guess_types is unreliable on windows") def test_media_type_inferred(extension: str, expected_type: MediaType, tmp_path: Path) -> None: tpl_name = f"hello{extension}" (tmp_path / tpl_name).write_text("hello") @get("/") def index() -> Template: return Template(template_name=tpl_name) with create_test_client( [index], template_config=TemplateConfig(directory=tmp_path, engine=JinjaTemplateEngine) ) as client: res = client.get("/") assert res.status_code == 200 if expected_type == MediaType.XML.value: assert res.headers["content-type"].startswith(expected_type) or res.headers["content-type"].startswith( "text/xml" ) else: assert res.headers["content-type"].startswith(expected_type) def test_before_request_handler_content_type(tmp_path: Path) -> None: template_loc = tmp_path / "about.html" def before_request_handler(_: Request) -> None: template_loc.write_text("before request") @get("/", before_request=before_request_handler) def index() -> Template: return Template(template_name="about.html") with create_test_client( [index], template_config=TemplateConfig(directory=tmp_path, engine=JinjaTemplateEngine) ) as client: res = client.get("/") assert res.status_code == 200 assert res.headers["content-type"].startswith(MediaType.HTML.value) assert res.text == "before request" test_cases = [ {"name": "both", "template_name": "dummy.html", "template_str": "Dummy", "raises": ValueError}, {"name": "none", "template_name": None, "template_str": None, "status_code": 500}, {"name": "name_only", "template_name": "dummy.html", "template_str": None, "status_code": 200}, {"name": "str_only", "template_name": None, "template_str": "Dummy", "status_code": 200}, ] @pytest.mark.parametrize("engine", (JinjaTemplateEngine, MakoTemplateEngine, MiniJinjaTemplateEngine)) @pytest.mark.parametrize("test_case", test_cases, ids=[case["name"] for case in test_cases]) # type: ignore[index] def test_template_scenarios(tmp_path: Path, engine: TemplateEngineProtocol, test_case: dict) -> None: if test_case["template_name"]: template_loc = tmp_path / test_case["template_name"] template_loc.write_text("Test content for template") @get("/") def index() -> Template: return Template(template_name=test_case["template_name"], template_str=test_case["template_str"]) with create_test_client([index], template_config=TemplateConfig(directory=tmp_path, engine=engine)) as client: if "raises" in test_case and test_case["raises"] is ValueError: response = client.get("/") assert response.status_code == 500 assert "ValueError" in response.text else: response = client.get("/") assert response.status_code == test_case["status_code"] if test_case["status_code"] == 200: if test_case["template_str"]: assert response.text == test_case["template_str"] else: assert response.text == "Test content for template" elif test_case["status_code"] == 500: assert "Either template_name or template_str must be provided" in response.text litestar-2.16.0/tests/unit/test_testing/000077500000000000000000000000001500564371300202705ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_testing/__init__.py000066400000000000000000000000001500564371300223670ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_testing/test_lifespan_handler.py000066400000000000000000000030241500564371300251760ustar00rootroot00000000000000import pytest from litestar.testing import TestClient from litestar.testing.life_span_handler import LifeSpanHandler from litestar.types import Receive, Scope, Send pytestmark = pytest.mark.filterwarnings("default") async def test_wait_startup_invalid_event() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: await send({"type": "lifespan.startup.something_unexpected"}) # type: ignore[typeddict-item] with pytest.raises(RuntimeError, match="Received unexpected ASGI message type"): with LifeSpanHandler(TestClient(app)): pass async def test_wait_shutdown_invalid_event() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: await send({"type": "lifespan.startup.complete"}) # type: ignore[typeddict-item] await send({"type": "lifespan.shutdown.something_unexpected"}) # type: ignore[typeddict-item] with LifeSpanHandler(TestClient(app)) as handler: with pytest.raises(RuntimeError, match="Received unexpected ASGI message type"): await handler.wait_shutdown() async def test_implicit_startup() -> None: async def app(scope: Scope, receive: Receive, send: Send) -> None: await send({"type": "lifespan.startup.complete"}) # type: ignore[typeddict-item] await send({"type": "lifespan.shutdown.complete"}) # type: ignore[typeddict-item] with pytest.warns(DeprecationWarning): handler = LifeSpanHandler(TestClient(app)) await handler.wait_shutdown() handler.close() litestar-2.16.0/tests/unit/test_testing/test_request_factory.py000066400000000000000000000160451500564371300251260ustar00rootroot00000000000000import json from dataclasses import dataclass from typing import Callable, Dict import msgspec import pytest from litestar import HttpMethod, Litestar, get from litestar.datastructures import Cookie, MultiDict from litestar.enums import ParamType, RequestEncodingType from litestar.serialization import encode_json from litestar.testing import RequestFactory from litestar.types import DataContainerType from tests.models import ( DataclassPerson, DataclassPersonFactory, DataclassPetFactory, MsgSpecStructPerson, ) _DEFAULT_REQUEST_FACTORY_URL = "http://test.org:3000/" pet = DataclassPetFactory.build() async def test_request_factory_empty_body() -> None: request = RequestFactory().post(data={}) await request.body() def test_request_factory_no_cookie_header() -> None: headers: Dict[str, str] = {} RequestFactory._create_cookie_header(headers) assert not headers def test_request_factory_str_cookie_header() -> None: headers: Dict[str, str] = {} cookie_as_str = "test=cookie; litestar=cookie" RequestFactory._create_cookie_header(headers, cookie_as_str) assert headers[ParamType.COOKIE] == cookie_as_str def test_request_factory_cookie_list_header() -> None: headers: Dict[str, str] = {} cookie_list = [Cookie(key="test", value="cookie"), Cookie(key="litestar", value="cookie", path="/test")] RequestFactory._create_cookie_header(headers, cookie_list) assert headers[ParamType.COOKIE] == "test=cookie; Path=/; SameSite=lax; litestar=cookie; Path=/test; SameSite=lax" def test_request_factory_build_headers() -> None: headers = { "header1": "value1", "header2": "value2", } built_headers = RequestFactory()._build_headers(headers) assert len(built_headers) == len(headers.keys()) for key, value in built_headers: decoded_key = key.decode("latin1") decoded_value = value.decode("latin1") assert decoded_key in headers assert headers[decoded_key] == decoded_value @pytest.mark.parametrize("data_cls", [DataclassPerson, MsgSpecStructPerson]) async def test_request_factory_create_with_data(data_cls: DataContainerType) -> None: person_data = msgspec.json.decode(encode_json(DataclassPersonFactory.build())) request = RequestFactory()._create_request_with_data( HttpMethod.POST, "/", data=data_cls(**person_data), # type: ignore[operator] ) body = await request.body() assert json.loads(body) == person_data async def test_request_factory_create_with_data_with_custom_encoder() -> None: class Foo: bar: str = "baz" request = RequestFactory(app=Litestar(type_encoders={Foo: lambda f: {"bar": f.bar}}))._create_request_with_data( HttpMethod.POST, "/", data=Foo(), # type: ignore[arg-type] ) body = await request.body() assert json.loads(body) == {"bar": "baz"} @pytest.mark.parametrize( "request_media_type, verify_data", [ [RequestEncodingType.JSON, lambda data: json.loads(data) == msgspec.to_builtins(pet)], [RequestEncodingType.MULTI_PART, lambda data: "Content-Disposition" in data], [ RequestEncodingType.URL_ENCODED, lambda data: data == f"name={pet.name}&age={pet.age}&species={pet.species.value}", ], ], ) async def test_request_factory_create_with_content_type( request_media_type: RequestEncodingType, verify_data: Callable[[str], bool] ) -> None: request = RequestFactory()._create_request_with_data( HttpMethod.POST, "/", data=msgspec.to_builtins(pet), request_media_type=request_media_type, ) assert request.headers["Content-Type"].startswith(request_media_type.value) body = await request.body() assert verify_data(body.decode("utf-8")) def test_request_factory_create_with_default_params() -> None: request = RequestFactory().get() assert isinstance(request.app, Litestar) assert request.url == request.base_url == _DEFAULT_REQUEST_FACTORY_URL assert request.method == HttpMethod.GET assert request.state.keys() == {"_ls_connection_state"} assert not request.query_params assert not request.path_params assert request.route_handler assert request.scope["http_version"] == "1.1" assert request.scope["raw_path"] == b"/" def test_request_factory_create_with_params() -> None: @dataclass class User: pass @dataclass class Auth: pass @get("/path") def handler() -> None: ... app = Litestar(route_handlers=[]) server = "litestar.org" port = 5000 root_path = "/root" path = "/path" user = User() auth = Auth() scheme = "https" session = {"param1": "a", "param2": 2} state = {"weather": "sunny"} path_params = {"param": "a"} request = RequestFactory(app, server, port, root_path, scheme).get( path, session=session, user=user, auth=auth, state=state, path_params=path_params, http_version="2.0", route_handler=handler, ) assert request.app == app assert request.base_url == f"{scheme}://{server}:{port}{root_path}/" assert request.url == f"{scheme}://{server}:{port}{root_path}{path}" assert request.method == HttpMethod.GET assert request.query_params == MultiDict() assert request.user == user assert request.auth == auth assert request.session == session assert request.state.weather == "sunny" assert request.path_params == path_params assert request.route_handler == handler assert request.scope["http_version"] == "2.0" assert request.scope["raw_path"] == path.encode("ascii") def test_request_factory_get() -> None: query_params = {"p1": "a", "p2": 2, "p3": ["c", "d"]} headers = {"header1": "value1"} request = RequestFactory().get(headers=headers, query_params=query_params) # type: ignore[arg-type] assert request.method == HttpMethod.GET assert request.url == f"{_DEFAULT_REQUEST_FACTORY_URL}?p1=a&p2=2&p3=c&p3=d" assert len(request.headers.keys()) == 1 assert request.headers.get("header1") == "value1" def test_request_factory_delete() -> None: headers = {"header1": "value1"} request = RequestFactory().delete(headers=headers) assert request.method == HttpMethod.DELETE assert request.url == _DEFAULT_REQUEST_FACTORY_URL assert len(request.headers.keys()) == 1 assert request.headers.get("header1") == "value1" @pytest.mark.parametrize( "factory, method", [ (RequestFactory().post, HttpMethod.POST), (RequestFactory().put, HttpMethod.PUT), (RequestFactory().patch, HttpMethod.PATCH), ], ) async def test_request_factory_post_put_patch(factory: Callable, method: HttpMethod) -> None: headers = {"header1": "value1"} request = factory("/", headers=headers, data=pet) assert request.method == method # Headers should include "header1" and "Content-Type" assert len(request.headers.keys()) == 3 assert request.headers.get("header1") == "value1" body = await request.body() assert json.loads(body) == msgspec.to_builtins(pet) litestar-2.16.0/tests/unit/test_testing/test_sub_client/000077500000000000000000000000001500564371300234565ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_testing/test_sub_client/__init__.py000066400000000000000000000000001500564371300255550ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_testing/test_sub_client/demo.py000066400000000000000000000011241500564371300247520ustar00rootroot00000000000000""" Assemble components into an app that shall be tested """ import asyncio from typing import AsyncIterator from litestar import Litestar, get from litestar.response import ServerSentEvent @get("/notify/{topic:str}") async def get_notified(topic: str) -> ServerSentEvent: async def generator() -> AsyncIterator[str]: yield topic while True: await asyncio.sleep(0.1) return ServerSentEvent(generator(), event_type="Notifier") def create_test_app() -> Litestar: return Litestar( route_handlers=[get_notified], ) app = create_test_app() litestar-2.16.0/tests/unit/test_testing/test_sub_client/test_subprocess_client.py000066400000000000000000000044151500564371300306210ustar00rootroot00000000000000""" Test the app running in a subprocess """ import asyncio import pathlib import sys from typing import AsyncIterator, Iterator import httpx import httpx_sse import pytest from litestar.testing import subprocess_async_client, subprocess_sync_client from litestar.testing.client.subprocess_client import StartupError, run_app if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) ROOT = pathlib.Path(__file__).parent APP = "demo:app" @pytest.fixture(name="async_client") async def fx_async_client() -> AsyncIterator[httpx.AsyncClient]: async with subprocess_async_client(workdir=ROOT, app=APP) as client: yield client @pytest.fixture(name="sync_client") def fx_sync_client() -> Iterator[httpx.Client]: with subprocess_sync_client(workdir=ROOT, app=APP) as client: yield client async def test_run_app() -> None: """Ensure that method returns application url if started successfully""" with run_app(workdir=ROOT, app=APP) as url: assert isinstance(url, str) assert url.startswith("http://127.0.0.1:") async def test_run_app_exception() -> None: """ Ensure that method throws a StartupError if the application fails to start. To simulate this, we set retry_count=0, so that we don't check if the application has started. """ with pytest.raises(StartupError): with run_app(workdir=ROOT, app=APP, retry_count=0): ... async def test_subprocess_async_client(async_client: httpx.AsyncClient) -> None: """Demonstrates functionality of the async client with an infinite SSE source that cannot be tested with the regular async test client. """ async with httpx_sse.aconnect_sse(async_client, "GET", "/notify/hello") as event_source: async for event in event_source.aiter_sse(): assert event.data == "hello" break def test_subprocess_sync_client(sync_client: httpx.Client) -> None: """Demonstrates functionality of the async client with an infinite SSE source that cannot be tested with the regular async test client. """ with httpx_sse.connect_sse(sync_client, "GET", "/notify/hello") as event_source: for event in event_source.iter_sse(): assert event.data == "hello" break litestar-2.16.0/tests/unit/test_testing/test_test_client.py000066400000000000000000000277201500564371300242260ustar00rootroot00000000000000from queue import Empty from typing import TYPE_CHECKING, Callable, Dict, NoReturn, Optional, Union, cast from _pytest.fixtures import FixtureRequest from litestar import Controller, WebSocket, delete, head, patch, put, websocket from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT from litestar.testing import AsyncTestClient, WebSocketTestSession, create_async_test_client, create_test_client if TYPE_CHECKING: from litestar.middleware.session.base import BaseBackendConfig from litestar.types import ( AnyIOBackend, HTTPResponseBodyEvent, HTTPResponseStartEvent, Receive, Scope, Send, ) from typing import Any, Type import pytest from litestar import Litestar, Request, get, post from litestar.stores.base import Store from litestar.testing import TestClient from litestar.utils.helpers import get_exception_group from tests.helpers import maybe_async, maybe_async_cm _ExceptionGroup = get_exception_group() AnyTestClient = Union[TestClient, AsyncTestClient] async def mock_asgi_app(scope: "Scope", receive: "Receive", send: "Send") -> None: pass @pytest.fixture(params=[AsyncTestClient, TestClient]) def test_client_cls(request: FixtureRequest) -> Type[AnyTestClient]: return cast(Type[AnyTestClient], request.param) @pytest.mark.parametrize( "anyio_backend", [ pytest.param("asyncio"), pytest.param("trio", marks=pytest.mark.xfail(reason="Known issue with trio backend", strict=False)), ], ) @pytest.mark.parametrize("with_domain", [False, True]) async def test_test_client_set_session_data( with_domain: bool, anyio_backend: str, session_backend_config: "BaseBackendConfig", test_client_backend: "AnyIOBackend", test_client_cls: Type[AnyTestClient], ) -> None: session_data = {"foo": "bar"} if with_domain: session_backend_config.domain = "testserver.local" @get(path="/test") async def get_session_data(request: Request) -> Dict[str, Any]: return request.session app = Litestar(route_handlers=[get_session_data], middleware=[session_backend_config.middleware]) async with maybe_async_cm( test_client_cls(app=app, session_config=session_backend_config, backend=test_client_backend) # pyright: ignore ) as client: await maybe_async(client.set_session_data(session_data)) # type: ignore[attr-defined] assert session_data == (await maybe_async(client.get("/test"))).json() # type: ignore[attr-defined] @pytest.mark.parametrize( "anyio_backend", [ pytest.param("asyncio"), pytest.param("trio", marks=pytest.mark.xfail(reason="Known issue with trio backend", strict=False)), ], ) @pytest.mark.parametrize("with_domain", [True, False]) async def test_test_client_get_session_data( with_domain: bool, anyio_backend: str, session_backend_config: "BaseBackendConfig", test_client_backend: "AnyIOBackend", store: Store, test_client_cls: Type[AnyTestClient], ) -> None: session_data = {"foo": "bar"} if with_domain: session_backend_config.domain = "testserver.local" @post(path="/test") async def set_session_data(request: Request) -> None: request.session.update(session_data) app = Litestar( route_handlers=[set_session_data], middleware=[session_backend_config.middleware], stores={"session": store} ) async with maybe_async_cm( test_client_cls(app=app, session_config=session_backend_config, backend=test_client_backend) # pyright: ignore ) as client: await maybe_async(client.post("/test")) # type: ignore[attr-defined] assert await maybe_async(client.get_session_data()) == session_data # type: ignore[attr-defined] async def test_use_testclient_in_endpoint( test_client_backend: "AnyIOBackend", test_client_cls: Type[AnyTestClient] ) -> None: """this test is taken from starlette.""" @get("/") def mock_service_endpoint() -> dict: return {"mock": "example"} mock_service = Litestar(route_handlers=[mock_service_endpoint]) @get("/") async def homepage() -> Any: local_client = test_client_cls(mock_service, backend=test_client_backend) local_response = await maybe_async(local_client.get("/")) return local_response.json() # type: ignore[union-attr] app = Litestar(route_handlers=[homepage]) client = test_client_cls(app) response = await maybe_async(client.get("/")) assert response.json() == {"mock": "example"} # type: ignore[union-attr] def raise_error(app: Litestar) -> NoReturn: raise RuntimeError() async def test_error_handling_on_startup( test_client_backend: "AnyIOBackend", test_client_cls: Type[AnyTestClient] ) -> None: with pytest.raises(_ExceptionGroup): async with maybe_async_cm( test_client_cls(Litestar(on_startup=[raise_error]), backend=test_client_backend) # pyright: ignore ): pass async def test_error_handling_on_shutdown( test_client_backend: "AnyIOBackend", test_client_cls: Type[AnyTestClient] ) -> None: with pytest.raises(RuntimeError): async with maybe_async_cm( test_client_cls(Litestar(on_shutdown=[raise_error]), backend=test_client_backend) # pyright: ignore ): pass @pytest.mark.parametrize("method", ["get", "post", "put", "patch", "delete", "head", "options"]) async def test_client_interface( method: str, test_client_backend: "AnyIOBackend", test_client_cls: Type[AnyTestClient] ) -> None: async def asgi_app(scope: "Scope", receive: "Receive", send: "Send") -> None: start_event: HTTPResponseStartEvent = { "type": "http.response.start", "status": HTTP_200_OK, "headers": [(b"content-type", b"text/plain")], } await send(start_event) body_event: HTTPResponseBodyEvent = {"type": "http.response.body", "body": b"", "more_body": False} await send(body_event) client = test_client_cls(asgi_app, backend=test_client_backend) if method == "get": response = await maybe_async(client.get("/")) elif method == "post": response = await maybe_async(client.post("/")) elif method == "put": response = await maybe_async(client.put("/")) elif method == "patch": response = await maybe_async(client.patch("/")) elif method == "delete": response = await maybe_async(client.delete("/")) elif method == "head": response = await maybe_async(client.head("/")) else: response = await maybe_async(client.options("/")) assert response.status_code == HTTP_200_OK # type: ignore[union-attr] def test_warns_problematic_domain(test_client_cls: Type[AnyTestClient]) -> None: with pytest.warns(UserWarning): test_client_cls(app=mock_asgi_app, base_url="http://testserver") @pytest.mark.parametrize("method", ["get", "post", "put", "patch", "delete", "head", "options"]) async def test_client_interface_context_manager( method: str, test_client_backend: "AnyIOBackend", test_client_cls: Type[AnyTestClient] ) -> None: class MockController(Controller): @get("/") def mock_service_endpoint_get(self) -> dict: return {"mock": "example"} @post("/") def mock_service_endpoint_post(self) -> dict: return {"mock": "example"} @put("/") def mock_service_endpoint_put(self) -> None: ... @patch("/") def mock_service_endpoint_patch(self) -> None: ... @delete("/") def mock_service_endpoint_delete(self) -> None: ... @head("/") def mock_service_endpoint_head(self) -> None: ... mock_service = Litestar(route_handlers=[MockController]) async with maybe_async_cm(test_client_cls(mock_service, backend=test_client_backend)) as client: # pyright: ignore if method == "get": response = await maybe_async(client.get("/")) # type: ignore[attr-defined] assert response.status_code == HTTP_200_OK # pyright: ignore elif method == "post": response = await maybe_async(client.post("/")) # type: ignore[attr-defined] assert response.status_code == HTTP_201_CREATED # pyright: ignore elif method == "put": response = await maybe_async(client.put("/")) # type: ignore[attr-defined] assert response.status_code == HTTP_200_OK # pyright: ignore elif method == "patch": response = await maybe_async(client.patch("/")) # type: ignore[attr-defined] assert response.status_code == HTTP_200_OK # pyright: ignore elif method == "delete": response = await maybe_async(client.delete("/")) # type: ignore[attr-defined] assert response.status_code == HTTP_204_NO_CONTENT # pyright: ignore elif method == "head": response = await maybe_async(client.head("/")) # type: ignore[attr-defined] assert response.status_code == HTTP_200_OK # pyright: ignore else: response = await maybe_async(client.options("/")) # type: ignore[attr-defined] assert response.status_code == HTTP_204_NO_CONTENT # pyright: ignore @pytest.mark.parametrize("block,timeout", [(False, None), (False, 0.001), (True, 0.001)]) @pytest.mark.parametrize( "receive_method", [ WebSocketTestSession.receive, WebSocketTestSession.receive_json, WebSocketTestSession.receive_text, WebSocketTestSession.receive_bytes, ], ) def test_websocket_test_session_block_timeout( receive_method: Callable[..., Any], block: bool, timeout: Optional[float], anyio_backend: "AnyIOBackend" ) -> None: @websocket() async def handler(socket: WebSocket) -> None: await socket.accept() with pytest.raises(Empty): with create_test_client(handler, backend=anyio_backend) as client, client.websocket_connect("/") as ws: receive_method(ws, timeout=timeout, block=block) def test_websocket_accept_timeout(anyio_backend: "AnyIOBackend") -> None: @websocket() async def handler(socket: WebSocket) -> None: pass with create_test_client(handler, backend=anyio_backend, timeout=0.1) as client, pytest.raises( Empty ), client.websocket_connect("/"): pass @pytest.mark.parametrize("block,timeout", [(False, None), (False, 0.001), (True, 0.001)]) @pytest.mark.parametrize( "receive_method", [ WebSocketTestSession.receive, WebSocketTestSession.receive_json, WebSocketTestSession.receive_text, WebSocketTestSession.receive_bytes, ], ) async def test_websocket_test_session_block_timeout_async( receive_method: Callable[..., Any], block: bool, timeout: Optional[float], anyio_backend: "AnyIOBackend" ) -> None: @websocket() async def handler(socket: WebSocket) -> None: await socket.accept() with pytest.raises(Empty): async with create_async_test_client(handler, backend=anyio_backend) as client: with await client.websocket_connect("/") as ws: receive_method(ws, timeout=timeout, block=block) async def test_websocket_accept_timeout_async(anyio_backend: "AnyIOBackend") -> None: @websocket() async def handler(socket: WebSocket) -> None: pass async with create_async_test_client(handler, backend=anyio_backend, timeout=0.1) as client: with pytest.raises(Empty): with await client.websocket_connect("/"): pass async def test_websocket_connect_async(anyio_backend: "AnyIOBackend") -> None: @websocket() async def handler(socket: WebSocket) -> None: await socket.accept() data = await socket.receive_json() await socket.send_json(data) await socket.close() async with create_async_test_client(handler, backend=anyio_backend, timeout=0.1) as client: with await client.websocket_connect("/", subprotocols="wamp") as ws: ws.send_json({"data": "123"}) data = ws.receive_json() assert data == {"data": "123"} litestar-2.16.0/tests/unit/test_types/000077500000000000000000000000001500564371300177575ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_types/__init__.py000066400000000000000000000000001500564371300220560ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_types/test_protocols.py000066400000000000000000000010741500564371300234160ustar00rootroot00000000000000from __future__ import annotations from typing import Any import pytest from litestar.types.protocols import InstantiableCollection @pytest.mark.parametrize( "collection,expected", [ (list, True), (tuple, True), (set, True), (frozenset, True), (str, True), (dict, True), (int, False), (float, False), (bool, False), ], ) def test_homogenous_instantiable_collection(collection: type[Any], expected: bool) -> None: assert issubclass(collection, InstantiableCollection) == expected litestar-2.16.0/tests/unit/test_typing.py000066400000000000000000000433421500564371300205050ustar00rootroot00000000000000from __future__ import annotations import sys from dataclasses import dataclass from typing import Any, ForwardRef, Generic, List, Optional, Tuple, TypeVar, Union import msgspec import pytest from typing_extensions import ( Annotated, NotRequired, Required, TypedDict, get_type_hints, ) from typing_extensions import ( TypeAliasType as TeTypeAliasType, ) try: from typing import TypeAliasType # type: ignore[attr-defined] except ImportError: TypeAliasType = TeTypeAliasType from litestar import get from litestar.exceptions import LitestarWarning from litestar.params import DependencyKwarg, KwargDefinition, Parameter, ParameterKwarg from litestar.typing import FieldDefinition from tests.unit.test_utils.test_signature import T, _check_field_definition, field_definition_int, test_type_hints @pytest.mark.parametrize( ("annotation", "expected"), [ ( int, { "raw": int, "annotation": int, "origin": None, "args": (), "metadata": (), "safe_generic_origin": None, "inner_types": (), }, ), ( List[int], { "raw": List[int], "annotation": List[int], "origin": list, "args": (int,), "metadata": (), "safe_generic_origin": List, "inner_types": (field_definition_int,), }, ), ( Annotated[int, "foo"], { "raw": Annotated[int, "foo"], "annotation": int, "origin": None, "args": (), "metadata": ("foo",), "safe_generic_origin": None, "inner_types": (), }, ), ( Annotated[List[int], "foo"], { "raw": Annotated[List[int], "foo"], "annotation": List[int], "origin": list, "args": (int,), "metadata": ("foo",), "safe_generic_origin": List, "inner_types": (field_definition_int,), }, ), ( test_type_hints["req_int"], { "raw": test_type_hints["req_int"], "annotation": int, "origin": None, "args": (), "metadata": (), "safe_generic_origin": None, "inner_types": (), }, ), ( test_type_hints["req_list_int"], { "raw": test_type_hints["req_list_int"], "annotation": List[int], "origin": list, "args": (int,), "metadata": (), "safe_generic_origin": List, "inner_types": (field_definition_int,), }, ), ( test_type_hints["not_req_int"], { "raw": test_type_hints["not_req_int"], "annotation": int, "origin": None, "args": (), "metadata": (), "safe_generic_origin": None, "inner_types": (), }, ), ( test_type_hints["not_req_list_int"], { "raw": test_type_hints["not_req_list_int"], "annotation": List[int], "origin": list, "args": (int,), "metadata": (), "safe_generic_origin": List, "inner_types": (field_definition_int,), }, ), ( test_type_hints["ann_req_int"], { "raw": test_type_hints["ann_req_int"], "annotation": int, "origin": None, "args": (), "metadata": ("foo",), "safe_generic_origin": None, "inner_types": (), }, ), ( test_type_hints["ann_req_list_int"], { "raw": test_type_hints["ann_req_list_int"], "annotation": List[int], "origin": list, "args": (int,), "metadata": ("foo",), "safe_generic_origin": List, "inner_types": (field_definition_int,), }, ), ], ) def test_field_definition_from_annotation(annotation: Any, expected: dict[str, Any]) -> None: """Test FieldDefinition.from_annotation.""" _check_field_definition(FieldDefinition.from_annotation(annotation), expected) def test_field_definition_kwarg_definition_from_extras() -> None: kwarg_definition = KwargDefinition() assert ( FieldDefinition.from_annotation(int, extra={"kwarg_definition": kwarg_definition}).kwarg_definition is kwarg_definition ) @pytest.mark.parametrize("kwarg_definition", [KwargDefinition(), DependencyKwarg()]) def test_field_definition_kwarg_definition_from_kwargs(kwarg_definition: KwargDefinition | DependencyKwarg) -> None: assert FieldDefinition.from_annotation(int, kwarg_definition=kwarg_definition).kwarg_definition is kwarg_definition def test_field_definition_with_annotated_kwarg_definition() -> None: kwarg_definition = KwargDefinition() fd = FieldDefinition.from_annotation(Annotated[str, kwarg_definition]) assert fd.kwarg_definition is kwarg_definition def test_field_definition_from_union_annotation() -> None: """Test FieldDefinition.from_annotation for Union.""" annotation = Union[int, List[int]] expected = { "raw": annotation, "annotation": annotation, "origin": Union, "args": (int, List[int]), "metadata": (), "safe_generic_origin": Union, "inner_types": (FieldDefinition.from_annotation(int), FieldDefinition.from_annotation(List[int])), } _check_field_definition(FieldDefinition.from_annotation(annotation), expected) @pytest.mark.parametrize("value", ["int", ForwardRef("int")]) def test_field_definition_is_forward_ref_predicate(value: Any) -> None: """Test FieldDefinition with ForwardRef.""" field_definition = FieldDefinition.from_annotation(value) assert field_definition.is_forward_ref is True assert field_definition.annotation == value assert field_definition.origin is None assert field_definition.args == () assert field_definition.metadata == () assert field_definition.is_annotated is False assert field_definition.is_required is True assert field_definition.safe_generic_origin is None assert field_definition.inner_types == () def test_field_definition_is_type_var_predicate() -> None: """Test FieldDefinition.is_type_var.""" assert FieldDefinition.from_annotation(int).is_type_var is False assert FieldDefinition.from_annotation(T).is_type_var is True assert FieldDefinition.from_annotation(Union[int, T]).is_type_var is False # pyright: ignore def test_field_definition_is_union_predicate() -> None: """Test FieldDefinition.is_union.""" assert FieldDefinition.from_annotation(int).is_union is False assert FieldDefinition.from_annotation(Optional[int]).is_union is True assert FieldDefinition.from_annotation(Union[int, None]).is_union is True assert FieldDefinition.from_annotation(Union[int, str]).is_union is True def test_field_definition_is_optional_predicate() -> None: """Test FieldDefinition.is_optional.""" assert FieldDefinition.from_annotation(int).is_optional is False assert FieldDefinition.from_annotation(Optional[int]).is_optional is True assert FieldDefinition.from_annotation(Union[int, None]).is_optional is True assert FieldDefinition.from_annotation(Union[int, None, str]).is_optional is True assert FieldDefinition.from_annotation(Union[int, str]).is_optional is False def test_field_definition_is_dataclass_predicate() -> None: """Test FieldDefinition.is_dataclass.""" class NormalClass: ... @dataclass class NormalDataclass: ... @dataclass class GenericDataclass(Generic[T]): ... assert FieldDefinition.from_annotation(NormalDataclass).is_dataclass_type is True assert FieldDefinition.from_annotation(GenericDataclass).is_dataclass_type is True assert FieldDefinition.from_annotation(GenericDataclass[int]).is_dataclass_type is True assert FieldDefinition.from_annotation(GenericDataclass[T]).is_dataclass_type is True # type: ignore[valid-type] assert FieldDefinition.from_annotation(NormalClass).is_dataclass_type is False def test_field_definition_is_typeddict_predicate() -> None: """Test FieldDefinition.is_typeddict.""" class NormalClass: ... class TypedDictClass(TypedDict): ... assert FieldDefinition.from_annotation(NormalClass).is_typeddict_type is False assert FieldDefinition.from_annotation(TypedDictClass).is_typeddict_type is True if sys.version_info >= (3, 11): class GenericTypedDictClass(TypedDict, Generic[T]): ... assert FieldDefinition.from_annotation(GenericTypedDictClass).is_typeddict_type is True assert FieldDefinition.from_annotation(GenericTypedDictClass[int]).is_typeddict_type is True assert FieldDefinition.from_annotation(GenericTypedDictClass[T]).is_typeddict_type is True def test_field_definition_is_subclass_of() -> None: """Test FieldDefinition.is_type_of.""" assert FieldDefinition.from_annotation(bool).is_subclass_of(int) is True assert FieldDefinition.from_annotation(bool).is_subclass_of(str) is False assert FieldDefinition.from_annotation(Union[int, str]).is_subclass_of(int) is False assert FieldDefinition.from_annotation(List[int]).is_subclass_of(list) is True assert FieldDefinition.from_annotation(List[int]).is_subclass_of(int) is False assert FieldDefinition.from_annotation(Optional[int]).is_subclass_of(int) is False assert FieldDefinition.from_annotation(Union[bool, int]).is_subclass_of(int) is True def test_field_definition_has_inner_subclass_of() -> None: """Test FieldDefinition.has_type_of.""" assert FieldDefinition.from_annotation(List[int]).has_inner_subclass_of(int) is True assert FieldDefinition.from_annotation(List[int]).has_inner_subclass_of(str) is False assert FieldDefinition.from_annotation(List[Union[int, str]]).has_inner_subclass_of(int) is False def test_field_definition_equality() -> None: assert FieldDefinition.from_annotation(int) == FieldDefinition.from_annotation(int) assert FieldDefinition.from_annotation(int) == FieldDefinition.from_annotation(Annotated[int, "meta"]) assert FieldDefinition.from_annotation(int) != int assert FieldDefinition.from_annotation(List[int]) == FieldDefinition.from_annotation(List[int]) assert FieldDefinition.from_annotation(List[int]) != FieldDefinition.from_annotation(List[str]) assert FieldDefinition.from_annotation(List[str]) != FieldDefinition.from_annotation(Tuple[str]) assert FieldDefinition.from_annotation(Optional[str]) == FieldDefinition.from_annotation(Union[str, None]) def test_field_definition_hash() -> None: assert hash(FieldDefinition.from_annotation(int)) == hash(FieldDefinition.from_annotation(int)) assert hash(FieldDefinition.from_annotation(Annotated[int, False])) == hash( FieldDefinition.from_annotation(Annotated[int, False]) ) assert hash(FieldDefinition.from_annotation(Annotated[int, False])) != hash( FieldDefinition.from_annotation(Annotated[int, True]) ) assert hash(FieldDefinition.from_annotation(Union[str, int])) != hash( FieldDefinition.from_annotation(Union[int, str]) ) def test_is_required() -> None: class Foo(TypedDict): required: Required[str] not_required: NotRequired[str] class Bar(msgspec.Struct): unset: Union[str, msgspec.UnsetType] = msgspec.UNSET # noqa: UP007 with_default: str = "" with_none_default: Union[str, None] = None # noqa: UP007 assert FieldDefinition.from_annotation(get_type_hints(Foo, include_extras=True)["required"]).is_required is True assert ( FieldDefinition.from_annotation(get_type_hints(Foo, include_extras=True)["not_required"]).is_required is False ) assert FieldDefinition.from_annotation(get_type_hints(Bar, include_extras=True)["unset"]).is_required is False assert ( FieldDefinition.from_kwarg( name="foo", kwarg_definition=ParameterKwarg(required=False), annotation=str ).is_required is False ) assert ( FieldDefinition.from_kwarg( name="foo", kwarg_definition=ParameterKwarg(required=True), annotation=str ).is_required is True ) assert ( FieldDefinition.from_kwarg( name="foo", kwarg_definition=ParameterKwarg(required=None, default=""), annotation=str ).is_required is False ) assert ( FieldDefinition.from_kwarg( name="foo", kwarg_definition=ParameterKwarg(required=None), annotation=str ).is_required is True ) assert FieldDefinition.from_annotation(Optional[str]).is_required is False assert FieldDefinition.from_annotation(str).is_required is True assert FieldDefinition.from_annotation(Any).is_required is False assert FieldDefinition.from_annotation(get_type_hints(Bar)["with_default"]).is_required is True assert FieldDefinition.from_annotation(get_type_hints(Bar)["with_none_default"]).is_required is False def test_field_definition_bound_type() -> None: class Foo: pass class Bar: pass bound = TypeVar("bound", bound=Foo) multiple_bounds = TypeVar("multiple_bounds", bound=Union[Foo, Bar]) assert FieldDefinition.from_annotation(str).bound_types is None assert FieldDefinition.from_annotation(T).bound_types is None bound_types = FieldDefinition.from_annotation(bound).bound_types assert bound_types assert len(bound_types) == 1 assert isinstance(bound_types[0], FieldDefinition) assert bound_types[0].raw is Foo bound_types_union = FieldDefinition.from_annotation(multiple_bounds).bound_types assert bound_types_union assert len(bound_types_union) == 2 assert bound_types_union[0].raw is Foo assert bound_types_union[1].raw is Bar def test_nested_generic_types() -> None: V = TypeVar("V") class Foo(Generic[T]): pass class Bar(Generic[T, V]): pass class Baz(Generic[T], Bar[T, str]): pass fd_simple = FieldDefinition.from_annotation(Foo) assert fd_simple.generic_types assert len(fd_simple.generic_types) == 1 assert fd_simple.generic_types[0].raw == T fd_union = FieldDefinition.from_annotation(Bar) assert fd_union.generic_types assert len(fd_union.generic_types) == 2 assert fd_union.generic_types[0].raw == T assert fd_union.generic_types[1].raw == V fd_nested = FieldDefinition.from_annotation(Baz) assert fd_nested.generic_types assert len(fd_nested.generic_types) == 3 assert fd_nested.generic_types[0].raw == T assert fd_nested.generic_types[1].raw == T assert fd_nested.generic_types[2].raw == str @dataclass class GenericDataclass(Generic[T]): foo: T @dataclass class NormalDataclass: foo: int @pytest.mark.parametrize( ("annotation", "expected_type_hints"), ((GenericDataclass[str], {"foo": str}), (GenericDataclass, {"foo": T}), (NormalDataclass, {"foo": int})), ) def test_field_definition_get_type_hints(annotation: Any, expected_type_hints: dict[str, Any]) -> None: assert ( FieldDefinition.from_annotation(annotation).get_type_hints(include_extras=True, resolve_generics=True) == expected_type_hints ) @pytest.mark.parametrize( ("annotation", "expected_type_hints"), ((GenericDataclass[str], {"foo": T}), (GenericDataclass, {"foo": T}), (NormalDataclass, {"foo": int})), ) def test_field_definition_get_type_hints_dont_resolve_generics( annotation: Any, expected_type_hints: dict[str, Any] ) -> None: assert ( FieldDefinition.from_annotation(annotation).get_type_hints(include_extras=True, resolve_generics=False) == expected_type_hints ) def test_warn_ambiguous_default_values() -> None: with pytest.warns((LitestarWarning, DeprecationWarning)): FieldDefinition.from_annotation(Annotated[int, Parameter(default=1)], default=2) def test_warn_defaults_inside_parameter_definition() -> None: with pytest.warns(DeprecationWarning, match="Deprecated default value specification"): FieldDefinition.from_annotation(Annotated[int, Parameter(default=1)], default=1) def test_warn_default_inside_kwarg_definition_and_default_empty() -> None: with pytest.warns() as warnings: @get(sync_to_thread=False) def handler(foo: Annotated[int, Parameter(default=1)]) -> None: pass _ = handler.parsed_fn_signature (record,) = warnings assert record.category == DeprecationWarning assert "Deprecated default value specification" in str(record.message) @pytest.mark.parametrize( "annotation", [ pytest.param(TypeAliasType("IntAlias", int), id="typing.TypeAliasType"), # pyright: ignore pytest.param(TeTypeAliasType("IntAlias", int), id="typing_extensions.TypeAliasType"), # pyright: ignore ], ) def test_is_type_alias_type(annotation: Any) -> None: field_definition = FieldDefinition.from_annotation(annotation) assert field_definition.is_type_alias_type @pytest.mark.skipif(sys.version_info < (3, 12), reason="type keyword not available before 3.12") def test_unwrap_type_alias_type_keyword() -> None: ctx: dict[str, Any] = {} exec("type IntAlias = int", ctx, None) annotation = ctx["IntAlias"] field_definition = FieldDefinition.from_annotation(annotation) assert field_definition.is_type_alias_type litestar-2.16.0/tests/unit/test_utils/000077500000000000000000000000001500564371300177535ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_utils/__init__.py000066400000000000000000000000001500564371300220520ustar00rootroot00000000000000litestar-2.16.0/tests/unit/test_utils/test_compat.py000066400000000000000000000005721500564371300226530ustar00rootroot00000000000000from typing import AsyncGenerator import pytest from litestar.utils.compat import async_next async def test_async_next() -> None: async def generator() -> AsyncGenerator: yield 1 gen = generator() assert await async_next(gen) == 1 assert await async_next(gen, None) is None with pytest.raises(StopAsyncIteration): await async_next(gen) litestar-2.16.0/tests/unit/test_utils/test_dataclass.py000066400000000000000000000104171500564371300233260ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass import pytest from litestar.types import DataclassProtocol, Empty, EmptyType from litestar.utils.dataclass import ( extract_dataclass_fields, extract_dataclass_items, simple_asdict, ) from litestar.utils.predicates import ( is_dataclass_class, is_dataclass_instance, ) def test_extract_dataclass_fields_exclude_none() -> None: """Test extract_dataclass_fields with exclude_none.""" @dataclass class Foo: """A Foo model.""" bar: str | None = None assert extract_dataclass_fields(Foo(), exclude_none=True) == () def test_extract_dataclass_fields_exclude_empty() -> None: """Test extract_dataclass_fields with exclude_empty.""" @dataclass class Foo: """A Foo model.""" bar: str | EmptyType = Empty assert extract_dataclass_fields(Foo(), exclude_empty=True) == () def test_extract_dataclass_fields_include() -> None: """Test extract_dataclass_items with include.""" @dataclass class Foo: """A Foo model.""" bar: str = "bar" baz: str = "baz" fields = extract_dataclass_fields(Foo(), include={"bar"}) assert len(fields) == 1 assert fields[0].name == "bar" assert fields[0].default == "bar" def test_extract_dataclass_fields_exclude() -> None: """Test extract_dataclass_items with exclude.""" @dataclass class Foo: """A Foo model.""" bar: str = "bar" baz: str = "baz" fields = extract_dataclass_fields(Foo(), exclude={"bar"}) assert len(fields) == 1 assert fields[0].name == "baz" assert fields[0].default == "baz" def test_extract_dataclass_fields_raises_for_common_include_exclude() -> None: """Test extract_dataclass_items raises for common include and exclude.""" @dataclass class Foo: """A Foo model.""" bar: str = "bar" with pytest.raises(ValueError): extract_dataclass_fields(Foo(), include={"bar"}, exclude={"bar"}) def test_extract_dataclass_items_returns_name_value_pairs() -> None: """Test extract_dataclass_items returns name, value pairs.""" @dataclass class Foo: """A Foo model.""" bar: str = "bar" baz: str = "baz" assert extract_dataclass_items(Foo()) == (("bar", "bar"), ("baz", "baz")) def test_simple_asdict_returns_dict() -> None: """Test simple_asdict returns a dict.""" @dataclass class Foo: """A Foo model.""" bar: str = "bar" baz: str = "baz" assert simple_asdict(Foo()) == {"bar": "bar", "baz": "baz"} def test_simple_asdict_recursive() -> None: """Test simple_asdict recursive.""" @dataclass class Foo: """A Foo model.""" bar: str = "bar" baz: str = "baz" @dataclass class Bar: """A Bar model.""" foo: Foo assert simple_asdict(Bar(foo=Foo())) == {"foo": {"bar": "bar", "baz": "baz"}} def test_simple_asdict_does_not_recurse_into_collections() -> None: """Test simple_asdict does not recurse into collections.""" @dataclass class Foo: """A Foo model.""" bar: str = "bar" baz: str = "baz" @dataclass class Bar: """A Bar model.""" foo: list[Foo] foo = Foo() assert simple_asdict(Bar(foo=[foo])) == {"foo": [foo]} def test_isinstance_with_dataclass_protocol_returns_true_for_both_types_and_instances() -> None: """Test to demonstrate that dataclass types return True for isinstance checks against DataclassProtocol.""" @dataclass class Foo: """A Foo model.""" bar: str = "bar" assert isinstance(Foo(), DataclassProtocol) assert isinstance(Foo, DataclassProtocol) def test_is_dataclass_instance() -> None: """is_dataclass_instance() should return True for instances and False for types.""" @dataclass class Foo: """A Foo model.""" bar: str = "bar" assert not is_dataclass_instance(Foo) assert is_dataclass_instance(Foo()) def test_is_dataclass_class() -> None: """is_dataclass_class() should return True for types and False for instances.""" @dataclass class Foo: """A Foo model.""" bar: str = "bar" assert is_dataclass_class(Foo) assert not is_dataclass_class(Foo()) litestar-2.16.0/tests/unit/test_utils/test_deprecation.py000066400000000000000000000020711500564371300236610ustar00rootroot00000000000000from __future__ import annotations import pytest from litestar.utils.deprecation import deprecated, warn_deprecation def test_warn_deprecation() -> None: with pytest.warns( DeprecationWarning, match="Call to deprecated function 'something'. Deprecated in litestar 3. This function will be removed in the next major version", ): warn_deprecation("3", deprecated_name="something", kind="function") def test_warn_pending_deprecation() -> None: with pytest.warns( PendingDeprecationWarning, match="Call to function awaiting deprecation 'something'. Deprecated in litestar 3. This function will be removed in the next major version", ): warn_deprecation("3", deprecated_name="something", kind="function", pending=True) def test_deprecated() -> None: @deprecated("3") def foo() -> None: pass with pytest.warns( DeprecationWarning, match="Call to deprecated function 'foo'. Deprecated in litestar 3. This function will be removed in the next major version", ): foo() litestar-2.16.0/tests/unit/test_utils/test_empty.py000066400000000000000000000005321500564371300225220ustar00rootroot00000000000000from __future__ import annotations import pytest from litestar.types.empty import Empty from litestar.utils.empty import EmptyValueError, value_or_raise def test_value_or_raise_empty() -> None: with pytest.raises(EmptyValueError): value_or_raise(Empty) def test_value_or_raise_value() -> None: assert value_or_raise(1) == 1 litestar-2.16.0/tests/unit/test_utils/test_helpers.py000066400000000000000000000020671500564371300230330ustar00rootroot00000000000000from functools import partial from typing import Any, Generic, TypeVar import pytest from litestar.utils.helpers import get_name, unique_name_for_scope, unwrap_partial T = TypeVar("T") class GenericFoo(Generic[T]): ... class Foo: ... @pytest.mark.parametrize( ("value", "expected"), ( (Foo, "Foo"), (Foo(), "Foo"), (GenericFoo, "GenericFoo"), (GenericFoo[int], "GenericFoo"), (GenericFoo[T], "GenericFoo"), # type: ignore[valid-type] (GenericFoo(), "GenericFoo"), ), ) def test_get_name(value: Any, expected: str) -> None: assert get_name(value) == expected def test_unwrap_partial() -> None: def func(*args: int) -> int: return sum(args) wrapped = partial(partial(partial(func, 1), 2)) assert wrapped() == 3 assert unwrap_partial(wrapped) is func def test_unique_name_for_scope() -> None: assert unique_name_for_scope("a", []) == "a_0" assert unique_name_for_scope("a", ["a", "a_0", "b"]) == "a_1" assert unique_name_for_scope("b", ["a", "a_0", "b"]) == "b_0" litestar-2.16.0/tests/unit/test_utils/test_module_loader.py000066400000000000000000000031031500564371300241740ustar00rootroot00000000000000from pathlib import Path import pytest from _pytest.monkeypatch import MonkeyPatch from litestar.config.compression import CompressionConfig from litestar.utils.module_loader import import_string, module_to_os_path def test_import_string() -> None: cls = import_string("litestar.config.compression.CompressionConfig") assert type(cls) == type(CompressionConfig) with pytest.raises(ImportError): _ = import_string("CompressionConfigNew") _ = import_string("litestar.config.compression.CompressionConfigNew") _ = import_string("imaginary_module_that_doesnt_exist.Config") # a random nonexistent class def test_module_path(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: the_path = module_to_os_path("litestar.config.compression") assert the_path.exists() tmp_path.joinpath("simple_module.py").write_text("x = 'foo'") monkeypatch.syspath_prepend(tmp_path) os_path = module_to_os_path("simple_module") assert os_path == Path(tmp_path) with pytest.raises(TypeError): _ = module_to_os_path("litestar.config.compression.Config") _ = module_to_os_path("litestar.config.compression.extra.module") def test_import_non_existing_attribute_raises() -> None: with pytest.raises(ImportError): import_string("litestar.app.some_random_string") def test_import_string_cached(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: tmp_path.joinpath("testmodule.py").write_text("x = 'foo'") monkeypatch.chdir(tmp_path) monkeypatch.syspath_prepend(tmp_path) assert import_string("testmodule.x") == "foo" litestar-2.16.0/tests/unit/test_utils/test_path.py000066400000000000000000000023211500564371300223160ustar00rootroot00000000000000import pytest from litestar.utils.path import join_paths, normalize_path @pytest.mark.parametrize( "base,fragment, expected", ( ("/path/", "sub", "/path/sub"), ("/path/", "/sub/", "/path/sub"), ("path/", "sub", "/path/sub"), ("path", "sub", "/path/sub"), ("/path/", "sub/", "/path/sub"), ("path/", "sub/", "/path/sub"), ("path", "sub/", "/path/sub"), ("/", "/root/sub", "/root/sub"), ), ) def test_join_url_fragments(base: str, fragment: str, expected: str) -> None: assert join_paths([base, fragment]) == expected def test_join_empty_list() -> None: assert join_paths([]) == "/" def test_join_single() -> None: assert join_paths([""]) == "/" assert join_paths(["/"]) == "/" assert join_paths(["root"]) == "/root" assert join_paths(["root//other"]) == "/root/other" @pytest.mark.parametrize( "base,expected", [ ("", "/"), ("/path", "/path"), ("path/", "/path"), ("path", "/path"), ("path////path", "/path/path"), ("path//", "/path"), ("///", "/"), ], ) def test_normalize_path(base: str, expected: str) -> None: assert normalize_path(base) == expected litestar-2.16.0/tests/unit/test_utils/test_predicates.py000066400000000000000000000160751500564371300235200ustar00rootroot00000000000000from collections import defaultdict, deque from dataclasses import MISSING, dataclass from functools import partial from inspect import Signature from typing import ( Any, AsyncGenerator, Callable, ClassVar, DefaultDict, Deque, Dict, FrozenSet, Generic, Iterable, List, Mapping, MutableMapping, Optional, Sequence, Set, Tuple, TypeVar, Union, cast, ) import pytest from typing_extensions import Annotated from litestar import Response, get from litestar.pagination import CursorPagination from litestar.types import Empty from litestar.utils import is_any, is_async_callable, is_class_and_subclass, is_optional_union, is_union from litestar.utils.predicates import ( is_class_var, is_dataclass_class, is_generic, is_mapping, is_non_string_iterable, is_non_string_sequence, is_undefined_sentinel, ) class C: pass @get("/", sync_to_thread=False) def naive_handler() -> Dict[str, int]: return {} @get("/", sync_to_thread=False) def response_handler() -> Response[Any]: return Response(content=b"") class Sub(C): ... @pytest.mark.parametrize( "args, expected", ( ((Sub, C), True), ((Signature.from_callable(cast("Any", naive_handler.fn)).return_annotation, C), False), ((Signature.from_callable(cast("Any", response_handler.fn)).return_annotation, Response), True), ((Dict[str, Any], C), False), ((C(), C), False), ), ) def test_is_class_and_subclass(args: Tuple[Any, Any], expected: bool) -> None: assert is_class_and_subclass(*args) is expected @pytest.mark.parametrize( "value, expected", ( ( (Tuple[int, ...], True), (Tuple[int], True), (List[str], True), (Set[str], True), (FrozenSet[str], True), (Deque[str], True), (Sequence[str], True), (Iterable[str], True), (list, True), (tuple, True), (deque, True), (set, True), (frozenset, True), (str, False), (bytes, False), (dict, True), (Dict[str, Any], True), (Union[str, int], False), (1, False), ) ), ) def test_is_non_string_iterable(value: Any, expected: bool) -> None: assert is_non_string_iterable(value) is expected @pytest.mark.parametrize( "value, expected", ( ( (Tuple[int, ...], True), (Tuple[int], True), (List[str], True), (Set[str], True), (FrozenSet[str], True), (Deque[str], True), (Sequence[str], True), (Iterable[str], False), (list, True), (tuple, True), (deque, True), (set, True), (frozenset, True), (str, False), (bytes, False), (dict, False), (Dict[str, Any], False), (Union[str, int], False), (1, False), ) ), ) def test_is_non_string_sequence(value: Any, expected: bool) -> None: assert is_non_string_sequence(value) is expected @pytest.mark.parametrize( "value, expected", ((CursorPagination[str, str], True), (dict, False)), ) def test_is_generic(value: Any, expected: bool) -> None: assert is_generic(value) is expected @pytest.mark.parametrize( "value, expected", ( (Dict, True), (dict, True), (defaultdict, True), (DefaultDict, True), (Mapping, True), (MutableMapping, True), (list, False), (Iterable, False), ), ) def test_is_mapping(value: Any, expected: bool) -> None: assert is_mapping(value) is expected @pytest.mark.parametrize( "value, expected", ((Any, True), (Union[Any, str], True), (int, False), (dict, False), (Dict[str, Any], False), (None, False)), ) def test_is_any(value: Any, expected: bool) -> None: assert is_any(value) is expected @pytest.mark.parametrize( "value, expected", ( (Optional[int], True), (Optional[Union[int, str]], True), (Union[str, None], True), (None, False), (int, False), (Union[int, str], True), ), ) def test_is_union(value: Any, expected: bool) -> None: assert is_union(value) is expected @pytest.mark.parametrize( "value, expected", ( (Optional[int], True), (Optional[Union[int, str]], True), (Union[str, None], True), (None, False), (int, False), (Union[int, str], False), ), ) def test_is_optional_union(value: Any, expected: bool) -> None: assert is_optional_union(value) is expected @pytest.mark.parametrize( "value, expected", ( (ClassVar[int], True), (Annotated[ClassVar[int], "abc"], True), (Dict[str, int], False), (None, False), ), ) def test_is_class_var(value: Any, expected: bool) -> None: assert is_class_var(value) is expected class AsyncTestCallable: async def __call__(self, param1: int, param2: int) -> None: ... async def method(self, param1: int, param2: int) -> None: ... async def async_generator() -> AsyncGenerator[int, None]: yield 1 class SyncTestCallable: def __call__(self, param1: int, param2: int) -> None: ... def method(self, param1: int, param2: int) -> None: ... async def async_func(param1: int, param2: int) -> None: ... def sync_func(param1: int, param2: int) -> None: ... async_callable = AsyncTestCallable() sync_callable = SyncTestCallable() @pytest.mark.parametrize( "c, exp", [ (async_callable, True), (sync_callable, False), (async_callable.method, True), (sync_callable.method, False), (async_func, True), (sync_func, False), (lambda: ..., False), (AsyncTestCallable, True), (SyncTestCallable, False), (async_generator, False), ], ) def test_is_async_callable(c: Callable[[int, int], None], exp: bool) -> None: assert is_async_callable(c) is exp partial_1 = partial(c, 1) assert is_async_callable(partial_1) is exp partial_2 = partial(partial_1, 2) assert is_async_callable(partial_2) is exp def test_not_undefined_sentinel() -> None: assert is_undefined_sentinel(Signature.empty) is True assert is_undefined_sentinel(Empty) is True assert is_undefined_sentinel(Ellipsis) is True assert is_undefined_sentinel(MISSING) is True assert is_undefined_sentinel(1) is False assert is_undefined_sentinel("") is False assert is_undefined_sentinel([]) is False assert is_undefined_sentinel({}) is False assert is_undefined_sentinel(None) is False T = TypeVar("T") @dataclass class NonGenericDataclass: foo: int @dataclass class GenericDataclass(Generic[T]): foo: T class NonDataclass: ... @pytest.mark.parametrize( ("cls", "expected"), ((NonGenericDataclass, True), (GenericDataclass, True), (GenericDataclass[int], True), (NonDataclass, False)), ) def test_is_dataclass_class(cls: Any, expected: bool) -> None: assert is_dataclass_class(cls) is expected litestar-2.16.0/tests/unit/test_utils/test_scope.py000066400000000000000000000052761500564371300225070ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Callable import pytest from litestar.types.empty import Empty from litestar.utils import ( delete_litestar_scope_state, get_litestar_scope_state, set_litestar_scope_state, ) from litestar.utils.scope.state import CONNECTION_STATE_KEY, ScopeState if TYPE_CHECKING: from litestar.types.asgi_types import Scope @pytest.fixture() def scope(create_scope: Callable[..., Scope]) -> Scope: return create_scope() def test_from_scope_without_state() -> None: scope = {} # type: ignore[var-annotated] state = ScopeState.from_scope(scope) # type: ignore[arg-type] assert scope["state"][CONNECTION_STATE_KEY] is state @pytest.mark.parametrize(("pop",), [(True,), (False,)]) def test_get_litestar_scope_state_arbitrary_value(pop: bool, scope: Scope) -> None: key = "test" value = {"key": "value"} connection_state = ScopeState.from_scope(scope) connection_state._compat_ns[key] = value retrieved_value = get_litestar_scope_state(scope, key, pop=pop) assert retrieved_value == value if pop: assert connection_state._compat_ns.get(key) is None else: assert connection_state._compat_ns.get(key) == value @pytest.mark.parametrize(("pop",), [(True,), (False,)]) def test_get_litestar_scope_state_defined_value(pop: bool, scope: Scope) -> None: connection_state = ScopeState.from_scope(scope) connection_state.is_cached = True assert get_litestar_scope_state(scope, "is_cached", pop=pop) is True if pop: assert connection_state.is_cached is Empty # type: ignore[comparison-overlap] else: assert connection_state.is_cached is True def test_set_litestar_scope_state_arbitrary_value(scope: Scope) -> None: connection_state = ScopeState.from_scope(scope) set_litestar_scope_state(scope, "key", "value") assert connection_state._compat_ns["key"] == "value" def test_set_litestar_scope_state_defined_value(scope: Scope) -> None: connection_state = ScopeState.from_scope(scope) set_litestar_scope_state(scope, "is_cached", True) assert connection_state.is_cached is True def test_delete_litestar_scope_state_arbitrary_value(scope: Scope) -> None: connection_state = ScopeState.from_scope(scope) connection_state._compat_ns["key"] = "value" delete_litestar_scope_state(scope, "key") assert "key" not in connection_state._compat_ns def test_delete_litestar_scope_state_defined_value(scope: Scope) -> None: connection_state = ScopeState.from_scope(scope) connection_state.is_cached = True delete_litestar_scope_state(scope, "is_cached") assert connection_state.is_cached is Empty # type: ignore[comparison-overlap] litestar-2.16.0/tests/unit/test_utils/test_sequence.py000066400000000000000000000010201500564371300231650ustar00rootroot00000000000000from litestar.utils.sequence import find_index, unique def test_find_index() -> None: assert find_index([1, 2], lambda x: x == 2) == 1 assert find_index([1, 3], lambda x: x == 2) == -1 def test_unique() -> None: assert unique([1, 1, 1, 2]) == [1, 2] def x() -> None: pass def y() -> None: pass unique_functions = unique([x, x, y, y]) assert unique_functions == [x, y] or [y, x] my_list: list = [] assert sorted(unique([my_list, my_list, my_list])) == sorted([my_list]) litestar-2.16.0/tests/unit/test_utils/test_signature.py000066400000000000000000000206461500564371300233750ustar00rootroot00000000000000# ruff: noqa: UP006,UP007 from __future__ import annotations import inspect import warnings from inspect import Parameter from types import ModuleType from typing import Any, Callable, Generic, List, Optional, TypeVar, Union import pytest from typing_extensions import Annotated, NotRequired, Required, TypedDict, get_args, get_type_hints from litestar import Controller, Router, post from litestar.exceptions import ImproperlyConfiguredException from litestar.exceptions.base_exceptions import LitestarWarning from litestar.file_system import BaseLocalFileSystem from litestar.static_files import StaticFiles from litestar.types.asgi_types import Receive, Scope, Send from litestar.types.builtin_types import NoneType from litestar.types.empty import Empty from litestar.typing import FieldDefinition from litestar.utils.signature import ParsedSignature, add_types_to_signature_namespace, get_fn_type_hints T = TypeVar("T") U = TypeVar("U") class ConcreteT: ... def test_get_fn_type_hints_asgi_app() -> None: app = StaticFiles(is_html_mode=False, directories=[], file_system=BaseLocalFileSystem()) assert get_fn_type_hints(app) == {"scope": Scope, "receive": Receive, "send": Send, "return": NoneType} def func(a: int, b: str, c: float) -> None: ... class C: def __init__(self, a: int, b: str, c: float) -> None: ... def method(self, a: int, b: str, c: float) -> None: ... def __call__(self, a: int, b: str, c: float) -> None: ... @pytest.mark.parametrize("fn", [func, C, C(1, "2", 3.0).method, C(1, "2", 3.0)]) def test_get_fn_type_hints(fn: Any) -> None: assert get_fn_type_hints(fn) == {"a": int, "b": str, "c": float, "return": NoneType} def test_get_fn_type_hints_class_no_init() -> None: """Test that get_fn_type_hints works with classes that don't have an __init__ method. Ref: https://github.com/litestar-org/litestar/issues/1504 """ class C: ... assert get_fn_type_hints(C) == {} @pytest.mark.parametrize( ("hint",), [ ("Optional[str]",), ("Union[str, None]",), ("Union[str, int, None]",), ("Optional[Union[str, int]]",), ("Union[str, int]",), ("str",), ], ) def test_get_fn_type_hints_with_none_default(hint: str, create_module: Callable[[str], ModuleType]) -> None: mod = create_module( f""" from typing import * from typing_extensions import Annotated def fn(plain: {hint} = None, annotated: Annotated[{hint}, ...] = None) -> None: ... """ ) hints = get_fn_type_hints(mod.fn) assert hints["plain"] == get_args(hints["annotated"])[0] class _TD(TypedDict): req_int: Required[int] req_list_int: Required[List[int]] not_req_int: NotRequired[int] not_req_list_int: NotRequired[List[int]] ann_req_int: Required[Annotated[int, "foo"]] ann_req_list_int: Required[Annotated[List[int], "foo"]] test_type_hints = get_type_hints(_TD, include_extras=True) field_definition_int = FieldDefinition.from_annotation(int) def _check_field_definition(field_definition: FieldDefinition, expected: dict[str, Any]) -> None: for key, value in expected.items(): assert getattr(field_definition, key) == value def test_field_definition_from_parameter() -> None: """Test FieldDefinition.""" param = Parameter("foo", Parameter.POSITIONAL_OR_KEYWORD, annotation=int) parsed_param = FieldDefinition.from_parameter(param, {"foo": int}) assert parsed_param.name == "foo" assert parsed_param.default is Empty assert parsed_param.annotation is int def test_field_definition_from_parameter_raises_improperly_configured_if_no_annotation() -> None: """Test FieldDefinition raises ImproperlyConfigured if no annotation.""" param = Parameter("foo", Parameter.POSITIONAL_OR_KEYWORD) with pytest.raises(ImproperlyConfiguredException): FieldDefinition.from_parameter(param, {}) def test_field_definition_from_parameter_has_default_predicate() -> None: """Test FieldDefinition.has_default.""" param = Parameter("foo", Parameter.POSITIONAL_OR_KEYWORD, annotation=int) parsed_param = FieldDefinition.from_parameter(param, {"foo": int}) assert parsed_param.has_default is False param = Parameter("foo", Parameter.POSITIONAL_OR_KEYWORD, annotation=int, default=42) parsed_param = FieldDefinition.from_parameter(param, {"foo": int}) assert parsed_param.has_default is True def test_field_definition_from_parameter_annotation_property() -> None: """Test FieldDefinition.annotation.""" param = Parameter("foo", Parameter.POSITIONAL_OR_KEYWORD, annotation=int) parsed_param = FieldDefinition.from_parameter(param, {"foo": int}) assert parsed_param.annotation is int assert parsed_param.annotation is int def test_parsed_signature() -> None: """Test ParsedSignature.""" def fn(foo: int, bar: Optional[List[int]] = None) -> None: ... parsed_sig = ParsedSignature.from_fn(fn, get_fn_type_hints(fn)) assert parsed_sig.return_type.annotation is NoneType assert parsed_sig.parameters["foo"].annotation is int assert parsed_sig.parameters["bar"].args == (List[int], NoneType) assert parsed_sig.parameters["bar"].annotation == Union[List[int], NoneType] assert parsed_sig.parameters["bar"].default is None assert parsed_sig.original_signature == inspect.signature(fn) def test_add_types_to_signature_namespace() -> None: """Test add_types_to_signature_namespace.""" ns = add_types_to_signature_namespace([int, str], {}) assert ns == {"int": int, "str": str} def test_add_types_to_signature_namespace_no_warn(monkeypatch: pytest.MonkeyPatch) -> None: """Test add_types_to_signature_namespace with existing types.""" monkeypatch.delenv("LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE", raising=False) with warnings.catch_warnings(): warnings.simplefilter("error") add_types_to_signature_namespace([int], {"int": int}) def test_add_types_to_signature_namespace_with_existing_types_warn(monkeypatch: pytest.MonkeyPatch) -> None: """Test add_types_to_signature_namespace with existing types raises.""" monkeypatch.delenv("LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE", raising=False) with pytest.warns(LitestarWarning): add_types_to_signature_namespace([int], {"int": str}) def test_add_types_to_signature_namespace_warn_disabled(monkeypatch: pytest.MonkeyPatch) -> None: """Test add_types_to_signature_namespace with existing types.""" monkeypatch.setenv("LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE", "0") with warnings.catch_warnings(): warnings.simplefilter("error") add_types_to_signature_namespace([int], {"int": str}) @pytest.mark.parametrize( ("namespace", "expected"), ( ({T: int}, {"data": int, "return": int}), ({}, {"data": T, "return": T}), ({T: ConcreteT}, {"data": ConcreteT, "return": ConcreteT}), ), ) def test_using_generics_in_fn_annotations(namespace: dict[str, Any], expected: dict[str, Any]) -> None: @post(signature_namespace=namespace) def create_item(data: T) -> T: return data signature = create_item.parsed_fn_signature actual = {"data": signature.parameters["data"].annotation, "return": signature.return_type.annotation} assert actual == expected class GenericController(Controller, Generic[T]): model_class: T def __class_getitem__(cls, model_class: type) -> type: cls_dict = {"model_class": model_class} return type(f"GenericController[{model_class.__name__}", (cls,), cls_dict) def __init__(self, owner: Router) -> None: super().__init__(owner) self.signature_namespace[T] = self.model_class # type: ignore[misc] class BaseController(GenericController[T]): @post() async def create(self, data: T) -> T: return data @pytest.mark.parametrize( ("annotation_type", "expected"), ( (int, {"data": int, "return": int}), (float, {"data": float, "return": float}), (ConcreteT, {"data": ConcreteT, "return": ConcreteT}), ), ) def test_using_generics_in_controller_annotations(annotation_type: type, expected: dict[str, Any]) -> None: class ConcreteController(BaseController[annotation_type]): # type: ignore[valid-type] path = "/" controller_object = ConcreteController(owner=None) # type: ignore[arg-type] signature = controller_object.get_route_handlers()[0].parsed_fn_signature actual = {"data": signature.parameters["data"].annotation, "return": signature.return_type.annotation} assert actual == expected litestar-2.16.0/tests/unit/test_utils/test_sync.py000066400000000000000000000052541500564371300223460ustar00rootroot00000000000000from litestar.utils.sync import ensure_async_callable async def test_function_wrapper_wraps_method_correctly() -> None: class MyClass: def __init__(self) -> None: self.value = 0 def my_method(self, value: int) -> None: self.value = value instance = MyClass() wrapped_method = ensure_async_callable(instance.my_method) await wrapped_method(1) assert instance.value == 1 await wrapped_method(value=10) assert instance.value == 10 async def test_function_wrapper_wraps_async_method_correctly() -> None: class MyClass: def __init__(self) -> None: self.value = 0 async def my_method(self, value: int) -> None: self.value = value instance = MyClass() wrapped_method = ensure_async_callable(instance.my_method) await wrapped_method(1) # type: ignore[unused-coroutine] assert instance.value == 1 await wrapped_method(value=10) # type: ignore[unused-coroutine] assert instance.value == 10 async def test_function_wrapper_wraps_function_correctly() -> None: obj = {"value": 0} def my_function(new_value: int) -> None: obj["value"] = new_value wrapped_function = ensure_async_callable(my_function) await wrapped_function(1) assert obj["value"] == 1 await wrapped_function(new_value=10) assert obj["value"] == 10 async def test_function_wrapper_wraps_async_function_correctly() -> None: obj = {"value": 0} async def my_function(new_value: int) -> None: obj["value"] = new_value wrapped_function = ensure_async_callable(my_function) await wrapped_function(1) # type: ignore[unused-coroutine] assert obj["value"] == 1 await wrapped_function(new_value=10) # type: ignore[unused-coroutine] assert obj["value"] == 10 async def test_function_wrapper_wraps_class_correctly() -> None: class MyCallable: value = 0 def __call__(self, new_value: int) -> None: self.value = new_value instance = MyCallable() wrapped_class = ensure_async_callable(instance) await wrapped_class(1) assert instance.value == 1 await wrapped_class(new_value=10) assert instance.value == 10 async def test_function_wrapper_wraps_async_class_correctly() -> None: class MyCallable: value = 0 async def __call__(self, new_value: int) -> None: self.value = new_value instance = MyCallable() wrapped_class = ensure_async_callable(instance) await wrapped_class(1) # type: ignore[unused-coroutine] assert instance.value == 1 await wrapped_class(new_value=10) # type: ignore[unused-coroutine] assert instance.value == 10 litestar-2.16.0/tests/unit/test_utils/test_typing.py000066400000000000000000000117131500564371300227010ustar00rootroot00000000000000# ruff: noqa: UP007, UP006 from __future__ import annotations from sys import version_info from typing import Any, Dict, Generic, List, Optional, TypeVar, Union import pytest from typing_extensions import Annotated from litestar.utils.typing import ( expand_type_var_in_type_hint, get_origin_or_inner_type, get_type_hints_with_generics_resolved, make_non_optional_union, ) from tests.models import DataclassPerson, DataclassPet # noqa: F401 if version_info >= (3, 10): from collections import deque # noqa: F401 py_310_plus_annotation = [ (eval(tp), exp) for tp, exp in [ ("tuple[DataclassPerson, ...]", True), ("list[DataclassPerson]", True), ("deque[DataclassPerson]", True), ("tuple[DataclassPet, ...]", False), ("list[DataclassPet]", False), ("deque[DataclassPet]", False), ] ] else: py_310_plus_annotation = [] @pytest.mark.parametrize( ("annotation", "expected"), [(Union[None, str, int], Union[str, int]), (Optional[Union[str, int]], Union[str, int])] ) def test_make_non_optional_union(annotation: Any, expected: Any) -> None: assert make_non_optional_union(annotation) == expected def test_get_origin_or_inner_type() -> None: assert get_origin_or_inner_type(List[DataclassPerson]) == list assert get_origin_or_inner_type(Annotated[List[DataclassPerson], "foo"]) == list assert get_origin_or_inner_type(Annotated[Dict[str, List[DataclassPerson]], "foo"]) == dict T = TypeVar("T") V = TypeVar("V", int, str) U = TypeVar("U", bound=int) ANNOTATION = object() class Foo(Generic[T]): foo: T class BoundFoo(Generic[U]): bound_foo: U class ConstrainedFoo(Generic[V]): constrained_foo: V class AnnotatedFoo(Generic[T]): annotated_foo: Annotated[T, ANNOTATION] class UnionFoo(Generic[T, V, U]): union_foo: Union[T, bool] constrained_union_foo: Union[V, bool] bound_union_foo: Union[U, bool] class MixedFoo(Generic[T]): foo: T list_foo: List[T] normal_foo: str normal_list_foo: List[str] class NestedFoo(Generic[T]): bound_foo: BoundFoo constrained_foo: ConstrainedFoo constrained_foo_with_t: ConstrainedFoo[int] @pytest.mark.parametrize( ("annotation", "expected_type_hints"), ( (Foo[int], {"foo": int}), (BoundFoo, {"bound_foo": int}), (BoundFoo[int], {"bound_foo": int}), (ConstrainedFoo[int], {"constrained_foo": int}), (ConstrainedFoo, {"constrained_foo": Union[int, str]}), (AnnotatedFoo[int], {"annotated_foo": Annotated[int, ANNOTATION]}), ( UnionFoo[T, V, U], # type: ignore[valid-type] { "union_foo": Union[T, bool], # pyright: ignore[reportGeneralTypeIssues] "constrained_union_foo": Union[int, str, bool], "bound_union_foo": Union[int, bool], }, ), ( UnionFoo, { "union_foo": Union[T, bool], # pyright: ignore[reportGeneralTypeIssues] "constrained_union_foo": Union[int, str, bool], "bound_union_foo": Union[int, bool], }, ), ( MixedFoo[int], { "foo": int, "list_foo": List[int], "normal_foo": str, "normal_list_foo": List[str], }, ), ( NestedFoo[int], { "bound_foo": BoundFoo[int], "constrained_foo": ConstrainedFoo[Union[int, str]], # type: ignore[type-var] "constrained_foo_with_t": ConstrainedFoo[int], }, ), ), ) def test_get_type_hints_with_generics(annotation: Any, expected_type_hints: dict[str, Any]) -> None: assert get_type_hints_with_generics_resolved(annotation, include_extras=True) == expected_type_hints class ConcreteT: ... @pytest.mark.parametrize( ("type_hint", "namespace", "expected"), ( ({"arg1": T, "return": int}, {}, {"arg1": T, "return": int}), ({"arg1": T, "return": int}, None, {"arg1": T, "return": int}), ({"arg1": T, "return": int}, {U: ConcreteT}, {"arg1": T, "return": int}), ({"arg1": T, "return": int}, {T: ConcreteT}, {"arg1": ConcreteT, "return": int}), ({"arg1": T, "return": int}, {T: int}, {"arg1": int, "return": int}), ({"arg1": int, "return": int}, {}, {"arg1": int, "return": int}), ({"arg1": int, "return": int}, None, {"arg1": int, "return": int}), ({"arg1": int, "return": int}, {T: int}, {"arg1": int, "return": int}), ({"arg1": T, "return": T}, {T: ConcreteT}, {"arg1": ConcreteT, "return": ConcreteT}), ({"arg1": T, "return": T}, {T: int}, {"arg1": int, "return": int}), ), ) def test_expand_type_var_in_type_hints( type_hint: dict[str, Any], namespace: dict[str, Any] | None, expected: dict[str, Any] ) -> None: assert expand_type_var_in_type_hint(type_hint, namespace) == expected litestar-2.16.0/tests/unit/test_utils/test_version.py000066400000000000000000000024561500564371300230600ustar00rootroot00000000000000import pytest from litestar.utils.version import Version, parse_version @pytest.mark.parametrize( "raw_version,expected", [ ("2.0.0alpha1", Version(2, 0, 0, "alpha", 1)), ("2.0.0a1", Version(2, 0, 0, "alpha", 1)), # test importlib.metadata.version coercion ("2.0.0alpha2", Version(2, 0, 0, "alpha", 2)), ("2.0.0beta1", Version(2, 0, 0, "beta", 1)), ("2.0.0b1", Version(2, 0, 0, "beta", 1)), # test importlib.metadata.version coercion ("2.0.0beta2", Version(2, 0, 0, "beta", 2)), ("2.0.0rc1", Version(2, 0, 0, "rc", 1)), ("2.0.0rc2", Version(2, 0, 0, "rc", 2)), ("2.0.0", Version(2, 0, 0, "final", 0)), ("2.13.45", Version(2, 13, 45, "final", 0)), ], ) def test_parse_version(raw_version: str, expected: Version) -> None: assert parse_version(raw_version) == expected @pytest.mark.parametrize("raw_version", ["0.1", "1.0.0foo1", "1.0.0alpha", "1.0.0.0"]) def test_parse_invalid_version(raw_version: str) -> None: with pytest.raises(ValueError): parse_version(raw_version) @pytest.mark.parametrize("short,expected_output", [(True, "2.0.0"), (False, "2.0.0alpha1")]) def test_formatted(short: bool, expected_output: str) -> None: assert parse_version("2.0.0alpha1").formatted(short=short) == expected_output litestar-2.16.0/tests/unit/test_websocket_class_resolution.py000066400000000000000000000106441500564371300246300ustar00rootroot00000000000000from typing import Type, Union import pytest from litestar import Controller, Litestar, Router, WebSocket from litestar.handlers.websocket_handlers.listener import WebsocketListener, websocket_listener RouterWebSocket: Type[WebSocket] = type("RouterWebSocket", (WebSocket,), {}) ControllerWebSocket: Type[WebSocket] = type("ControllerWebSocket", (WebSocket,), {}) AppWebSocket: Type[WebSocket] = type("AppWebSocket", (WebSocket,), {}) HandlerWebSocket: Type[WebSocket] = type("HandlerWebSocket", (WebSocket,), {}) @pytest.mark.parametrize( "handler_websocket_class, controller_websocket_class, router_websocket_class, app_websocket_class, has_default_app_class, expected", ( (HandlerWebSocket, ControllerWebSocket, RouterWebSocket, AppWebSocket, True, HandlerWebSocket), (None, ControllerWebSocket, RouterWebSocket, AppWebSocket, True, ControllerWebSocket), (None, None, RouterWebSocket, AppWebSocket, True, RouterWebSocket), (None, None, None, AppWebSocket, True, AppWebSocket), (None, None, None, None, True, WebSocket), (None, None, None, None, False, WebSocket), ), ids=( "Custom class for all layers", "Custom class for all above handler layer", "Custom class for all above controller layer", "Custom class for all above router layer", "No custom class for layers", "No default class in app", ), ) def test_websocket_class_resolution_of_layers( handler_websocket_class: Union[Type[WebSocket], None], controller_websocket_class: Union[Type[WebSocket], None], router_websocket_class: Union[Type[WebSocket], None], app_websocket_class: Union[Type[WebSocket], None], has_default_app_class: bool, expected: Type[WebSocket], ) -> None: class MyController(Controller): @websocket_listener("/") def handler(self, data: str) -> None: return if controller_websocket_class: MyController.websocket_class = ControllerWebSocket router = Router(path="/", route_handlers=[MyController]) if router_websocket_class: router.websocket_class = router_websocket_class app = Litestar(route_handlers=[router]) if app_websocket_class or not has_default_app_class: app.websocket_class = app_websocket_class # type: ignore[assignment] route_handler = app.routes[0].route_handler # type: ignore[union-attr] if handler_websocket_class: route_handler.websocket_class = handler_websocket_class # type: ignore[union-attr] websocket_class = route_handler.resolve_websocket_class() # type: ignore[union-attr] assert websocket_class is expected @pytest.mark.parametrize( "handler_websocket_class, router_websocket_class, app_websocket_class, has_default_app_class, expected", ( (HandlerWebSocket, RouterWebSocket, AppWebSocket, True, HandlerWebSocket), (None, RouterWebSocket, AppWebSocket, True, RouterWebSocket), (None, None, AppWebSocket, True, AppWebSocket), (None, None, None, True, WebSocket), (None, None, None, False, WebSocket), ), ids=( "Custom class for all layers", "Custom class for all above handler layer", "Custom class for all above router layer", "No custom class for layers", "No default class in app", ), ) def test_listener_websocket_class_resolution_of_layers( handler_websocket_class: Union[Type[WebSocket], None], router_websocket_class: Union[Type[WebSocket], None], app_websocket_class: Union[Type[WebSocket], None], has_default_app_class: bool, expected: Type[WebSocket], ) -> None: class Handler(WebsocketListener): path = "/" websocket_class = handler_websocket_class def on_receive(self, data: str) -> str: # pyright: ignore return data router = Router(path="/", route_handlers=[Handler]) if router_websocket_class: router.websocket_class = router_websocket_class app = Litestar(route_handlers=[router]) if app_websocket_class or not has_default_app_class: app.websocket_class = app_websocket_class # type: ignore[assignment] route_handler = app.routes[0].route_handler # type: ignore[union-attr] if handler_websocket_class: route_handler.websocket_class = handler_websocket_class # type: ignore[union-attr] websocket_class = route_handler.resolve_websocket_class() # type: ignore[union-attr] assert websocket_class is expected litestar-2.16.0/tools/000077500000000000000000000000001500564371300145735ustar00rootroot00000000000000litestar-2.16.0/tools/__init__.py000066400000000000000000000000001500564371300166720ustar00rootroot00000000000000litestar-2.16.0/tools/build_docs.py000066400000000000000000000052461500564371300172630ustar00rootroot00000000000000from __future__ import annotations import argparse import importlib.metadata import json import os import shutil import subprocess from contextlib import contextmanager from pathlib import Path from typing import TypedDict REDIRECT_TEMPLATE = """ Page Redirection You are being redirected. If this does not work, click this link """ parser = argparse.ArgumentParser() parser.add_argument("--version", required=False) parser.add_argument("output") class VersionSpec(TypedDict): versions: list[str] latest: str @contextmanager def checkout(branch: str) -> None: subprocess.run(["git", "checkout", branch], check=True) # noqa: S603 S607 yield subprocess.run(["git", "checkout", "-"], check=True) # noqa: S603 S607 def load_version_spec() -> VersionSpec: versions_file = Path("docs/_static/versions.json") if versions_file.exists(): return json.loads(versions_file.read_text()) return {"versions": [], "latest": ""} def build(output_dir: str, version: str | None) -> None: if version is None: version = importlib.metadata.version("litestar").rsplit(".")[0] else: os.environ["_LITESTAR_DOCS_BUILD_VERSION"] = version subprocess.run(["make", "docs"], check=True) # noqa: S603 S607 output_dir = Path(output_dir) output_dir.mkdir() output_dir.joinpath(".nojekyll").touch(exist_ok=True) version_spec = load_version_spec() is_latest = version == version_spec["latest"] docs_src_path = Path("docs/_build/html") output_dir.joinpath("index.html").write_text(REDIRECT_TEMPLATE.format(target="latest")) if is_latest: shutil.copytree(docs_src_path, output_dir / "latest", dirs_exist_ok=True) shutil.copytree(docs_src_path, output_dir / version, dirs_exist_ok=True) # copy existing versions into our output dir to preserve them when cleaning the branch with checkout("gh-pages"): for other_version in [*version_spec["versions"], "latest"]: other_version_path = Path(other_version) other_version_target_path = output_dir / other_version if other_version_path.exists() and not other_version_target_path.exists(): shutil.copytree(other_version_path, other_version_target_path) def main() -> None: args = parser.parse_args() build(output_dir=args.output, version=args.version) if __name__ == "__main__": main() litestar-2.16.0/tools/prepare_release.py000066400000000000000000000364031500564371300203110ustar00rootroot00000000000000from __future__ import annotations import asyncio import contextlib import datetime import os import pathlib import re import shutil import subprocess from collections import defaultdict from dataclasses import dataclass from typing import Generator import click import httpx import msgspec _polar = "[Polar.sh](https://polar.sh/litestar-org)" _open_collective = "[OpenCollective](https://opencollective.com/litestar)" _github_sponsors = "[GitHub Sponsors](https://github.com/sponsors/litestar-org/)" class PullRequest(msgspec.Struct, kw_only=True): title: str number: int body: str created_at: str user: RepoUser merge_commit_sha: str | None = None class Comp(msgspec.Struct): sha: str class _Commit(msgspec.Struct): message: str url: str commit: _Commit class RepoUser(msgspec.Struct): login: str id: int type: str @dataclass class PRInfo: url: str title: str clean_title: str cc_type: str number: int closes: list[int] created_at: datetime.datetime description: str user: RepoUser @dataclass class ReleaseInfo: base: str release_tag: str version: str pull_requests: dict[str, list[PRInfo]] first_time_prs: list[PRInfo] @property def compare_url(self) -> str: return f"https://github.com/litestar-org/litestar/compare/{self.base}...{self.release_tag}" def _pr_number_from_commit(comp: Comp) -> int: # this is an ugly hack, but it appears to actually be the most reliably way to # extract the most "reliable" way to extract the info we want from GH ¯\_(ツ)_/¯ message_head = comp.commit.message.split("\n\n")[0] match = re.search(r"\(#(\d+)\)$", message_head) if not match: print(f"Could not find PR number in {message_head}") # noqa: T201 return int(match[1]) if match else None class _Thing: def __init__(self, *, gh_token: str, base: str, release_branch: str, tag: str, version: str) -> None: self._gh_token = gh_token self._base = base self._new_release_tag = tag self._release_branch = release_branch self._new_release_version = version self._base_client = httpx.AsyncClient( headers={ "Authorization": f"Bearer {gh_token}", } ) self._api_client = httpx.AsyncClient( headers={ **self._base_client.headers, "X-GitHub-Api-Version": "2022-11-28", "Accept": "application/vnd.github+json", }, base_url="https://api.github.com/repos/litestar-org/litestar/", ) async def get_closing_issues_references(self, pr_number: int) -> list[int]: graphql_query = """{ repository(owner: "litestar-org", name: "litestar") { pullRequest(number: %d) { id closingIssuesReferences (first: 10) { edges { node { number } } } } } }""" query = graphql_query % (pr_number,) res = await self._base_client.post("https://api.github.com/graphql", json={"query": query}) res.raise_for_status() data = res.json() return [ edge["node"]["number"] for edge in data["data"]["repository"]["pullRequest"]["closingIssuesReferences"]["edges"] ] async def _get_pr_info_for_pr(self, number: int) -> PRInfo | None: res = await self._api_client.get(f"/pulls/{number}") if res.is_client_error: click.secho( f"Could not get PR info for {number}. Fetch request returned a status of {res.status_code}", fg="yellow", ) return None res.raise_for_status() data = res.json() if not data["body"]: data["body"] = "" if not data: return None pr = msgspec.convert(data, type=PullRequest) cc_prefix, clean_title = pr.title.split(":", maxsplit=1) cc_type = cc_prefix.split("(", maxsplit=1)[0].lower() closes_issues = await self.get_closing_issues_references(pr_number=pr.number) return PRInfo( number=pr.number, cc_type=cc_type, clean_title=clean_title.strip(), url=f"https://github.com/litestar-org/litestar/pull/{pr.number}", closes=closes_issues, title=pr.title, created_at=datetime.datetime.strptime(pr.created_at, "%Y-%m-%dT%H:%M:%S%z"), description=pr.body, user=pr.user, ) async def get_prs(self) -> dict[str, list[PRInfo]]: res = await self._api_client.get(f"/compare/{self._base}...{self._release_branch}") res.raise_for_status() compares = msgspec.convert(res.json()["commits"], list[Comp]) pr_numbers = list(filter(None, (_pr_number_from_commit(c) for c in compares))) pulls = await asyncio.gather(*map(self._get_pr_info_for_pr, pr_numbers)) prs = defaultdict(list) for pr in pulls: if not pr: continue if pr.user.type != "Bot": prs[pr.cc_type].append(pr) return prs async def _get_first_time_contributions(self, prs: dict[str, list[PRInfo]]) -> list[PRInfo]: # there's probably a way to peel this information out of the GraphQL API but # this was easier to implement, and it works well enough ¯\_(ツ)_/¯ # the logic is: if we don't find a commit to the main branch, dated before the # first commit within this release, it's the user's first contribution prs_by_user_login: dict[str, list[PRInfo]] = defaultdict(list) for pr in [p for type_prs in prs.values() for p in type_prs]: prs_by_user_login[pr.user.login].append(pr) first_prs: list[PRInfo] = [] async def is_user_first_commit(user_login: str) -> None: first_pr = sorted(prs_by_user_login[user_login], key=lambda p: p.created_at)[0] res = await self._api_client.get( "/commits", params={ "author": user_login, "sha": "main", "until": first_pr.created_at.isoformat(), "per_page": 1, }, ) res.raise_for_status() if len(res.json()) == 0: first_prs.append(first_pr) await asyncio.gather(*map(is_user_first_commit, prs_by_user_login.keys())) return first_prs async def get_release_info(self) -> ReleaseInfo: prs = await self.get_prs() first_time_contributors = await self._get_first_time_contributions(prs) return ReleaseInfo( pull_requests=prs, first_time_prs=first_time_contributors, base=self._base, release_tag=self._new_release_tag, version=self._new_release_version, ) async def create_draft_release(self, body: str, release_branch: str) -> str: res = await self._api_client.post( "/releases", json={ "tag_name": self._new_release_tag, "target_commitish": release_branch, "name": self._new_release_tag, "draft": True, "body": body, }, ) res.raise_for_status() return res.json()["html_url"] # type: ignore[no-any-return] class GHReleaseWriter: def __init__(self) -> None: self.text = "" def add_line(self, line: str) -> None: self.text += line + "\n" def add_pr_descriptions(self, infos: list[PRInfo]) -> None: for info in infos: self.add_line(f"* {info.title} by @{info.user.login} in {info.url}") class ChangelogEntryWriter: def __init__(self) -> None: self.text = "" self._level = 0 self._indent = " " self._cc_type_map = {"fix": "bugfix", "feat": "feature"} def add_line(self, line: str) -> None: self.text += (self._indent * self._level) + line + "\n" def add_change(self, pr: PRInfo) -> None: with self.directive( "change", arg=pr.clean_title, type=self._cc_type_map.get(pr.cc_type, "misc"), pr=str(pr.number), issue=", ".join(map(str, pr.closes)), ): self.add_line("") for line in pr.description.splitlines(): self.add_line(line) @contextlib.contextmanager def directive(self, name: str, arg: str | None = None, **options: str) -> Generator[None, None, None]: self.add_line(f".. {name}:: {arg or ''}") self._level += 1 for key, value in options.items(): if value: self.add_line(f":{key}: {value}") yield self._level -= 1 self.add_line("") def build_gh_release_notes(release_info: ReleaseInfo) -> str: # this is for the most part just recreating GitHub's autogenerated release notes # but with three important differences: # 1. PRs are sorted into categories # 2. The conventional commit type is stripped from the title # 3. It works with our release branch process. GitHub doesn't pick up (all) commits # made there depending on how things were merged doc = GHReleaseWriter() doc.add_line("## Sponsors 🌟") doc.add_line( "⚠️ Maintainers: Please adjust business/individual sponsors section here as defined by our tier rewards" ) doc.add_line(f"- A huge 'Thank you!' to all sponsors across {_polar}, {_open_collective} and {_github_sponsors}!") doc.add_line("## What's changed") if release_info.first_time_prs: doc.add_line("\n## New contributors 🎉") for pr in release_info.first_time_prs: doc.add_line(f"* @{pr.user.login} made their first contribution in {pr.url}") if fixes := release_info.pull_requests.get("fix"): doc.add_line("\n### Bugfixes 🐛") doc.add_pr_descriptions(fixes) if features := release_info.pull_requests.get("feat"): doc.add_line("\nNew features 🚀") doc.add_pr_descriptions(features) ignore_sections = {"fix", "feat", "ci", "chore"} if other := [pr for k, prs in release_info.pull_requests.items() if k not in ignore_sections for pr in prs]: doc.add_line("\n") doc.add_line("### Other changes") doc.add_pr_descriptions(other) doc.add_line("\n**Full Changelog**") doc.add_line(release_info.compare_url) return doc.text def build_changelog_entry(release_info: ReleaseInfo, interactive: bool = False) -> str: doc = ChangelogEntryWriter() with doc.directive("changelog", release_info.version): doc.add_line(f":date: {datetime.datetime.now(tz=datetime.timezone.utc).date().isoformat()}") doc.add_line("") change_types = {"fix", "feat"} for prs in release_info.pull_requests.values(): for pr in prs: cc_type = pr.cc_type if cc_type in change_types or (interactive and click.confirm(f"Include PR #{pr.number} {pr.title!r}?")): doc.add_change(pr) else: click.secho(f"Ignoring change with type {cc_type}", fg="yellow") return doc.text def _get_gh_token() -> str: if gh_token := os.getenv("GH_TOKEN"): click.secho("Using GitHub token from env", fg="blue") return gh_token gh_executable = shutil.which("gh") if not gh_executable: click.secho("GitHub CLI not installed", fg="yellow") else: click.secho("Using GitHub CLI to obtain GitHub token", fg="blue") proc = subprocess.run([gh_executable, "auth", "token"], check=True, capture_output=True, text=True) if out := (proc.stdout or "").strip(): return out click.secho("Could not find any GitHub token", fg="red") quit(1) def _get_latest_tag() -> str: click.secho("Using latest tag", fg="blue") return subprocess.run( # noqa: S602 "git tag --sort=taggerdate | tail -1", check=True, capture_output=True, text=True, shell=True, ).stdout.strip() def _write_changelog_entry(changelog_entry: str) -> None: changelog_path = pathlib.Path("docs/release-notes/changelog.rst") changelog_lines = changelog_path.read_text().splitlines() line_no = next( (i for i, line in enumerate(changelog_lines) if line.startswith(".. changelog::")), None, ) if not line_no: raise ValueError("Changelog start not found") changelog_lines[line_no:line_no] = changelog_entry.splitlines() changelog_path.write_text("\n".join(changelog_lines)) def update_pyproject_version(new_version: str) -> None: # can't use tomli-w / tomllib for this as is messes up the formatting pyproject = pathlib.Path("pyproject.toml") content = pyproject.read_text() content = re.sub(r'(\nversion ?= ?")\d+\.\d+\.\d+("\s*\n)', rf"\g<1>{new_version}\g<2>", content) pyproject.write_text(content) @click.command() @click.argument("version") @click.option("--base", help="Previous release tag. Defaults to the latest tag") @click.option("--branch", help="Release branch", default="main") @click.option( "--gh-token", help="GitHub token. If not provided, read from the GH_TOKEN env variable. " "Alternatively, if the GitHub CLI is installed, it will be used to fetch a token", ) @click.option( "-i", "--interactive", is_flag=True, help="Interactively decide which commits should be included in the release notes", ) @click.option("-c", "--create-draft-release", is_flag=True, help="Create draft release on GitHub") @click.option( "-u", "--update-version", is_flag=True, help="Update the version number in pyproject.toml", ) def cli( base: str | None, branch: str, version: str, gh_token: str | None, interactive: bool, create_draft_release: bool, update_version: bool, ) -> None: if gh_token is None: gh_token = _get_gh_token() if base is None: base = _get_latest_tag() if not re.match(r"\d+\.\d+\.\d+", version): click.secho(f"Invalid version: {version!r}") quit(1) new_tag = f"v{version}" if update_version: click.secho("Updating version in pyproject.toml", fg="green") update_pyproject_version(version) click.secho(f"Creating release notes for tag {new_tag}, using {base} as a base", fg="cyan") thing = _Thing(gh_token=gh_token, base=base, release_branch=branch, tag=new_tag, version=version) loop = asyncio.new_event_loop() release_info = loop.run_until_complete(thing.get_release_info()) gh_release_notes = build_gh_release_notes(release_info) changelog_entry = build_changelog_entry(release_info, interactive=interactive) click.secho("Writing changelog entry", fg="green") _write_changelog_entry(changelog_entry) if create_draft_release: click.secho("Creating draft release", fg="blue") release_url = loop.run_until_complete(thing.create_draft_release(body=gh_release_notes, release_branch=branch)) click.echo(f"Draft release available at: {release_url}") else: click.echo(gh_release_notes) loop.close() if __name__ == "__main__": cli() litestar-2.16.0/tools/pypi_readme.py000066400000000000000000000016731500564371300174520ustar00rootroot00000000000000import re from pathlib import Path PYPI_BANNER = 'Litestar Logo - Light' def generate_pypi_readme() -> None: source = Path("README.md").read_text(encoding="utf-8") output = re.sub(r"[\w\W]*", PYPI_BANNER, source) output = re.sub(r"[\w\W]*", "", output) output = re.sub(r"", "", output) # ensure a newline here so the other pre-commit hooks don't complain output = output.strip() + "\n" Path("docs/PYPI_README.md").write_text(output, encoding="utf-8") if __name__ == "__main__": generate_pypi_readme() litestar-2.16.0/tools/sphinx_ext/000077500000000000000000000000001500564371300167645ustar00rootroot00000000000000litestar-2.16.0/tools/sphinx_ext/__init__.py000066400000000000000000000006451500564371300211020ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from . import changelog, missing_references, run_examples if TYPE_CHECKING: from sphinx.application import Sphinx def setup(app: Sphinx) -> dict[str, bool]: ext_config = {} ext_config.update(run_examples.setup(app)) ext_config.update(missing_references.setup(app)) ext_config.update(changelog.setup(app)) return ext_config litestar-2.16.0/tools/sphinx_ext/changelog.py000066400000000000000000000126571500564371300213000ustar00rootroot00000000000000from functools import partial from typing import Literal, cast from docutils import nodes from docutils.parsers.rst import directives from sphinx.application import Sphinx from sphinx.domains.std import StandardDomain from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import clean_astext _GH_BASE_URL = "https://github.com/litestar-org/litestar" def _parse_gh_reference(raw: str, type_: Literal["issues", "pull"]) -> list[str]: return [f"{_GH_BASE_URL}/{type_}/{r.strip()}" for r in raw.split(" ") if r] class Change(nodes.General, nodes.Element): pass class ChangeDirective(SphinxDirective): required_arguments = 1 has_content = True final_argument_whitespace = True option_spec = { "type": partial(directives.choice, values=("feature", "bugfix", "misc")), "breaking": directives.flag, "issue": directives.unchanged, "pr": directives.unchanged, } def run(self) -> list[nodes.Node]: self.assert_has_content() change_type = self.options.get("type", "misc").lower() title = self.arguments[0] change_node = nodes.container("\n".join(self.content)) change_node.attributes["classes"].append("changelog-change") self.state.nested_parse(self.content, self.content_offset, change_node) reference_links = [ *_parse_gh_reference(self.options.get("issue", ""), "issues"), *_parse_gh_reference(self.options.get("pr", ""), "pull"), ] references_paragraph = nodes.paragraph() references_paragraph.append(nodes.Text("References: ")) for i, link in enumerate(reference_links, 1): link_node = nodes.inline() link_node += nodes.reference("", link, refuri=link, external=True) references_paragraph.append(link_node) if i != len(reference_links): references_paragraph.append(nodes.Text(", ")) change_node.append(references_paragraph) return [ Change( "", change_node, title=self.state.inliner.parse(title, 0, self.state.memo, change_node)[0], change_type=change_type, breaking="breaking" in self.options, ) ] class ChangelogDirective(SphinxDirective): required_arguments = 1 has_content = True option_spec = {"date": directives.unchanged} def run(self) -> list[nodes.Node]: self.assert_has_content() version = self.arguments[0] release_date = self.options.get("date") changelog_node = nodes.section() changelog_node += nodes.title(version, version) section_target = nodes.target("", "", ids=[version]) if release_date: changelog_node += nodes.strong("", "Released: ") changelog_node += nodes.Text(release_date) self.state.nested_parse(self.content, self.content_offset, changelog_node) domain = cast(StandardDomain, self.env.get_domain("std")) change_group_lists = { "feature": nodes.definition_list(), "bugfix": nodes.definition_list(), "misc": nodes.definition_list(), } change_group_titles = {"bugfix": "Bugfixes", "feature": "Features", "misc": "Other changes"} nodes_to_remove = [] for _i, change_node in enumerate(changelog_node.findall(Change)): change_type = change_node.attributes["change_type"] title = change_node.attributes["title"] list_item = nodes.definition_list_item("") term = nodes.term() term += title target_id = f"{version}-{nodes.fully_normalize_name(title[0].astext())}" term += nodes.reference( "#", "#", refuri=f"#{target_id}", internal=True, classes=["headerlink"], ids=[target_id], ) reference_id = f"change:{target_id}" domain.anonlabels[reference_id] = self.env.docname, target_id domain.labels[reference_id] = ( self.env.docname, target_id, f"Change: {clean_astext(title[0])}", ) if change_node.attributes["breaking"]: breaking_notice = nodes.inline("breaking", "breaking") breaking_notice.attributes["classes"].append("breaking-change") term += breaking_notice list_item += [term] list_item += nodes.definition("", change_node.children[0]) nodes_to_remove.append(change_node) change_group_lists[change_type] += list_item for node in nodes_to_remove: changelog_node.remove(node) for change_group_type, change_group_list in change_group_lists.items(): if not change_group_list.children: continue section = nodes.section() target_id = f"{version}-{change_group_type}" target_node = nodes.target("", "", ids=[target_id]) title = change_group_titles[change_group_type] section += nodes.title(title, title) section += change_group_list changelog_node += [target_node, section] return [section_target, changelog_node] def setup(app: Sphinx) -> dict[str, str]: app.add_directive("changelog", ChangelogDirective) app.add_directive("change", ChangeDirective) return {} litestar-2.16.0/tools/sphinx_ext/missing_references.py000066400000000000000000000113421500564371300232110ustar00rootroot00000000000000from __future__ import annotations import ast import importlib import inspect import re from functools import cache from pathlib import Path from typing import TYPE_CHECKING, Any, Generator from docutils.utils import get_source_line if TYPE_CHECKING: from docutils.nodes import Element, Node from sphinx.addnodes import pending_xref from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment @cache def _get_module_ast(source_file: str) -> ast.AST | ast.Module: return ast.parse(Path(source_file).read_text()) def _get_import_nodes(nodes: list[ast.stmt]) -> Generator[ast.Import | ast.ImportFrom, None, None]: for node in nodes: if isinstance(node, (ast.Import, ast.ImportFrom)): yield node elif isinstance(node, ast.If) and getattr(node.test, "id", None) == "TYPE_CHECKING": yield from _get_import_nodes(node.body) @cache def get_module_global_imports(module_import_path: str, reference_target_source_obj: str) -> set[str]: """Return a set of names that are imported globally within the containing module of ``reference_target_source_obj``, including imports in ``if TYPE_CHECKING`` blocks. """ module = importlib.import_module(module_import_path) obj = getattr(module, reference_target_source_obj) try: tree = _get_module_ast(inspect.getsourcefile(obj)) except TypeError: return set() import_nodes = _get_import_nodes(tree.body) return {path.asname or path.name for import_node in import_nodes for path in import_node.names} def on_warn_missing_reference(app: Sphinx, domain: str, node: Node) -> bool | None: ignore_refs: dict[str | re.Pattern, set[str] | re.Pattern] = app.config["ignore_missing_refs"] if node.tagname != "pending_xref": # type: ignore[attr-defined] return None if not hasattr(node, "attributes"): return None attributes = node.attributes # type: ignore[attr-defined] target = attributes["reftarget"] if reference_target_source_obj := attributes.get("py:class", attributes.get("py:meth", attributes.get("py:func"))): global_names = get_module_global_imports(attributes["py:module"], reference_target_source_obj) if target in global_names: # autodoc has issues with if TYPE_CHECKING imports, and randomly with type aliases in annotations, # so we ignore those errors if we can validate that such a name exists in the containing modules global # scope or an if TYPE_CHECKING block. # see: https://github.com/sphinx-doc/sphinx/issues/11225 and https://github.com/sphinx-doc/sphinx/issues/9813 # for reference return True # for various other autodoc issues that can't be resolved automatically, we check the exact path to be able # to suppress specific warnings source_line = get_source_line(node)[0] source = source_line.split(" ")[-1] if target in ignore_refs.get(source, []): return True ignore_ref_rgs = {rg: targets for rg, targets in ignore_refs.items() if isinstance(rg, re.Pattern)} for pattern, targets in ignore_ref_rgs.items(): if not pattern.match(source): continue if isinstance(targets, set): if target in targets: return True elif targets.match(target): return True return None def on_missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref, contnode: Element) -> Any: if not hasattr(node, "attributes"): return None attributes = node.attributes # type: ignore[attr-defined] target = attributes["reftarget"] py_domain = env.domains["py"] # autodoc sometimes incorrectly resolves these types, so we try to resolve them as py:data fist and fall back to any new_node = py_domain.resolve_xref(env, node["refdoc"], app.builder, "data", target, node, contnode) if new_node is None: resolved_xrefs = py_domain.resolve_any_xref(env, node["refdoc"], app.builder, target, node, contnode) for ref in resolved_xrefs: if ref: return ref[1] return new_node def on_env_before_read_docs(app: Sphinx, env: BuildEnvironment, docnames: set[str]) -> None: tmp_examples_path = Path.cwd() / "docs/_build/_tmp_examples" tmp_examples_path.mkdir(exist_ok=True, parents=True) env.tmp_examples_path = tmp_examples_path def setup(app: Sphinx) -> dict[str, bool]: app.connect("env-before-read-docs", on_env_before_read_docs) app.connect("missing-reference", on_missing_reference) app.connect("warn-missing-reference", on_warn_missing_reference) app.add_config_value("ignore_missing_refs", default={}, rebuild=False) return {"parallel_read_safe": True, "parallel_write_safe": True} litestar-2.16.0/tools/sphinx_ext/run_examples.py000066400000000000000000000134161500564371300220450ustar00rootroot00000000000000from __future__ import annotations import importlib import logging import multiprocessing import os import re import shlex import socket import subprocess import sys import time from contextlib import contextmanager, redirect_stderr from pathlib import Path from typing import TYPE_CHECKING, Generator import httpx import uvicorn from auto_pytabs.sphinx_ext import CodeBlockOverride, LiteralIncludeOverride from docutils.nodes import Node, admonition, literal_block, title from docutils.parsers.rst import directives from sphinx.addnodes import highlightlang from litestar import Litestar if TYPE_CHECKING: from sphinx.application import Sphinx RGX_RUN = re.compile(r"# +?run:(.*)") logger = logging.getLogger("sphinx") ignore_missing_output = os.getenv("LITESTAR_DOCS_IGNORE_MISSING_EXAMPLE_OUTPUT", "") == "1" class StartupError(RuntimeError): pass def _load_app_from_path(path: Path) -> Litestar: module = importlib.import_module(str(path.with_suffix("")).replace("/", ".")) for obj in module.__dict__.values(): if isinstance(obj, Litestar): return obj raise RuntimeError(f"No Litestar app found in {path}") def _get_available_port() -> int: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: # Bind to a free port provided by the host try: sock.bind(("localhost", 0)) except OSError as e: raise StartupError("Could not find an open port") from e else: return sock.getsockname()[1] @contextmanager def run_app(path: Path) -> Generator[int, None, None]: """Run an example app from a python file. The first ``Litestar`` instance found in the file will be used as target to run. """ port = _get_available_port() app = _load_app_from_path(path) def run() -> None: with redirect_stderr(Path(os.devnull).open()): uvicorn.run(app, port=port, access_log=False) count = 0 while count < 100: proc = multiprocessing.Process(target=run) proc.start() try: for _ in range(100): try: httpx.get(f"http://127.0.0.1:{port}", timeout=0.1) break except httpx.TransportError: time.sleep(0.1) else: raise StartupError(f"App {path} failed to come online") yield port break except StartupError: time.sleep(0.2) count += 1 port = _get_available_port() finally: proc.kill() else: raise StartupError(f"App {path} failed to come online") def extract_run_args(content: str) -> tuple[str, list[list[str]]]: """Extract run args from a python file. Return the file content stripped of the run comments and a list of argument lists """ new_lines = [] run_configs = [] for line in content.splitlines(): if run_stmt_match := RGX_RUN.match(line): run_stmt = run_stmt_match.group(1).lstrip() run_configs.append(shlex.split(run_stmt)) else: new_lines.append(line) return "\n".join(new_lines), run_configs def exec_examples(app_file: Path, run_configs: list[list[str]]) -> str: """Start a server with the example application, run the specified requests against it and return their results """ results = [] with run_app(app_file) as port: for run_args in run_configs: url_path, *options = run_args args = ["curl", "-s", f"http://127.0.0.1:{port}{url_path}", *options] clean_args = ["curl", f"http://127.0.0.1:8000{url_path}", *options] proc = subprocess.run( # noqa: PLW1510, S603 args, capture_output=True, text=True, ) stdout = proc.stdout.splitlines() if not stdout: logger.debug(proc.stderr) if not ignore_missing_output: logger.error(f"Example: {app_file}:{args} yielded no results") continue result = "\n".join(("> " + (" ".join(clean_args)), *stdout)) results.append(result) return "\n".join(results) class LiteralInclude(LiteralIncludeOverride): option_spec = {**LiteralIncludeOverride.option_spec, "no-run": directives.flag} def run(self) -> list[Node]: cwd = Path.cwd() docs_dir = cwd / "docs" language = self.options.get("language") file_path = Path(self.env.relfn2path(self.arguments[0])[1]) if (language != "python" and file_path.suffix != ".py") or "no-run" in self.options: return super().run() content = file_path.read_text() clean_content, run_args = extract_run_args(content) if not run_args: return super().run() tmp_file = self.env.tmp_examples_path / str(file_path.relative_to(docs_dir)).replace("/", "_") self.arguments[0] = f"/{tmp_file.relative_to(docs_dir)!s}" tmp_file.write_text(clean_content) nodes = super().run() result = exec_examples(file_path.relative_to(cwd), run_args) nodes.append( admonition( "", title("", "Run it"), highlightlang( "", literal_block("", result), lang="shell", force=False, linenothreshold=sys.maxsize, ), literal_block("", result), ) ) return nodes def setup(app: Sphinx) -> dict[str, bool]: app.add_directive("literalinclude", LiteralInclude, override=True) app.add_directive("code-block", CodeBlockOverride, override=True) return {"parallel_read_safe": True, "parallel_write_safe": True} litestar-2.16.0/typos.toml000066400000000000000000000004671500564371300155150ustar00rootroot00000000000000[default] extend-ignore-re = ["(?Rm)^.*(#|//)\\s*codespell:ignore\\s*$"] [default.extend-words] selectin = 'selectin' odf = 'odf' splitted = 'splitted' [files] extend-exclude = [ "docs/changelog.rst", "docs/release-notes/changelog.rst", "/docs/examples/contrib/sqlalchemy/us_state_lookup.json", ] litestar-2.16.0/uv.lock000066400000000000000000026300621500564371300147500ustar00rootroot00000000000000version = 1 revision = 1 requires-python = ">=3.8, <4.0" resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'win32'", "python_full_version >= '3.9' and python_full_version < '3.11' and sys_platform != 'win32'", "python_full_version < '3.9' and sys_platform != 'win32'", "python_full_version >= '3.13' and sys_platform == 'linux'", "python_full_version >= '3.13' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version >= '3.9' and python_full_version < '3.11' and sys_platform == 'win32'", "python_full_version < '3.9' and sys_platform == 'win32'", "python_full_version >= '3.13' and sys_platform == 'win32'", ] [[package]] name = "accessible-pygments" version = "0.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/50/7055ebd9b7928eca202768bcf5f8f69d8d69dec1767c956c08f055c5b31e/accessible-pygments-0.0.4.tar.gz", hash = "sha256:e7b57a9b15958e9601c7e9eb07a440c813283545a20973f2574a5f453d0e953e", size = 11650 } wheels = [ { url = "https://files.pythonhosted.org/packages/20/d7/45cfa326d945e411c7e02764206845b05f8f5766aa7ebc812ef3bc4138cd/accessible_pygments-0.0.4-py2.py3-none-any.whl", hash = "sha256:416c6d8c1ea1c5ad8701903a20fcedf953c6e720d64f33dc47bfb2d3f2fa4e8d", size = 29320 }, ] [[package]] name = "advanced-alchemy" version = "0.26.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alembic" }, { name = "eval-type-backport", marker = "python_full_version < '3.10'" }, { name = "greenlet", marker = "sys_platform == 'darwin'" }, { name = "sqlalchemy" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3e/94/8783c58213448ae3fe44615e3efd2eb5bfc3d90a8fe47d29f9e6164681f2/advanced_alchemy-0.26.2.tar.gz", hash = "sha256:b56a9c42b7c1b1ab322cccb39b5fd0601232850b10191337f0504debc71735d2", size = 983000 } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/ef/35219f6be810e636fbe26e05af6c767d02de825075b7e633f49cf886b355/advanced_alchemy-0.26.2-py3-none-any.whl", hash = "sha256:1f9b1207e757076e13a41782e76ac32f50ab5851a88d40f27321005cd46b6b94", size = 147848 }, ] [[package]] name = "aiosqlite" version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0d/3a/22ff5415bf4d296c1e92b07fd746ad42c96781f13295a074d58e77747848/aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7", size = 21691 } wheels = [ { url = "https://files.pythonhosted.org/packages/00/c4/c93eb22025a2de6b83263dfe3d7df2e19138e345bca6f18dba7394120930/aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6", size = 15564 }, ] [[package]] name = "alabaster" version = "0.7.13" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/71/a8ee96d1fd95ca04a0d2e2d9c4081dac4c2d2b12f7ddb899c8cb9bfd1532/alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2", size = 11454 } wheels = [ { url = "https://files.pythonhosted.org/packages/64/88/c7083fc61120ab661c5d0b82cb77079fc1429d3f913a456c1c82cf4658f7/alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", size = 13857 }, ] [[package]] name = "alembic" version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.9'" }, { name = "importlib-resources", marker = "python_full_version < '3.9'" }, { name = "mako" }, { name = "sqlalchemy" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/1e/8cb8900ba1b6360431e46fb7a89922916d3a1b017a8908a7c0499cc7e5f6/alembic-1.14.0.tar.gz", hash = "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b", size = 1916172 } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/06/8b505aea3d77021b18dcbd8133aa1418f1a1e37e432a465b14c46b2c0eaa/alembic-1.14.0-py3-none-any.whl", hash = "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25", size = 233482 }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] [[package]] name = "anyio" version = "4.5.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9' and sys_platform != 'win32'", "python_full_version < '3.9' and sys_platform == 'win32'", ] dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, { name = "idna", marker = "python_full_version < '3.9'" }, { name = "sniffio", marker = "python_full_version < '3.9'" }, { name = "typing-extensions", marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/f9/9a7ce600ebe7804daf90d4d48b1c0510a4561ddce43a596be46676f82343/anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b", size = 171293 } wheels = [ { url = "https://files.pythonhosted.org/packages/1b/b4/f7e396030e3b11394436358ca258a81d6010106582422f23443c16ca1873/anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f", size = 89766 }, ] [[package]] name = "anyio" version = "4.8.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'win32'", "python_full_version >= '3.9' and python_full_version < '3.11' and sys_platform != 'win32'", "python_full_version >= '3.13' and sys_platform == 'linux'", "python_full_version >= '3.13' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version >= '3.9' and python_full_version < '3.11' and sys_platform == 'win32'", "python_full_version >= '3.13' and sys_platform == 'win32'", ] dependencies = [ { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, { name = "idna", marker = "python_full_version >= '3.9'" }, { name = "sniffio", marker = "python_full_version >= '3.9'" }, { name = "typing-extensions", marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } wheels = [ { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, ] [[package]] name = "apeye" version = "1.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "apeye-core" }, { name = "domdf-python-tools" }, { name = "platformdirs" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4f/6b/cc65e31843d7bfda8313a9dc0c77a21e8580b782adca53c7cb3e511fe023/apeye-1.4.1.tar.gz", hash = "sha256:14ea542fad689e3bfdbda2189a354a4908e90aee4bf84c15ab75d68453d76a36", size = 99219 } wheels = [ { url = "https://files.pythonhosted.org/packages/89/7b/2d63664777b3e831ac1b1d8df5bbf0b7c8bee48e57115896080890527b1b/apeye-1.4.1-py3-none-any.whl", hash = "sha256:44e58a9104ec189bf42e76b3a7fe91e2b2879d96d48e9a77e5e32ff699c9204e", size = 107989 }, ] [[package]] name = "apeye-core" version = "1.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "domdf-python-tools" }, { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/4c/4f108cfd06923bd897bf992a6ecb6fb122646ee7af94d7f9a64abd071d4c/apeye_core-1.1.5.tar.gz", hash = "sha256:5de72ed3d00cc9b20fea55e54b7ab8f5ef8500eb33a5368bc162a5585e238a55", size = 96511 } wheels = [ { url = "https://files.pythonhosted.org/packages/77/9f/fa9971d2a0c6fef64c87ba362a493a4f230eff4ea8dfb9f4c7cbdf71892e/apeye_core-1.1.5-py3-none-any.whl", hash = "sha256:dc27a93f8c9e246b3b238c5ea51edf6115ab2618ef029b9f2d9a190ec8228fbf", size = 99286 }, ] [[package]] name = "asgiref" version = "3.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } wheels = [ { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, ] [[package]] name = "async-timeout" version = "5.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, ] [[package]] name = "asyncpg" version = "0.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 } wheels = [ { url = "https://files.pythonhosted.org/packages/bb/07/1650a8c30e3a5c625478fa8aafd89a8dd7d85999bf7169b16f54973ebf2c/asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e", size = 673143 }, { url = "https://files.pythonhosted.org/packages/a0/9a/568ff9b590d0954553c56806766914c149609b828c426c5118d4869111d3/asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0", size = 645035 }, { url = "https://files.pythonhosted.org/packages/de/11/6f2fa6c902f341ca10403743701ea952bca896fc5b07cc1f4705d2bb0593/asyncpg-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3152fef2e265c9c24eec4ee3d22b4f4d2703d30614b0b6753e9ed4115c8a146f", size = 2912384 }, { url = "https://files.pythonhosted.org/packages/83/83/44bd393919c504ffe4a82d0aed8ea0e55eb1571a1dea6a4922b723f0a03b/asyncpg-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7255812ac85099a0e1ffb81b10dc477b9973345793776b128a23e60148dd1af", size = 2947526 }, { url = "https://files.pythonhosted.org/packages/08/85/e23dd3a2b55536eb0ded80c457b0693352262dc70426ef4d4a6fc994fa51/asyncpg-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:578445f09f45d1ad7abddbff2a3c7f7c291738fdae0abffbeb737d3fc3ab8b75", size = 2895390 }, { url = "https://files.pythonhosted.org/packages/9b/26/fa96c8f4877d47dc6c1864fef5500b446522365da3d3d0ee89a5cce71a3f/asyncpg-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c42f6bb65a277ce4d93f3fba46b91a265631c8df7250592dd4f11f8b0152150f", size = 3015630 }, { url = "https://files.pythonhosted.org/packages/34/00/814514eb9287614188a5179a8b6e588a3611ca47d41937af0f3a844b1b4b/asyncpg-0.30.0-cp310-cp310-win32.whl", hash = "sha256:aa403147d3e07a267ada2ae34dfc9324e67ccc4cdca35261c8c22792ba2b10cf", size = 568760 }, { url = "https://files.pythonhosted.org/packages/f0/28/869a7a279400f8b06dd237266fdd7220bc5f7c975348fea5d1e6909588e9/asyncpg-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb622c94db4e13137c4c7f98834185049cc50ee01d8f657ef898b6407c7b9c50", size = 625764 }, { url = "https://files.pythonhosted.org/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506 }, { url = "https://files.pythonhosted.org/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922 }, { url = "https://files.pythonhosted.org/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565 }, { url = "https://files.pythonhosted.org/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962 }, { url = "https://files.pythonhosted.org/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791 }, { url = "https://files.pythonhosted.org/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696 }, { url = "https://files.pythonhosted.org/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358 }, { url = "https://files.pythonhosted.org/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375 }, { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 }, { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 }, { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 }, { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 }, { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 }, { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 }, { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 }, { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 }, { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 }, { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 }, { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 }, { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 }, { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 }, { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 }, { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 }, { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 }, { url = "https://files.pythonhosted.org/packages/82/0a/71e58396323b70e2e65cc8e9b48d87837bd405cf40585e51d0a78dea1124/asyncpg-0.30.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29ff1fc8b5bf724273782ff8b4f57b0f8220a1b2324184846b39d1ab4122031d", size = 671916 }, { url = "https://files.pythonhosted.org/packages/fc/2c/1ac00d77a31c62684332b74a478390e6976803a49bc5038064f4ba0cecc0/asyncpg-0.30.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64e899bce0600871b55368b8483e5e3e7f1860c9482e7f12e0a771e747988168", size = 644256 }, { url = "https://files.pythonhosted.org/packages/96/aa/c698df40084474cd4afc3f967cc7353dfecad9b4a0a7fbd8f9bcf1f9ac7a/asyncpg-0.30.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b290f4726a887f75dcd1b3006f484252db37602313f806e9ffc4e5996cfe5cb", size = 3339515 }, { url = "https://files.pythonhosted.org/packages/5f/32/db782ec573549ccac59ca23832d4dc045408571b1df37d9209ac86e22298/asyncpg-0.30.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f86b0e2cd3f1249d6fe6fd6cfe0cd4538ba994e2d8249c0491925629b9104d0f", size = 3367592 }, { url = "https://files.pythonhosted.org/packages/80/da/77118d538ca70256955e5e137225f075906593b03793b4defb2b80a8401a/asyncpg-0.30.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:393af4e3214c8fa4c7b86da6364384c0d1b3298d45803375572f415b6f673f38", size = 3302393 }, { url = "https://files.pythonhosted.org/packages/b7/50/7adbd4f47e75af969148df58e279e25e5a4c0f9f059cde8710df42180882/asyncpg-0.30.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fd4406d09208d5b4a14db9a9dbb311b6d7aeeab57bded7ed2f8ea41aeef39b34", size = 3434078 }, { url = "https://files.pythonhosted.org/packages/52/49/fc25f8a28bc337824f4bfea8abd8ffa8057f3d0980d85d82cba3ed37f841/asyncpg-0.30.0-cp38-cp38-win32.whl", hash = "sha256:0b448f0150e1c3b96cb0438a0d0aa4871f1472e58de14a3ec320dbb2798fb0d4", size = 569762 }, { url = "https://files.pythonhosted.org/packages/a7/07/cc33b589a31e1e539c7970666e52daaac4e4266fc78a3e78dd927057b936/asyncpg-0.30.0-cp38-cp38-win_amd64.whl", hash = "sha256:f23b836dd90bea21104f69547923a02b167d999ce053f3d502081acea2fba15b", size = 628443 }, { url = "https://files.pythonhosted.org/packages/b4/82/d94f3ed6921136a0ef40a825740eda19437ccdad7d92d924302dca1d5c9e/asyncpg-0.30.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f4e83f067b35ab5e6371f8a4c93296e0439857b4569850b178a01385e82e9ad", size = 673026 }, { url = "https://files.pythonhosted.org/packages/4e/db/7db8b73c5d86ec9a21807f405e0698f8f637a8a3ca14b7b6fd4259b66bcf/asyncpg-0.30.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5df69d55add4efcd25ea2a3b02025b669a285b767bfbf06e356d68dbce4234ff", size = 644732 }, { url = "https://files.pythonhosted.org/packages/eb/a0/1f1910659d08050cb3e8f7d82b32983974798d7fd4ddf7620b8e2023d4ac/asyncpg-0.30.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3479a0d9a852c7c84e822c073622baca862d1217b10a02dd57ee4a7a081f708", size = 2911761 }, { url = "https://files.pythonhosted.org/packages/4d/53/5aa0d92488ded50bab2b6626430ed9743b0b7e2d864a2b435af1ccbf219a/asyncpg-0.30.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26683d3b9a62836fad771a18ecf4659a30f348a561279d6227dab96182f46144", size = 2946595 }, { url = "https://files.pythonhosted.org/packages/c5/cd/d6d548d8ee721f4e0f7fbbe509bbac140d556c2e45814d945540c96cf7d4/asyncpg-0.30.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1b982daf2441a0ed314bd10817f1606f1c28b1136abd9e4f11335358c2c631cb", size = 2890135 }, { url = "https://files.pythonhosted.org/packages/46/f0/28df398b685dabee20235e24880e1f6486d84ae7e6b0d11bdebc17740e7a/asyncpg-0.30.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1c06a3a50d014b303e5f6fc1e5f95eb28d2cee89cf58384b700da621e5d5e547", size = 3011889 }, { url = "https://files.pythonhosted.org/packages/c8/07/8c7ffe6fe8bccff9b12fcb6410b1b2fa74b917fd8b837806a40217d5228b/asyncpg-0.30.0-cp39-cp39-win32.whl", hash = "sha256:1b11a555a198b08f5c4baa8f8231c74a366d190755aa4f99aacec5970afe929a", size = 569406 }, { url = "https://files.pythonhosted.org/packages/05/51/f59e4df6d9b8937530d4b9fdee1598b93db40c631fe94ff3ce64207b7a95/asyncpg-0.30.0-cp39-cp39-win_amd64.whl", hash = "sha256:8b684a3c858a83cd876f05958823b68e8d14ec01bb0c0d14a6704c5bf9711773", size = 626581 }, ] [[package]] name = "asyncpg-stubs" version = "0.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asyncpg" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/92/fb/08e27995b5444c888d58040203a2a73b9151855e267d171b3aff69033e7d/asyncpg_stubs-0.30.0.tar.gz", hash = "sha256:8bfe20f1b1e24a19674152ec9abbcc2df72c01e78af696f44fc275d56fe335ba", size = 20946 } wheels = [ { url = "https://files.pythonhosted.org/packages/db/92/fb8ba4baca7f02ae627ad1f3b84fff8c550c93bd71fd7f993e6792d5718e/asyncpg_stubs-0.30.0-py3-none-any.whl", hash = "sha256:1eac258c10fc45a781729913a2fcfba775888bed160ae47f55fe0964d639e9cd", size = 26816 }, ] [[package]] name = "attrs" version = "24.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, ] [[package]] name = "auto-pytabs" version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruff" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f9/ff/f5752f43f659ee62dd563af5bb0fe0a63111c3ff4708e9596279385f52bb/auto_pytabs-0.5.0.tar.gz", hash = "sha256:30087831c8be5b2314e663efd06c96b84c096572a060a492540f586362cc4326", size = 15362 } wheels = [ { url = "https://files.pythonhosted.org/packages/6e/df/e76dc1261882283f7ae93ebbf75438e85d8bb713a51dbbd5d17fef29e607/auto_pytabs-0.5.0-py3-none-any.whl", hash = "sha256:e59fb6d2f8b41b05d0906a322dd4bb1a86749d429483ec10036587de3657dcc8", size = 13748 }, ] [package.optional-dependencies] sphinx = [ { name = "sphinx" }, ] [[package]] name = "autobahn" version = "23.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "hyperlink" }, { name = "setuptools" }, { name = "txaio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/99/b6e0ffa0e8bafe9dfae1c9ab46d44d07317cbf297fbf8f07aff8a80e5bd8/autobahn-23.1.2.tar.gz", hash = "sha256:c5ef8ca7422015a1af774a883b8aef73d4954c9fcd182c9b5244e08e973f7c3a", size = 480717 } [[package]] name = "autodocsumm" version = "0.2.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/03/96/92afe8a7912b327c01f0a8b6408c9556ee13b1aba5b98d587ac7327ff32d/autodocsumm-0.2.14.tar.gz", hash = "sha256:2839a9d4facc3c4eccd306c08695540911042b46eeafcdc3203e6d0bab40bc77", size = 46357 } wheels = [ { url = "https://files.pythonhosted.org/packages/87/bc/3f66af9beb683728e06ca08797e4e9d3e44f432f339718cae3ba856a9cad/autodocsumm-0.2.14-py3-none-any.whl", hash = "sha256:3bad8717fc5190802c60392a7ab04b9f3c97aa9efa8b3780b3d81d615bfe5dc0", size = 14640 }, ] [[package]] name = "automat" version = "24.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8d/2d/ede4ad7fc34ab4482389fa3369d304f2fa22e50770af706678f6a332fa82/automat-24.8.1.tar.gz", hash = "sha256:b34227cf63f6325b8ad2399ede780675083e439b20c323d376373d8ee6306d88", size = 128679 } wheels = [ { url = "https://files.pythonhosted.org/packages/af/cc/55a32a2c98022d88812b5986d2a92c4ff3ee087e83b712ebc703bba452bf/Automat-24.8.1-py3-none-any.whl", hash = "sha256:bf029a7bc3da1e2c24da2343e7598affaa9f10bf0ab63ff808566ce90551e02a", size = 42585 }, ] [[package]] name = "babel" version = "2.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytz", marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } wheels = [ { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, ] [[package]] name = "backports-zoneinfo" version = "0.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ad/85/475e514c3140937cf435954f78dedea1861aeab7662d11de232bdaa90655/backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2", size = 74098 } wheels = [ { url = "https://files.pythonhosted.org/packages/4a/6d/eca004eeadcbf8bd64cc96feb9e355536147f0577420b44d80c7cac70767/backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987", size = 35816 }, { url = "https://files.pythonhosted.org/packages/c1/8f/9b1b920a6a95652463143943fa3b8c000cb0b932ab463764a6f2a2416560/backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1", size = 72147 }, { url = "https://files.pythonhosted.org/packages/1a/ab/3e941e3fcf1b7d3ab3d0233194d99d6a0ed6b24f8f956fc81e47edc8c079/backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9", size = 74033 }, { url = "https://files.pythonhosted.org/packages/c0/34/5fdb0a3a28841d215c255be8fc60b8666257bb6632193c86fd04b63d4a31/backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328", size = 36803 }, { url = "https://files.pythonhosted.org/packages/78/cc/e27fd6493bbce8dbea7e6c1bc861fe3d3bc22c4f7c81f4c3befb8ff5bfaf/backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6", size = 38967 }, ] [[package]] name = "beanie" version = "1.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "lazy-model" }, { name = "motor" }, { name = "pydantic" }, { name = "toml" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ca/d1/ebd474f9e552e32378bad032e2dcce9ba78473b488ef29c9295b1e8d5c23/beanie-1.27.0.tar.gz", hash = "sha256:a5eee40f1e52214afeb8558c0823d7504856884770c3d56fc3cd5765efb87314", size = 169370 } wheels = [ { url = "https://files.pythonhosted.org/packages/55/4d/9b302c451625e3b570b0dcafd157d92b633f96b4b17eca1c88a081b1a7b9/beanie-1.27.0-py3-none-any.whl", hash = "sha256:2cc6762bdd59b9040dd004ecbc7d4fd5ddd22e52743915e38d1f0f92f276bcaf", size = 84066 }, ] [[package]] name = "beautifulsoup4" version = "4.12.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 } wheels = [ { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, ] [[package]] name = "black" version = "24.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "mypy-extensions" }, { name = "packaging" }, { name = "pathspec" }, { name = "platformdirs" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/b0/46fb0d4e00372f4a86a6f8efa3cb193c9f64863615e39010b1477e010578/black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", size = 644810 } wheels = [ { url = "https://files.pythonhosted.org/packages/47/6e/74e29edf1fba3887ed7066930a87f698ffdcd52c5dbc263eabb06061672d/black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", size = 1632092 }, { url = "https://files.pythonhosted.org/packages/ab/49/575cb6c3faee690b05c9d11ee2e8dba8fbd6d6c134496e644c1feb1b47da/black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", size = 1457529 }, { url = "https://files.pythonhosted.org/packages/7a/b4/d34099e95c437b53d01c4aa37cf93944b233066eb034ccf7897fa4e5f286/black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", size = 1757443 }, { url = "https://files.pythonhosted.org/packages/87/a0/6d2e4175ef364b8c4b64f8441ba041ed65c63ea1db2720d61494ac711c15/black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", size = 1418012 }, { url = "https://files.pythonhosted.org/packages/08/a6/0a3aa89de9c283556146dc6dbda20cd63a9c94160a6fbdebaf0918e4a3e1/black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1", size = 1615080 }, { url = "https://files.pythonhosted.org/packages/db/94/b803d810e14588bb297e565821a947c108390a079e21dbdcb9ab6956cd7a/black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", size = 1438143 }, { url = "https://files.pythonhosted.org/packages/a5/b5/f485e1bbe31f768e2e5210f52ea3f432256201289fd1a3c0afda693776b0/black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", size = 1738774 }, { url = "https://files.pythonhosted.org/packages/a8/69/a000fc3736f89d1bdc7f4a879f8aaf516fb03613bb51a0154070383d95d9/black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", size = 1427503 }, { url = "https://files.pythonhosted.org/packages/a2/a8/05fb14195cfef32b7c8d4585a44b7499c2a4b205e1662c427b941ed87054/black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", size = 1646132 }, { url = "https://files.pythonhosted.org/packages/41/77/8d9ce42673e5cb9988f6df73c1c5c1d4e9e788053cccd7f5fb14ef100982/black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", size = 1448665 }, { url = "https://files.pythonhosted.org/packages/cc/94/eff1ddad2ce1d3cc26c162b3693043c6b6b575f538f602f26fe846dfdc75/black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", size = 1762458 }, { url = "https://files.pythonhosted.org/packages/28/ea/18b8d86a9ca19a6942e4e16759b2fa5fc02bbc0eb33c1b866fcd387640ab/black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", size = 1436109 }, { url = "https://files.pythonhosted.org/packages/9f/d4/ae03761ddecc1a37d7e743b89cccbcf3317479ff4b88cfd8818079f890d0/black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", size = 1617322 }, { url = "https://files.pythonhosted.org/packages/14/4b/4dfe67eed7f9b1ddca2ec8e4418ea74f0d1dc84d36ea874d618ffa1af7d4/black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", size = 1442108 }, { url = "https://files.pythonhosted.org/packages/97/14/95b3f91f857034686cae0e73006b8391d76a8142d339b42970eaaf0416ea/black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", size = 1745786 }, { url = "https://files.pythonhosted.org/packages/95/54/68b8883c8aa258a6dde958cd5bdfada8382bec47c5162f4a01e66d839af1/black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", size = 1426754 }, { url = "https://files.pythonhosted.org/packages/13/b2/b3f24fdbb46f0e7ef6238e131f13572ee8279b70f237f221dd168a9dba1a/black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", size = 1631706 }, { url = "https://files.pythonhosted.org/packages/d9/35/31010981e4a05202a84a3116423970fd1a59d2eda4ac0b3570fbb7029ddc/black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", size = 1457429 }, { url = "https://files.pythonhosted.org/packages/27/25/3f706b4f044dd569a20a4835c3b733dedea38d83d2ee0beb8178a6d44945/black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", size = 1756488 }, { url = "https://files.pythonhosted.org/packages/63/72/79375cd8277cbf1c5670914e6bd4c1b15dea2c8f8e906dc21c448d0535f0/black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", size = 1417721 }, { url = "https://files.pythonhosted.org/packages/27/1e/83fa8a787180e1632c3d831f7e58994d7aaf23a0961320d21e84f922f919/black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", size = 206504 }, ] [[package]] name = "brotli" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270 } wheels = [ { url = "https://files.pythonhosted.org/packages/6d/3a/dbf4fb970c1019a57b5e492e1e0eae745d32e59ba4d6161ab5422b08eefe/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", size = 873045 }, { url = "https://files.pythonhosted.org/packages/dd/11/afc14026ea7f44bd6eb9316d800d439d092c8d508752055ce8d03086079a/Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", size = 446218 }, { url = "https://files.pythonhosted.org/packages/36/83/7545a6e7729db43cb36c4287ae388d6885c85a86dd251768a47015dfde32/Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", size = 2903872 }, { url = "https://files.pythonhosted.org/packages/32/23/35331c4d9391fcc0f29fd9bec2c76e4b4eeab769afbc4b11dd2e1098fb13/Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", size = 2941254 }, { url = "https://files.pythonhosted.org/packages/3b/24/1671acb450c902edb64bd765d73603797c6c7280a9ada85a195f6b78c6e5/Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", size = 2857293 }, { url = "https://files.pythonhosted.org/packages/d5/00/40f760cc27007912b327fe15bf6bfd8eaecbe451687f72a8abc587d503b3/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", size = 3002385 }, { url = "https://files.pythonhosted.org/packages/b8/cb/8aaa83f7a4caa131757668c0fb0c4b6384b09ffa77f2fba9570d87ab587d/Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", size = 2911104 }, { url = "https://files.pythonhosted.org/packages/bc/c4/65456561d89d3c49f46b7fbeb8fe6e449f13bdc8ea7791832c5d476b2faf/Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", size = 2809981 }, { url = "https://files.pythonhosted.org/packages/05/1b/cf49528437bae28abce5f6e059f0d0be6fecdcc1d3e33e7c54b3ca498425/Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", size = 2935297 }, { url = "https://files.pythonhosted.org/packages/81/ff/190d4af610680bf0c5a09eb5d1eac6e99c7c8e216440f9c7cfd42b7adab5/Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", size = 2930735 }, { url = "https://files.pythonhosted.org/packages/80/7d/f1abbc0c98f6e09abd3cad63ec34af17abc4c44f308a7a539010f79aae7a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", size = 2933107 }, { url = "https://files.pythonhosted.org/packages/34/ce/5a5020ba48f2b5a4ad1c0522d095ad5847a0be508e7d7569c8630ce25062/Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", size = 2845400 }, { url = "https://files.pythonhosted.org/packages/44/89/fa2c4355ab1eecf3994e5a0a7f5492c6ff81dfcb5f9ba7859bd534bb5c1a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", size = 3031985 }, { url = "https://files.pythonhosted.org/packages/af/a4/79196b4a1674143d19dca400866b1a4d1a089040df7b93b88ebae81f3447/Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", size = 2927099 }, { url = "https://files.pythonhosted.org/packages/e9/54/1c0278556a097f9651e657b873ab08f01b9a9ae4cac128ceb66427d7cd20/Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", size = 333172 }, { url = "https://files.pythonhosted.org/packages/f7/65/b785722e941193fd8b571afd9edbec2a9b838ddec4375d8af33a50b8dab9/Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", size = 357255 }, { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068 }, { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244 }, { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500 }, { url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950 }, { url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527 }, { url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489 }, { url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080 }, { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051 }, { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172 }, { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023 }, { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871 }, { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784 }, { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905 }, { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467 }, { url = "https://files.pythonhosted.org/packages/e7/71/8f161dee223c7ff7fea9d44893fba953ce97cf2c3c33f78ba260a91bcff5/Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", size = 333169 }, { url = "https://files.pythonhosted.org/packages/02/8a/fece0ee1057643cb2a5bbf59682de13f1725f8482b2c057d4e799d7ade75/Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", size = 357253 }, { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693 }, { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489 }, { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081 }, { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244 }, { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505 }, { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152 }, { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252 }, { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955 }, { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304 }, { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452 }, { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751 }, { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757 }, { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146 }, { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055 }, { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102 }, { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029 }, { url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276 }, { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255 }, { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681 }, { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475 }, { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173 }, { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803 }, { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946 }, { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707 }, { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231 }, { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157 }, { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122 }, { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206 }, { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804 }, { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517 }, { url = "https://files.pythonhosted.org/packages/34/1b/16114a20c0a43c20331f03431178ed8b12280b12c531a14186da0bc5b276/Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3", size = 873053 }, { url = "https://files.pythonhosted.org/packages/36/49/2afe4aa5a23a13dad4c7160ae574668eec58b3c80b56b74a826cebff7ab8/Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208", size = 446211 }, { url = "https://files.pythonhosted.org/packages/10/9d/6463edb80a9e0a944f70ed0c4d41330178526626d7824f729e81f78a3f24/Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7", size = 2904604 }, { url = "https://files.pythonhosted.org/packages/a4/bd/cfaac88c14f97d9e1f2e51a304c3573858548bb923d011b19f76b295f81c/Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751", size = 2941707 }, { url = "https://files.pythonhosted.org/packages/60/3f/2618fa887d7af6828246822f10d9927244dab22db7a96ec56041a2fd1fbd/Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48", size = 2672420 }, { url = "https://files.pythonhosted.org/packages/e7/41/1c6d15c8d5b55db2c3c249c64c352c8a1bc97f5e5c55183f5930866fc012/Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619", size = 2757410 }, { url = "https://files.pythonhosted.org/packages/6c/5b/ca72fd8aa1278dfbb12eb320b6e409aefabcd767b85d607c9d54c9dadd1a/Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97", size = 2911143 }, { url = "https://files.pythonhosted.org/packages/b1/53/110657f4017d34a2e9a96d9630a388ad7e56092023f1d46d11648c6c0bce/Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a", size = 2809968 }, { url = "https://files.pythonhosted.org/packages/3f/2a/fbc95429b45e4aa4a3a3a815e4af11772bfd8ef94e883dcff9ceaf556662/Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088", size = 2935402 }, { url = "https://files.pythonhosted.org/packages/4e/52/02acd2992e5a2c10adf65fa920fad0c29e11e110f95eeb11bcb20342ecd2/Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596", size = 2931208 }, { url = "https://files.pythonhosted.org/packages/6b/35/5d258d1aeb407e1fc6fcbbff463af9c64d1ecc17042625f703a1e9d22ec5/Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7", size = 2933171 }, { url = "https://files.pythonhosted.org/packages/cc/58/b25ca26492da9880e517753967685903c6002ddc2aade93d6e56df817b30/Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5", size = 2845347 }, { url = "https://files.pythonhosted.org/packages/12/cf/91b84beaa051c9376a22cc38122dc6fbb63abcebd5a4b8503e9c388de7b1/Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943", size = 3031668 }, { url = "https://files.pythonhosted.org/packages/38/05/04a57ba75aed972be0c6ad5f2f5ea34c83f5fecf57787cc6e54aac21a323/Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a", size = 2926949 }, { url = "https://files.pythonhosted.org/packages/c9/2f/fbe6938f33d2cd9b7d7fb591991eb3fb57ffa40416bb873bbbacab60a381/Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b", size = 333179 }, { url = "https://files.pythonhosted.org/packages/39/a5/9322c8436072e77b8646f6bde5e19ee66f62acf7aa01337ded10777077fa/Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0", size = 357254 }, { url = "https://files.pythonhosted.org/packages/1b/aa/aa6e0c9848ee4375514af0b27abf470904992939b7363ae78fc8aca8a9a8/Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", size = 873048 }, { url = "https://files.pythonhosted.org/packages/ae/32/38bba1a8bef9ecb1cda08439fd28d7e9c51aff13b4783a4f1610da90b6c2/Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f", size = 446207 }, { url = "https://files.pythonhosted.org/packages/3c/6a/14cc20ddc53efc274601c8195791a27cfb7acc5e5134e0f8c493a8b8821a/Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9", size = 2903803 }, { url = "https://files.pythonhosted.org/packages/9a/26/62b2d894d4e82d7a7f4e0bb9007a42bbc765697a5679b43186acd68d7a79/Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf", size = 2941149 }, { url = "https://files.pythonhosted.org/packages/a9/ca/00d55bbdd8631236c61777742d8a8454cf6a87eb4125cad675912c68bec7/Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac", size = 2672253 }, { url = "https://files.pythonhosted.org/packages/e2/e6/4a730f6e5b5d538e92d09bc51bf69119914f29a222f9e1d65ae4abb27a4e/Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578", size = 2757005 }, { url = "https://files.pythonhosted.org/packages/cb/6b/8cf297987fe3c1bf1c87f0c0b714af2ce47092b8d307b9f6ecbc65f98968/Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474", size = 2910658 }, { url = "https://files.pythonhosted.org/packages/2c/1f/be9443995821c933aad7159803f84ef4923c6f5b72c2affd001192b310fc/Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c", size = 2809728 }, { url = "https://files.pythonhosted.org/packages/76/2f/213bab6efa902658c80a1247142d42b138a27ccdd6bade49ca9cd74e714a/Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d", size = 2935043 }, { url = "https://files.pythonhosted.org/packages/27/89/bbb14fa98e895d1e601491fba54a5feec167d262f0d3d537a3b0d4cd0029/Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59", size = 2930639 }, { url = "https://files.pythonhosted.org/packages/14/87/03a6d6e1866eddf9f58cc57e35befbeb5514da87a416befe820150cae63f/Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419", size = 2932834 }, { url = "https://files.pythonhosted.org/packages/a4/d5/e5f85e04f75144d1a89421ba432def6bdffc8f28b04f5b7d540bbd03362c/Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2", size = 2845213 }, { url = "https://files.pythonhosted.org/packages/99/bf/25ef07add7afbb1aacd4460726a1a40370dfd60c0810b6f242a6d3871d7e/Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f", size = 3031573 }, { url = "https://files.pythonhosted.org/packages/55/22/948a97bda5c9dc9968d56b9ed722d9727778db43739cf12ef26ff69be94d/Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb", size = 2926885 }, { url = "https://files.pythonhosted.org/packages/31/ba/e53d107399b535ef89deb6977dd8eae468e2dde7b1b74c6cbe2c0e31fda2/Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64", size = 333171 }, { url = "https://files.pythonhosted.org/packages/99/b3/f7b3af539f74b82e1c64d28685a5200c631cc14ae751d37d6ed819655627/Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", size = 357258 }, ] [[package]] name = "cachecontrol" version = "0.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "msgpack" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d2/23/db12e0b6b241e33f77f7cce01a06b4cc6f8071728656cc0ea262d2a14dad/cachecontrol-0.14.1.tar.gz", hash = "sha256:06ef916a1e4eb7dba9948cdfc9c76e749db2e02104a9a1277e8b642591a0f717", size = 28928 } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/aa/481eb52af52aae093c61c181f2308779973ffd6f0f5f6c0881b2138f3087/cachecontrol-0.14.1-py3-none-any.whl", hash = "sha256:65e3abd62b06382ce3894df60dde9e0deb92aeb734724f68fa4f3b91e97206b9", size = 22085 }, ] [package.optional-dependencies] filecache = [ { name = "filelock" }, ] [[package]] name = "certifi" version = "2024.8.30" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } wheels = [ { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, ] [[package]] name = "cffi" version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } wheels = [ { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, { url = "https://files.pythonhosted.org/packages/48/08/15bf6b43ae9bd06f6b00ad8a91f5a8fe1069d4c9fab550a866755402724e/cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", size = 182457 }, { url = "https://files.pythonhosted.org/packages/c2/5b/f1523dd545f92f7df468e5f653ffa4df30ac222f3c884e51e139878f1cb5/cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", size = 425932 }, { url = "https://files.pythonhosted.org/packages/53/93/7e547ab4105969cc8c93b38a667b82a835dd2cc78f3a7dad6130cfd41e1d/cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", size = 448585 }, { url = "https://files.pythonhosted.org/packages/56/c4/a308f2c332006206bb511de219efeff090e9d63529ba0a77aae72e82248b/cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", size = 456268 }, { url = "https://files.pythonhosted.org/packages/ca/5b/b63681518265f2f4060d2b60755c1c77ec89e5e045fc3773b72735ddaad5/cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", size = 436592 }, { url = "https://files.pythonhosted.org/packages/bb/19/b51af9f4a4faa4a8ac5a0e5d5c2522dcd9703d07fac69da34a36c4d960d3/cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", size = 446512 }, { url = "https://files.pythonhosted.org/packages/e2/63/2bed8323890cb613bbecda807688a31ed11a7fe7afe31f8faaae0206a9a3/cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", size = 171576 }, { url = "https://files.pythonhosted.org/packages/2f/70/80c33b044ebc79527447fd4fbc5455d514c3bb840dede4455de97da39b4d/cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", size = 181229 }, { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, ] [[package]] name = "cfgv" version = "3.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, ] [[package]] name = "charset-normalizer" version = "3.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } wheels = [ { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 }, { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 }, { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 }, { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 }, { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 }, { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 }, { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 }, { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 }, { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 }, { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 }, { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 }, { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 }, { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 }, { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 }, { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 }, { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, { url = "https://files.pythonhosted.org/packages/86/f4/ccab93e631e7293cca82f9f7ba39783c967f823a0000df2d8dd743cad74f/charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", size = 193961 }, { url = "https://files.pythonhosted.org/packages/94/d4/2b21cb277bac9605026d2d91a4a8872bc82199ed11072d035dc674c27223/charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", size = 124507 }, { url = "https://files.pythonhosted.org/packages/9a/e0/a7c1fcdff20d9c667342e0391cfeb33ab01468d7d276b2c7914b371667cc/charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", size = 119298 }, { url = "https://files.pythonhosted.org/packages/70/de/1538bb2f84ac9940f7fa39945a5dd1d22b295a89c98240b262fc4b9fcfe0/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", size = 139328 }, { url = "https://files.pythonhosted.org/packages/e9/ca/288bb1a6bc2b74fb3990bdc515012b47c4bc5925c8304fc915d03f94b027/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", size = 149368 }, { url = "https://files.pythonhosted.org/packages/aa/75/58374fdaaf8406f373e508dab3486a31091f760f99f832d3951ee93313e8/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", size = 141944 }, { url = "https://files.pythonhosted.org/packages/32/c8/0bc558f7260db6ffca991ed7166494a7da4fda5983ee0b0bfc8ed2ac6ff9/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", size = 143326 }, { url = "https://files.pythonhosted.org/packages/0e/dd/7f6fec09a1686446cee713f38cf7d5e0669e0bcc8288c8e2924e998cf87d/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", size = 146171 }, { url = "https://files.pythonhosted.org/packages/4c/a8/440f1926d6d8740c34d3ca388fbd718191ec97d3d457a0677eb3aa718fce/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", size = 139711 }, { url = "https://files.pythonhosted.org/packages/e9/7f/4b71e350a3377ddd70b980bea1e2cc0983faf45ba43032b24b2578c14314/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", size = 148348 }, { url = "https://files.pythonhosted.org/packages/1e/70/17b1b9202531a33ed7ef41885f0d2575ae42a1e330c67fddda5d99ad1208/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", size = 151290 }, { url = "https://files.pythonhosted.org/packages/44/30/574b5b5933d77ecb015550aafe1c7d14a8cd41e7e6c4dcea5ae9e8d496c3/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", size = 149114 }, { url = "https://files.pythonhosted.org/packages/0b/11/ca7786f7e13708687443082af20d8341c02e01024275a28bc75032c5ce5d/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", size = 143856 }, { url = "https://files.pythonhosted.org/packages/f9/c2/1727c1438256c71ed32753b23ec2e6fe7b6dff66a598f6566cfe8139305e/charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", size = 94333 }, { url = "https://files.pythonhosted.org/packages/09/c8/0e17270496a05839f8b500c1166e3261d1226e39b698a735805ec206967b/charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", size = 101454 }, { url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326 }, { url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614 }, { url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450 }, { url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135 }, { url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413 }, { url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992 }, { url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871 }, { url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756 }, { url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034 }, { url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434 }, { url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443 }, { url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294 }, { url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314 }, { url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724 }, { url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159 }, { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, ] [[package]] name = "click" version = "8.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } wheels = [ { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, ] [[package]] name = "codecov-cli" version = "0.1.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "httpx" }, { name = "ijson" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pyyaml" }, { name = "requests" }, { name = "smart-open" }, { name = "tree-sitter" }, ] sdist = { url = "https://files.pythonhosted.org/packages/69/02/eb826787da15c87d970ac817ad4d97c61a80ee434cc2aac0ba25fce45ec4/codecov-cli-0.1.13.tar.gz", hash = "sha256:4f4dd59469f9324803f98bd8573ce78636e8802ffc99e967f0fb63ca30df028b", size = 261659 } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/20/70dfcf53af8471404f571388accd02b9be7521e5354376ebc8a2799c588f/codecov_cli-0.1.13-cp310-cp310-macosx_12_6_x86_64.whl", hash = "sha256:13c87a1e55eab7bf981eac4bacafe5e06af7dc5aebe087de53981716b4da28df", size = 276278 }, { url = "https://files.pythonhosted.org/packages/4b/3a/2f7526e8be590638701c1d3797835e30743914d0e80b63b56acd3c33a7cf/codecov_cli-0.1.13-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:6acd8bb8e34519f19b83c82bc216cafe06b1c086fd4f3f8f83f503a8b1f11aab", size = 276280 }, { url = "https://files.pythonhosted.org/packages/9d/e9/d18725208ac8d2fdb0c158795b1a0f4c5d8a4f9f3a445cb8a07ae2615030/codecov_cli-0.1.13-cp310-cp310-win_amd64.whl", hash = "sha256:43239e136bc4b3ef4450c1d6c1976ab78de6d40ee4f16a23ea5d1eed2995a97b", size = 276271 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] name = "constantly" version = "23.10.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/cb2a94494ff74aa9528a36c5b1422756330a75a8367bf20bd63171fc324d/constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd", size = 13300 } wheels = [ { url = "https://files.pythonhosted.org/packages/b8/40/c199d095151addf69efdb4b9ca3a4f20f70e20508d6222bffb9b76f58573/constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", size = 13547 }, ] [[package]] name = "covdefaults" version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/ee/9a6f2611f72e4c5657ae5542a510cf4164d2c673687c0ea73bb1cbd85b4d/covdefaults-2.3.0.tar.gz", hash = "sha256:4e99f679f12d792bc62e5510fa3eb59546ed47bd569e36e4fddc4081c9c3ebf7", size = 4835 } wheels = [ { url = "https://files.pythonhosted.org/packages/76/4c/823bc951445aa97e5a1b7e337690db3abf85212c8d138e170922e7916ac8/covdefaults-2.3.0-py2.py3-none-any.whl", hash = "sha256:2832961f6ffcfe4b57c338bc3418a3526f495c26fb9c54565409c5532f7c41be", size = 5144 }, ] [[package]] name = "coverage" version = "7.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690 }, { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127 }, { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654 }, { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598 }, { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732 }, { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816 }, { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325 }, { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418 }, { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343 }, { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136 }, { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796 }, { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244 }, { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279 }, { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859 }, { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549 }, { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477 }, { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134 }, { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910 }, { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348 }, { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230 }, { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983 }, { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221 }, { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342 }, { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371 }, { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455 }, { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924 }, { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252 }, { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897 }, { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 }, { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 }, { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 }, { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 }, { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 }, { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 }, { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 }, { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 }, { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 }, { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 }, { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 }, { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 }, { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 }, { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 }, { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 }, { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 }, { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 }, { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 }, { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 }, { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674 }, { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101 }, { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554 }, { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440 }, { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889 }, { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142 }, { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805 }, { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655 }, { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296 }, { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137 }, { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688 }, { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120 }, { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249 }, { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237 }, { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311 }, { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453 }, { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958 }, { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938 }, { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352 }, { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153 }, { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "cryptography" version = "44.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/91/4c/45dfa6829acffa344e3967d6006ee4ae8be57af746ae2eba1c431949b32c/cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", size = 710657 } wheels = [ { url = "https://files.pythonhosted.org/packages/55/09/8cc67f9b84730ad330b3b72cf867150744bf07ff113cda21a15a1c6d2c7c/cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", size = 6541833 }, { url = "https://files.pythonhosted.org/packages/7e/5b/3759e30a103144e29632e7cb72aec28cedc79e514b2ea8896bb17163c19b/cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", size = 3922710 }, { url = "https://files.pythonhosted.org/packages/5f/58/3b14bf39f1a0cfd679e753e8647ada56cddbf5acebffe7db90e184c76168/cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", size = 4137546 }, { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 }, { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 }, { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 }, { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 }, { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 }, { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 }, { url = "https://files.pythonhosted.org/packages/64/b1/50d7739254d2002acae64eed4fc43b24ac0cc44bf0a0d388d1ca06ec5bb1/cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", size = 3202055 }, { url = "https://files.pythonhosted.org/packages/11/18/61e52a3d28fc1514a43b0ac291177acd1b4de00e9301aaf7ef867076ff8a/cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", size = 6542801 }, { url = "https://files.pythonhosted.org/packages/1a/07/5f165b6c65696ef75601b781a280fc3b33f1e0cd6aa5a92d9fb96c410e97/cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", size = 3922613 }, { url = "https://files.pythonhosted.org/packages/28/34/6b3ac1d80fc174812486561cf25194338151780f27e438526f9c64e16869/cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", size = 4137925 }, { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 }, { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 }, { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 }, { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 }, { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 }, { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 }, { url = "https://files.pythonhosted.org/packages/97/9b/443270b9210f13f6ef240eff73fd32e02d381e7103969dc66ce8e89ee901/cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", size = 3202071 }, { url = "https://files.pythonhosted.org/packages/77/d4/fea74422326388bbac0c37b7489a0fcb1681a698c3b875959430ba550daa/cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", size = 3338857 }, { url = "https://files.pythonhosted.org/packages/1a/aa/ba8a7467c206cb7b62f09b4168da541b5109838627f582843bbbe0235e8e/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4", size = 3850615 }, { url = "https://files.pythonhosted.org/packages/89/fa/b160e10a64cc395d090105be14f399b94e617c879efd401188ce0fea39ee/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", size = 4081622 }, { url = "https://files.pythonhosted.org/packages/47/8f/20ff0656bb0cf7af26ec1d01f780c5cfbaa7666736063378c5f48558b515/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", size = 3867546 }, { url = "https://files.pythonhosted.org/packages/38/d9/28edf32ee2fcdca587146bcde90102a7319b2f2c690edfa627e46d586050/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", size = 4090937 }, { url = "https://files.pythonhosted.org/packages/cc/9d/37e5da7519de7b0b070a3fedd4230fe76d50d2a21403e0f2153d70ac4163/cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", size = 3128774 }, ] [[package]] name = "cssutils" version = "2.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/9f/329d26121fe165be44b1dfff21aa0dc348f04633931f1d20ed6cf448a236/cssutils-2.11.1.tar.gz", hash = "sha256:0563a76513b6af6eebbe788c3bf3d01c920e46b3f90c8416738c5cfc773ff8e2", size = 711657 } wheels = [ { url = "https://files.pythonhosted.org/packages/a7/ec/bb273b7208c606890dc36540fe667d06ce840a6f62f9fae7e658fcdc90fb/cssutils-2.11.1-py3-none-any.whl", hash = "sha256:a67bfdfdff4f3867fab43698ec4897c1a828eca5973f4073321b3bccaf1199b1", size = 385747 }, ] [[package]] name = "daphne" version = "4.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "autobahn" }, { name = "twisted", extra = ["tls"] }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/c1/aedf180beb12395835cba791ce7239b8880009d9d37564d72b7590cde605/daphne-4.1.2.tar.gz", hash = "sha256:fcbcace38eb86624ae247c7ffdc8ac12f155d7d19eafac4247381896d6f33761", size = 37882 } wheels = [ { url = "https://files.pythonhosted.org/packages/ab/d6/466f9219281472ecc269ab1d351c5b22a3cfca2d52f72881917949e414df/daphne-4.1.2-py3-none-any.whl", hash = "sha256:618d1322bb4d875342b99dd2a10da2d9aae7ee3645f765965fdc1e658ea5290a", size = 30940 }, ] [[package]] name = "deprecated" version = "1.2.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/a3/53e7d78a6850ffdd394d7048a31a6f14e44900adedf190f9a165f6b69439/deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d", size = 2977612 } wheels = [ { url = "https://files.pythonhosted.org/packages/1d/8f/c7f227eb42cfeaddce3eb0c96c60cbca37797fa7b34f8e1aeadf6c5c0983/Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320", size = 9941 }, ] [[package]] name = "dict2css" version = "0.3.0.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cssutils" }, { name = "domdf-python-tools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/24/eb/776eef1f1aa0188c0fc165c3a60b71027539f71f2eedc43ad21b060e9c39/dict2css-0.3.0.post1.tar.gz", hash = "sha256:89c544c21c4ca7472c3fffb9d37d3d926f606329afdb751dc1de67a411b70719", size = 7845 } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/47/290daabcf91628f4fc0e17c75a1690b354ba067066cd14407712600e609f/dict2css-0.3.0.post1-py3-none-any.whl", hash = "sha256:f006a6b774c3e31869015122ae82c491fd25e7de4a75607a62aa3e798f837e0d", size = 25647 }, ] [[package]] name = "distlib" version = "0.3.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } wheels = [ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, ] [[package]] name = "dnspython" version = "2.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/37/7d/c871f55054e403fdfd6b8f65fd6d1c4e147ed100d3e9f9ba1fe695403939/dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc", size = 332727 } wheels = [ { url = "https://files.pythonhosted.org/packages/87/a1/8c5287991ddb8d3e4662f71356d9656d91ab3a36618c3dd11b280df0d255/dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50", size = 307696 }, ] [[package]] name = "docstring-parser" version = "0.16" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565 } wheels = [ { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533 }, ] [[package]] name = "docutils" version = "0.20.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1f/53/a5da4f2c5739cf66290fac1431ee52aff6851c7c8ffd8264f13affd7bcdd/docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b", size = 2058365 } wheels = [ { url = "https://files.pythonhosted.org/packages/26/87/f238c0670b94533ac0353a4e2a1a771a0cc73277b88bff23d3ae35a256c1/docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", size = 572666 }, ] [[package]] name = "domdf-python-tools" version = "3.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.9'" }, { name = "importlib-resources", marker = "python_full_version < '3.9'" }, { name = "natsort" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6b/78/974e10c583ba9d2302e748c9585313a7f2c7ba00e4f600324f432e38fe68/domdf_python_tools-3.9.0.tar.gz", hash = "sha256:1f8a96971178333a55e083e35610d7688cd7620ad2b99790164e1fc1a3614c18", size = 103792 } wheels = [ { url = "https://files.pythonhosted.org/packages/de/e9/7447a88b217650a74927d3444a89507986479a69b83741900eddd34167fe/domdf_python_tools-3.9.0-py3-none-any.whl", hash = "sha256:4e1ef365cbc24627d6d1e90cf7d46d8ab8df967e1237f4a26885f6986c78872e", size = 127106 }, ] [[package]] name = "editorconfig" version = "0.12.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3d/85/7b5c2fac7fdc37d959fab714b13b9acb75884490dcc0e8b1dc5e64105084/EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80", size = 13278 } [[package]] name = "email-validator" version = "2.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967 } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 }, ] [[package]] name = "eval-type-backport" version = "0.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/23/ca/1601a9fa588867fe2ab6c19ed4c936929160d08a86597adf61bbd443fe57/eval_type_backport-0.2.0.tar.gz", hash = "sha256:68796cfbc7371ebf923f03bdf7bef415f3ec098aeced24e054b253a0e78f7b37", size = 8977 } wheels = [ { url = "https://files.pythonhosted.org/packages/ac/ac/aa3d8e0acbcd71140420bc752d7c9779cf3a2a3bb1d7ef30944e38b2cd39/eval_type_backport-0.2.0-py3-none-any.whl", hash = "sha256:ac2f73d30d40c5a30a80b8739a789d6bb5e49fdffa66d7912667e2015d9c9933", size = 5855 }, ] [[package]] name = "exceptiongroup" version = "1.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } wheels = [ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, ] [[package]] name = "execnet" version = "2.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524 } wheels = [ { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 }, ] [[package]] name = "faker" version = "33.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1e/9f/012fd6049fc86029951cba5112d32c7ba076c4290d7e8873b0413655b808/faker-33.1.0.tar.gz", hash = "sha256:1c925fc0e86a51fc46648b504078c88d0cd48da1da2595c4e712841cab43a1e4", size = 1850515 } wheels = [ { url = "https://files.pythonhosted.org/packages/08/9c/2bba87fbfa42503ddd9653e3546ffc4ed18b14ecab7a07ee86491b886486/Faker-33.1.0-py3-none-any.whl", hash = "sha256:d30c5f0e2796b8970de68978365247657486eb0311c5abe88d0b895b68dff05d", size = 1889127 }, ] [[package]] name = "fast-query-parsers" version = "1.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/dd/20/3a00b889a196e8dc5bede2f168d4a14edc8b5bccc3978a9f497f0f863e79/fast_query_parsers-1.0.3.tar.gz", hash = "sha256:5200a9e02997ad51d4d76a60ea1b256a68a184b04359540eb6310a15013df68f", size = 25275 } wheels = [ { url = "https://files.pythonhosted.org/packages/43/18/4179ac7064b4216ca42f2ed6f74e71254454acf2ec25ce6bb3ffbfda4aa6/fast_query_parsers-1.0.3-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:afbf71c1b4398dacfb9d84755eb026f8e759f68a066f1f3cc19e471fc342e74f", size = 766210 }, { url = "https://files.pythonhosted.org/packages/c5/21/c8c160f61a740efc4577079eb5747a6b2cb8d1168a84a0bfda6044113768/fast_query_parsers-1.0.3-cp38-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:42f26875311d1b151c3406adfa39ec2db98df111a369d75f6fa243ec8462f147", size = 1466147 }, { url = "https://files.pythonhosted.org/packages/51/5b/b10719598dbd14201271efd0b950c6a09efa0a3f6246fec3c192c6b7a8d2/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66630ad423b5b1f5709f82a4d8482cd6aa2f3fa73d2c779ff1877f25dee08d55", size = 764016 }, { url = "https://files.pythonhosted.org/packages/75/06/8861197982909bec00b180527df1e0e9791715271bfb84c8be389b6bf077/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6e3d816c572a6fad1ae9b93713b2db0d3db6e8f594e035ad52361d668dd94a8", size = 729912 }, { url = "https://files.pythonhosted.org/packages/f0/35/7a9a0c50588033edd9efba48f21e251dfcf77eaec2aff470988f622fbd3a/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0bdcc0ddb4cc69d823c2c0dedd8f5affc71042db39908ad2ca06261bf388cac6", size = 1003340 }, { url = "https://files.pythonhosted.org/packages/41/9b/5a42ddd23b85357be6764e14daa607d9b16bc6a395aae2c1cc2077e0a11d/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6720505f2d2a764c76bcc4f3730a9dff69d9871740e46264f6605d73f9ce3794", size = 969496 }, { url = "https://files.pythonhosted.org/packages/c3/9f/4dfa29d74276fa07c40689bfaa3b21d057249314aeb20150f0f41373d16d/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e947e7251769593da93832a10861f59565a46149fa117ebdf25377e7b2853936", size = 939972 }, { url = "https://files.pythonhosted.org/packages/74/34/950b6d799839c11e93566aef426b67f0a446c4906e45e592026fde894459/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55a30b7cee0a53cddf9016b86fdad87221980d5a02a6126c491bd309755e6de9", size = 828557 }, { url = "https://files.pythonhosted.org/packages/81/a8/ee95263abc9806c81d77be8a3420d1f4dde467a10030dde8b0fa0e63f700/fast_query_parsers-1.0.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc2b457caa38371df1a30cfdfc57bd9bfdf348367abdaf6f36533416a0b0e93", size = 863119 }, { url = "https://files.pythonhosted.org/packages/05/d4/5eb8c9d400230b9a45a0ce47a443e9fe37b0902729f9440adef677af1f0d/fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5736d3c32d6ba23995fa569fe572feabcfcfc30ac9e4709e94cff6f2c456a3d1", size = 911046 }, { url = "https://files.pythonhosted.org/packages/f8/b8/bf5e44588f6ebd81d0c53ba49c79999dc54cb0fe81ad6dde6fed2cd45b56/fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3a6377eb0c5b172fbc77c3f96deaf1e51708b4b96d27ce173658bf11c1c00b20", size = 962966 }, { url = "https://files.pythonhosted.org/packages/6f/a9/132572b9f40c2635fdedb7a1cb6cedd9c880f8ffbbfdd6215ee493bb6936/fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:7ca6be04f443a1b055e910ccad01b1d72212f269a530415df99a87c5f1e9c927", size = 965422 }, { url = "https://files.pythonhosted.org/packages/ea/58/942327d3f2694b8f1a2fffaaaef1cc3147571852473a80070ebd6156a62e/fast_query_parsers-1.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a70d4d8852606f2dd5b798ab628b9d8dc6970ddfdd9e96f4543eb0cc89a74fb5", size = 967734 }, { url = "https://files.pythonhosted.org/packages/0a/e3/21bc18edc003b54a2069eb854b9f92cacb5acc99e03c609487a23a673755/fast_query_parsers-1.0.3-cp38-abi3-win32.whl", hash = "sha256:14b3fab7e9a6ac1c1efaf66c3fd2a3fd1e25ede03ed14118035e530433830a11", size = 646366 }, { url = "https://files.pythonhosted.org/packages/ae/4b/07fe4d7b5c458bdde9b0bfd8e8cb5762341af6c9727b43c2331c0cb0dbc3/fast_query_parsers-1.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:21ae5f3a209aee7d3b84bdcdb33dd79f39fc8cb608b3ae8cfcb78123758c1a16", size = 689717 }, ] [[package]] name = "filelock" version = "3.16.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } wheels = [ { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, ] [[package]] name = "fsspec" version = "2024.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a0/52/f16a068ebadae42526484c31f4398e62962504e5724a8ba5dc3409483df2/fsspec-2024.10.0.tar.gz", hash = "sha256:eda2d8a4116d4f2429db8550f2457da57279247dd930bb12f821b58391359493", size = 286853 } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/b2/454d6e7f0158951d8a78c2e1eb4f69ae81beb8dca5fee9809c6c99e9d0d0/fsspec-2024.10.0-py3-none-any.whl", hash = "sha256:03b9a6785766a4de40368b88906366755e2819e758b83705c88cd7cb5fe81871", size = 179641 }, ] [[package]] name = "greenlet" version = "3.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } wheels = [ { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235 }, { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168 }, { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826 }, { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443 }, { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295 }, { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544 }, { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456 }, { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111 }, { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392 }, { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 }, { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 }, { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 }, { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 }, { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 }, { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 }, { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 }, { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 }, { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 }, { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 }, { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 }, { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 }, { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 }, { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 }, { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 }, { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 }, { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 }, { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 }, { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, { url = "https://files.pythonhosted.org/packages/97/83/bdf5f69fcf304065ec7cf8fc7c08248479cfed9bcca02bf0001c07e000aa/greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", size = 271017 }, { url = "https://files.pythonhosted.org/packages/31/4a/2d4443adcb38e1e90e50c653a26b2be39998ea78ca1a4cf414dfdeb2e98b/greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", size = 642888 }, { url = "https://files.pythonhosted.org/packages/5a/c9/b5d9ac1b932aa772dd1eb90a8a2b30dbd7ad5569dcb7fdac543810d206b4/greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", size = 655451 }, { url = "https://files.pythonhosted.org/packages/a8/18/218e21caf7caba5b2236370196eaebc00987d4a2b2d3bf63cc4d4dd5a69f/greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", size = 651409 }, { url = "https://files.pythonhosted.org/packages/a7/25/de419a2b22fa6e18ce3b2a5adb01d33ec7b2784530f76fa36ba43d8f0fac/greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", size = 650661 }, { url = "https://files.pythonhosted.org/packages/d8/88/0ce16c0afb2d71d85562a7bcd9b092fec80a7767ab5b5f7e1bbbca8200f8/greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", size = 605959 }, { url = "https://files.pythonhosted.org/packages/5a/10/39a417ad0afb0b7e5b150f1582cdeb9416f41f2e1df76018434dfac4a6cc/greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", size = 1132341 }, { url = "https://files.pythonhosted.org/packages/9f/f5/e9b151ddd2ed0508b7a47bef7857e46218dbc3fd10e564617a3865abfaac/greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7", size = 1159409 }, { url = "https://files.pythonhosted.org/packages/86/97/2c86989ca4e0f089fbcdc9229c972a01ef53abdafd5ae89e0f3dcdcd4adb/greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef", size = 281126 }, { url = "https://files.pythonhosted.org/packages/d3/50/7b7a3e10ed82c760c1fd8d3167a7c95508e9fdfc0b0604f05ed1a9a9efdc/greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d", size = 298285 }, { url = "https://files.pythonhosted.org/packages/8c/82/8051e82af6d6b5150aacb6789a657a8afd48f0a44d8e91cb72aaaf28553a/greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", size = 270027 }, { url = "https://files.pythonhosted.org/packages/f9/74/f66de2785880293780eebd18a2958aeea7cbe7814af1ccef634f4701f846/greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", size = 634822 }, { url = "https://files.pythonhosted.org/packages/68/23/acd9ca6bc412b02b8aa755e47b16aafbe642dde0ad2f929f836e57a7949c/greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f", size = 646866 }, { url = "https://files.pythonhosted.org/packages/a9/ab/562beaf8a53dc9f6b2459f200e7bc226bb07e51862a66351d8b7817e3efd/greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", size = 641985 }, { url = "https://files.pythonhosted.org/packages/03/d3/1006543621f16689f6dc75f6bcf06e3c23e044c26fe391c16c253623313e/greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", size = 641268 }, { url = "https://files.pythonhosted.org/packages/2f/c1/ad71ce1b5f61f900593377b3f77b39408bce5dc96754790311b49869e146/greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", size = 597376 }, { url = "https://files.pythonhosted.org/packages/f7/ff/183226685b478544d61d74804445589e069d00deb8ddef042699733950c7/greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", size = 1123359 }, { url = "https://files.pythonhosted.org/packages/c0/8b/9b3b85a89c22f55f315908b94cd75ab5fed5973f7393bbef000ca8b2c5c1/greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", size = 1147458 }, { url = "https://files.pythonhosted.org/packages/b8/1c/248fadcecd1790b0ba793ff81fa2375c9ad6442f4c748bf2cc2e6563346a/greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", size = 281131 }, { url = "https://files.pythonhosted.org/packages/ae/02/e7d0aef2354a38709b764df50b2b83608f0621493e47f47694eb80922822/greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", size = 298306 }, ] [[package]] name = "h11" version = "0.14.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } wheels = [ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, ] [[package]] name = "h2" version = "4.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "hpack" }, { name = "hyperframe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2a/32/fec683ddd10629ea4ea46d206752a95a2d8a48c22521edd70b142488efe1/h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb", size = 2145593 } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e5/db6d438da759efbb488c4f3fbdab7764492ff3c3f953132efa6b9f0e9e53/h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d", size = 57488 }, ] [[package]] name = "hiredis" version = "3.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8b/80/740fb0dfa7a42416ce8376490f41dcdb1e5deed9c3739dfe4200fad865a9/hiredis-3.0.0.tar.gz", hash = "sha256:fed8581ae26345dea1f1e0d1a96e05041a727a45e7d8d459164583e23c6ac441", size = 87581 } wheels = [ { url = "https://files.pythonhosted.org/packages/1f/cc/41521d38c77f404c31e08a0118f369f37dc6a9e19cf315dbbc8b0b8afaba/hiredis-3.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:4b182791c41c5eb1d9ed736f0ff81694b06937ca14b0d4dadde5dadba7ff6dae", size = 81483 }, { url = "https://files.pythonhosted.org/packages/99/35/0138fe68b0da01ea91ad67910577905b7f4a34b5c11e2f665d44067c52df/hiredis-3.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:13c275b483a052dd645eb2cb60d6380f1f5215e4c22d6207e17b86be6dd87ffa", size = 44763 }, { url = "https://files.pythonhosted.org/packages/45/53/64fa74d43c17a406c2dc3cb4f1a3729ac00c5451f31f5940ca577b24afa9/hiredis-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1018cc7f12824506f165027eabb302735b49e63af73eb4d5450c66c88f47026", size = 42452 }, { url = "https://files.pythonhosted.org/packages/af/b8/40c58b7db70e3850adeac85d5fca67e2fce6bf15c2705ca6af9c8bb32b5d/hiredis-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83a29cc7b21b746cb6a480189e49f49b2072812c445e66a9e38d2004d496b81c", size = 165712 }, { url = "https://files.pythonhosted.org/packages/ff/8e/7afd36941d58cb0a7f0142ba3a043a5b3743dfff60596e98b355fb048113/hiredis-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e241fab6332e8fb5f14af00a4a9c6aefa22f19a336c069b7ddbf28ef8341e8d6", size = 176842 }, { url = "https://files.pythonhosted.org/packages/ff/39/482970200e65cdcea037a595083e145fc089b8368312f6f2b0d3c5a7c266/hiredis-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fb8de899f0145d6c4d5d4bd0ee88a78eb980a7ffabd51e9889251b8f58f1785", size = 166127 }, { url = "https://files.pythonhosted.org/packages/3a/2b/655e8b4b54ff28c88e2ac536d4aa24c9119c6160169c043351a91db69bca/hiredis-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b23291951959141173eec10f8573538e9349fa27f47a0c34323d1970bf891ee5", size = 165983 }, { url = "https://files.pythonhosted.org/packages/81/d8/bc917412f95da9904a83a04263aa2760051c118d0199eac7250623bfcf17/hiredis-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e421ac9e4b5efc11705a0d5149e641d4defdc07077f748667f359e60dc904420", size = 162249 }, { url = "https://files.pythonhosted.org/packages/77/93/d6585264bb50f9f79537429fa90f4a2a5c29fd5e70d57dec7705ff161a7c/hiredis-3.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77c8006c12154c37691b24ff293c077300c22944018c3ff70094a33e10c1d795", size = 160013 }, { url = "https://files.pythonhosted.org/packages/48/a5/302868a60e963c1b768bd5622f125f5b38a3ea084bdcb374c9251dcc7c02/hiredis-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:41afc0d3c18b59eb50970479a9c0e5544fb4b95e3a79cf2fbaece6ddefb926fe", size = 159315 }, { url = "https://files.pythonhosted.org/packages/82/77/c02d516ab8f31d85378916055dbf980ef7ca431d93ba1f7ac11ac4304863/hiredis-3.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:04ccae6dcd9647eae6025425ab64edb4d79fde8b9e6e115ebfabc6830170e3b2", size = 171008 }, { url = "https://files.pythonhosted.org/packages/e1/28/c080805a340b418b1d022fa58465e365636c0ed201837e0fe70cc7beb0d3/hiredis-3.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fe91d62b0594db5ea7d23fc2192182b1a7b6973f628a9b8b2e0a42a2be721ac6", size = 163290 }, { url = "https://files.pythonhosted.org/packages/6a/f9/caacca69987de597487360565e34dfd191ab23ce147144c13df1f2db6c8d/hiredis-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:99516d99316062824a24d145d694f5b0d030c80da693ea6f8c4ecf71a251d8bb", size = 161037 }, { url = "https://files.pythonhosted.org/packages/88/3a/0d560473ca21facc1de5ba538f655aeae71303afd71f2a5e35fadee0c698/hiredis-3.0.0-cp310-cp310-win32.whl", hash = "sha256:562eaf820de045eb487afaa37e6293fe7eceb5b25e158b5a1974b7e40bf04543", size = 20034 }, { url = "https://files.pythonhosted.org/packages/9c/af/23c2ce80faffb0ceb1775fe4581829c229400d6faacc0e2567ae179e8bc2/hiredis-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1c81c89ed765198da27412aa21478f30d54ef69bf5e4480089d9c3f77b8f882", size = 21863 }, { url = "https://files.pythonhosted.org/packages/42/3e/502e2ce2487673214fbb4cc733b1a279bc71309a689803d9ba8ad6f2fa8f/hiredis-3.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:4664dedcd5933364756d7251a7ea86d60246ccf73a2e00912872dacbfcef8978", size = 81442 }, { url = "https://files.pythonhosted.org/packages/18/0b/171d85b2ee0ac51f94e993a323beffdb6b273b838a4f86d9abaaca22e2f7/hiredis-3.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:47de0bbccf4c8a9f99d82d225f7672b9dd690d8fd872007b933ef51a302c9fa6", size = 44742 }, { url = "https://files.pythonhosted.org/packages/6a/67/466e0b16caff07bc8df8f3ff8b0b279f81066e0fb6a201b0ec66288fe5a4/hiredis-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e43679eca508ba8240d016d8cca9d27342d70184773c15bea78a23c87a1922f1", size = 42424 }, { url = "https://files.pythonhosted.org/packages/01/50/e1f21e1cc9426bdf62e9ca8106294fbc3e5d27ddbae2e85e47fb9f251d1b/hiredis-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13c345e7278c210317e77e1934b27b61394fee0dec2e8bd47e71570900f75823", size = 166331 }, { url = "https://files.pythonhosted.org/packages/98/40/8d8e4e15045ce066570f82f49604c6273b186eda1e5c9b93b450dd25d7b9/hiredis-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00018f22f38530768b73ea86c11f47e8d4df65facd4e562bd78773bd1baef35e", size = 177350 }, { url = "https://files.pythonhosted.org/packages/5d/9c/f7b6d7afa2bd9c6671de853069222d9d874725e387100dfb0f1a22aab122/hiredis-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ea3a86405baa8eb0d3639ced6926ad03e07113de54cb00fd7510cb0db76a89d", size = 166794 }, { url = "https://files.pythonhosted.org/packages/53/0c/1076e0c045412081ec44dc81969373cda15c093a0692e10f2941e154e583/hiredis-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c073848d2b1d5561f3903879ccf4e1a70c9b1e7566c7bdcc98d082fa3e7f0a1d", size = 166566 }, { url = "https://files.pythonhosted.org/packages/05/69/e081b023f86b0128fcf9f76c8ed5a5f9426895ad86de234b0332c18a57b8/hiredis-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a8dffb5f5b3415a4669d25de48b617fd9d44b0bccfc4c2ab24b06406ecc9ecb", size = 162561 }, { url = "https://files.pythonhosted.org/packages/96/e0/7f957fb2158c6f6800b6faa2f90bedcc485ca038a2d42166761d400683a3/hiredis-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22c17c96143c2a62dfd61b13803bc5de2ac526b8768d2141c018b965d0333b66", size = 160472 }, { url = "https://files.pythonhosted.org/packages/5c/31/d68020aa6276bd1a7436ece96d540ad17c204d97285639e0757ef1c3d430/hiredis-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3ece960008dab66c6b8bb3a1350764677ee7c74ccd6270aaf1b1caf9ccebb46", size = 159705 }, { url = "https://files.pythonhosted.org/packages/f7/68/5d101f8ffd764a96c2b959815adebb1e4b7e06db68122f9d3dbbc19b81eb/hiredis-3.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f75999ae00a920f7dce6ecae76fa5e8674a3110e5a75f12c7a2c75ae1af53396", size = 171498 }, { url = "https://files.pythonhosted.org/packages/83/86/66131743a2012f668f84aa2eddc07e7b2462b4a07a753b27125f14e4b8bc/hiredis-3.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e069967cbd5e1900aafc4b5943888f6d34937fc59bf8918a1a546cb729b4b1e4", size = 163951 }, { url = "https://files.pythonhosted.org/packages/a5/ea/58976d9c21086975a90c7fa2337591ea3903eeb55083e366b5ea36b99ca5/hiredis-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0aacc0a78e1d94d843a6d191f224a35893e6bdfeb77a4a89264155015c65f126", size = 161566 }, { url = "https://files.pythonhosted.org/packages/39/69/cdb255e3d37f82f31f4b7b2db5bbd8500eae8d22c0d7992fe474fd02babd/hiredis-3.0.0-cp311-cp311-win32.whl", hash = "sha256:719c32147ba29528cb451f037bf837dcdda4ff3ddb6cdb12c4216b0973174718", size = 20037 }, { url = "https://files.pythonhosted.org/packages/9d/cf/40d209e0458ac28a26973d1449df2922c7b8259f7f88d7738d11c87f9ff6/hiredis-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:bdc144d56333c52c853c31b4e2e52cfbdb22d3da4374c00f5f3d67c42158970f", size = 21862 }, { url = "https://files.pythonhosted.org/packages/ae/09/0a3eace00115d8c82a8e7d8e58e60aacec10334f4f1512f09ffbac3252e3/hiredis-3.0.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:484025d2eb8f6348f7876fc5a2ee742f568915039fcb31b478fd5c242bb0fe3a", size = 81540 }, { url = "https://files.pythonhosted.org/packages/1c/e8/1a7a5ded4fb11e91aafc5ba5518392f22883d54e79c4b47f188fb712ea46/hiredis-3.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fcdb552ffd97151dab8e7bc3ab556dfa1512556b48a367db94b5c20253a35ee1", size = 44814 }, { url = "https://files.pythonhosted.org/packages/3b/f5/4e055dc9b55484644afb18063f28649cdbd19be4f15bc152bd633dccd6f7/hiredis-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bb6f9fd92f147ba11d338ef5c68af4fd2908739c09e51f186e1d90958c68cc1", size = 42478 }, { url = "https://files.pythonhosted.org/packages/65/7b/e06f55b9dcdf10cb6b3f08d7917d3080096cd83deaef1bd4927720fbb280/hiredis-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa86bf9a0ed339ec9e8a9a9d0ae4dccd8671625c83f9f9f2640729b15e07fbfd", size = 168303 }, { url = "https://files.pythonhosted.org/packages/f4/16/081e90137bb896acd9dc2e1e68480cc84d652af4d959e75e52d6ce9dd602/hiredis-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e194a0d5df9456995d8f510eab9f529213e7326af6b94770abf8f8b7952ddcaa", size = 179151 }, { url = "https://files.pythonhosted.org/packages/1e/0f/f5aba1c82977f4b639e5b450c0d8685333f1200cd1972647eb3f4d972e55/hiredis-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a1df39d74ec507d79c7a82c8063eee60bf80537cdeee652f576059b9cdd15c", size = 168580 }, { url = "https://files.pythonhosted.org/packages/60/86/aa24c20f6d3038bf244bc60a2fe8cde61fb3c0d6a82e2bed30b08d55f96c/hiredis-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f91456507427ba36fd81b2ca11053a8e112c775325acc74e993201ea912d63e9", size = 169147 }, { url = "https://files.pythonhosted.org/packages/6e/03/a4c7a28b6320ef3e36062c1c51e9d66e889c9e09ee7d7ae38b8a2ffdb365/hiredis-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9862db92ef67a8a02e0d5370f07d380e14577ecb281b79720e0d7a89aedb9ee5", size = 164722 }, { url = "https://files.pythonhosted.org/packages/cd/66/d60106b56ba0ddd9789656d204a577591ff0cd91ab94178bb96c84d0d918/hiredis-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d10fcd9e0eeab835f492832b2a6edb5940e2f1230155f33006a8dfd3bd2c94e4", size = 162561 }, { url = "https://files.pythonhosted.org/packages/6a/30/f33f2b782096efe9fe6b24c67a4df13b5055d9c859f615a74fb4f18cce41/hiredis-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:48727d7d405d03977d01885f317328dc21d639096308de126c2c4e9950cbd3c9", size = 161388 }, { url = "https://files.pythonhosted.org/packages/45/02/34d9b151f9ea4655bfe00e0230f7db8fd8a52c7b7bd728efdf1c17655860/hiredis-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e0bb6102ebe2efecf8a3292c6660a0e6fac98176af6de67f020bea1c2343717", size = 173561 }, { url = "https://files.pythonhosted.org/packages/cf/54/68285d208918b6d83e32d872d8dcbf8d479ed2c74b863b836e48a2702a3f/hiredis-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:df274e3abb4df40f4c7274dd3e587dfbb25691826c948bc98d5fead019dfb001", size = 165914 }, { url = "https://files.pythonhosted.org/packages/56/4f/5f36865f9f032caf00d603ff9cbde21506d2b1e0e0ce0b5d2ce2851411c9/hiredis-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:034925b5fb514f7b11aac38cd55b3fd7e9d3af23bd6497f3f20aa5b8ba58e232", size = 163968 }, { url = "https://files.pythonhosted.org/packages/d3/ee/c38693bd1dbce34806ecc3536dc425e87e420030de7018194865511860c2/hiredis-3.0.0-cp312-cp312-win32.whl", hash = "sha256:120f2dda469b28d12ccff7c2230225162e174657b49cf4cd119db525414ae281", size = 20189 }, { url = "https://files.pythonhosted.org/packages/4e/67/f50b45071bb8652fa9a28a84ee470a02042fb7a096a16f3c08842f2a5c2b/hiredis-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e584fe5f4e6681d8762982be055f1534e0170f6308a7a90f58d737bab12ff6a8", size = 21971 }, { url = "https://files.pythonhosted.org/packages/1a/f4/d0c39512eee1a4f3bd6b14bc0ab3f8e13b45a68be58b41916468ecffd2e4/hiredis-3.0.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:122171ff47d96ed8dd4bba6c0e41d8afaba3e8194949f7720431a62aa29d8895", size = 81496 }, { url = "https://files.pythonhosted.org/packages/b7/57/1bf54704603c6edef75a1311b43468f01cd78908e2823c0646dbf08255ed/hiredis-3.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:ba9fc605ac558f0de67463fb588722878641e6fa1dabcda979e8e69ff581d0bd", size = 44770 }, { url = "https://files.pythonhosted.org/packages/e8/a4/7f4826236ff3cafd94aa2bdb31498f16949929adf05d56fe85cbc32abfc7/hiredis-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a631e2990b8be23178f655cae8ac6c7422af478c420dd54e25f2e26c29e766f1", size = 42459 }, { url = "https://files.pythonhosted.org/packages/1a/92/bc29d66789c6cf6e3ba21589f0e918893feb9dc096fda0a6a8ac775db7cb/hiredis-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63482db3fadebadc1d01ad33afa6045ebe2ea528eb77ccaabd33ee7d9c2bad48", size = 166802 }, { url = "https://files.pythonhosted.org/packages/eb/de/97204c87a023d0f6bdd25c65bb1b2c1ce69b96aeac16a810df068cd28cfb/hiredis-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f669212c390eebfbe03c4e20181f5970b82c5d0a0ad1df1785f7ffbe7d61150", size = 177667 }, { url = "https://files.pythonhosted.org/packages/bb/fe/a421f3cf94099c5d0493dac1761506c9e4a6d7445021e5bd5b384a97109a/hiredis-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a49ef161739f8018c69b371528bdb47d7342edfdee9ddc75a4d8caddf45a6e", size = 167172 }, { url = "https://files.pythonhosted.org/packages/98/7f/834353b508fd183d5440a812773d8695b2c6878fd4dbd87199d18a1b44a3/hiredis-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98a152052b8878e5e43a2e3a14075218adafc759547c98668a21e9485882696c", size = 166944 }, { url = "https://files.pythonhosted.org/packages/ef/6f/afda01cad5d8f212b58445c4a21e1c87c634d6617e98e928e63ce8b340dd/hiredis-3.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50a196af0ce657fcde9bf8a0bbe1032e22c64d8fcec2bc926a35e7ff68b3a166", size = 162703 }, { url = "https://files.pythonhosted.org/packages/86/b9/6dd603b027f5b1ce370b4179412ca8e1d2b1e5f61a9cb359981056215139/hiredis-3.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f2f312eef8aafc2255e3585dcf94d5da116c43ef837db91db9ecdc1bc930072d", size = 160278 }, { url = "https://files.pythonhosted.org/packages/4c/9c/4444140eccbaddd77217657040d80056ee822917d67806884ac7bf776a16/hiredis-3.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:6ca41fa40fa019cde42c21add74aadd775e71458051a15a352eabeb12eb4d084", size = 159515 }, { url = "https://files.pythonhosted.org/packages/da/b8/49d4685ba10e5d808b0736b5a478c50011590c23a8998f83219aa812d918/hiredis-3.0.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6eecb343c70629f5af55a8b3e53264e44fa04e155ef7989de13668a0cb102a90", size = 171232 }, { url = "https://files.pythonhosted.org/packages/1b/98/6864287631dd1e2acce42bae26c25ac58f9ff1874e460d825def4f550ebe/hiredis-3.0.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:c3fdad75e7837a475900a1d3a5cc09aa024293c3b0605155da2d42f41bc0e482", size = 163478 }, { url = "https://files.pythonhosted.org/packages/33/31/7d75a335f4d744439c3c694c5aeb5e8257d846013aee5580f59633c2871b/hiredis-3.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8854969e7480e8d61ed7549eb232d95082a743e94138d98d7222ba4e9f7ecacd", size = 161171 }, { url = "https://files.pythonhosted.org/packages/57/92/1a870e1fcab1e70221e4d47f0b26749760c4c9daebf825603544b8a56373/hiredis-3.0.0-cp38-cp38-win32.whl", hash = "sha256:f114a6c86edbf17554672b050cce72abf489fe58d583c7921904d5f1c9691605", size = 20022 }, { url = "https://files.pythonhosted.org/packages/0d/6b/5d1853b9f6db1cf40c765930279a02e5a2536b1073a65395e752120981cc/hiredis-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:7d99b91e42217d7b4b63354b15b41ce960e27d216783e04c4a350224d55842a4", size = 21888 }, { url = "https://files.pythonhosted.org/packages/d7/3b/d4719b058647b59d23962dc4de9fc4b4730101d5c3539606aafc827f965b/hiredis-3.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:4c6efcbb5687cf8d2aedcc2c3ed4ac6feae90b8547427d417111194873b66b06", size = 81465 }, { url = "https://files.pythonhosted.org/packages/61/fe/472c2cfdcca138584dd49fa53e0a5cba03d90739d37582fb2303ca41066c/hiredis-3.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5b5cff42a522a0d81c2ae7eae5e56d0ee7365e0c4ad50c4de467d8957aff4414", size = 44756 }, { url = "https://files.pythonhosted.org/packages/5b/fa/179e62c6c909fe23064769e3668cf4456b81091be7ad026d36c43b5851cb/hiredis-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:82f794d564f4bc76b80c50b03267fe5d6589e93f08e66b7a2f674faa2fa76ebc", size = 42439 }, { url = "https://files.pythonhosted.org/packages/f9/c7/34e337f18ce599afc0fb26cc0200dfad4f83cdc9bed2f4c62002d8a5d34c/hiredis-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7a4c1791d7aa7e192f60fe028ae409f18ccdd540f8b1e6aeb0df7816c77e4a4", size = 164975 }, { url = "https://files.pythonhosted.org/packages/cf/66/934e046ff490b87b77963cf8dfe50a3b40cea28dbb32254c768b0854f225/hiredis-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2537b2cd98192323fce4244c8edbf11f3cac548a9d633dbbb12b48702f379f4", size = 176144 }, { url = "https://files.pythonhosted.org/packages/a9/5c/43ebeb2e58b655f8acee72a863cb151b46d1ae1a767b65da0f231cf54f97/hiredis-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fed69bbaa307040c62195a269f82fc3edf46b510a17abb6b30a15d7dab548df", size = 165549 }, { url = "https://files.pythonhosted.org/packages/de/23/e4e372e6a1afa07db9d15b0baa264cc4e9e420e77e676cda8244aae0f4e5/hiredis-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869f6d5537d243080f44253491bb30aa1ec3c21754003b3bddeadedeb65842b0", size = 165333 }, { url = "https://files.pythonhosted.org/packages/ee/01/7fa60640541d697f666b1fc71b5e5cd03999547a824036eb6f4b7fc7107f/hiredis-3.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d435ae89073d7cd51e6b6bf78369c412216261c9c01662e7008ff00978153729", size = 161659 }, { url = "https://files.pythonhosted.org/packages/b6/15/c793034d32be8d135491ccd049e5441ff95f3e07d7e49045d2ef528b9fad/hiredis-3.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:204b79b30a0e6be0dc2301a4d385bb61472809f09c49f400497f1cdd5a165c66", size = 159668 }, { url = "https://files.pythonhosted.org/packages/b2/8a/d9dfb08be4fae5e2226a3f0e9ad2f0d5be81d4b1dd8c8dcd06ac7290c325/hiredis-3.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ea635101b739c12effd189cc19b2671c268abb03013fd1f6321ca29df3ca625", size = 158695 }, { url = "https://files.pythonhosted.org/packages/dd/cd/32ca226e2452b62e3422956ba6fad6566182d91fd3fe74402e5e17923e84/hiredis-3.0.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f359175197fd833c8dd7a8c288f1516be45415bb5c939862ab60c2918e1e1943", size = 170483 }, { url = "https://files.pythonhosted.org/packages/12/0d/21002b893e9f5980b7de9afe35cd21653ae8aab2cb4cda1ba8a2d8380d97/hiredis-3.0.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac6d929cb33dd12ad3424b75725975f0a54b5b12dbff95f2a2d660c510aa106d", size = 162768 }, { url = "https://files.pythonhosted.org/packages/79/b9/ba7c9ae8711d54f34e6c35bcfc546fd44c77deb832b07a649397be6df88d/hiredis-3.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:100431e04d25a522ef2c3b94f294c4219c4de3bfc7d557b6253296145a144c11", size = 160452 }, { url = "https://files.pythonhosted.org/packages/04/17/913e1f784bed3f144f546416d905aff36c2daf0cad875c2bf88187ffff29/hiredis-3.0.0-cp39-cp39-win32.whl", hash = "sha256:e1a9c14ae9573d172dc050a6f63a644457df5d01ec4d35a6a0f097f812930f83", size = 20020 }, { url = "https://files.pythonhosted.org/packages/e5/09/e6590cfdaf8ef465570e16cd49fe1b5fa84d173b57d24fcc9efbaf166542/hiredis-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:54a6dd7b478e6eb01ce15b3bb5bf771e108c6c148315bf194eb2ab776a3cac4d", size = 21891 }, { url = "https://files.pythonhosted.org/packages/6c/26/fee1a29d7d0cbb76e27ac0914bb17565b1d7cfa24d58922010a667190afc/hiredis-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50da7a9edf371441dfcc56288d790985ee9840d982750580710a9789b8f4a290", size = 39805 }, { url = "https://files.pythonhosted.org/packages/c7/da/4e9fadc0615958b58e6632d6e85375062f80b60b268b21fa3f449aeee02e/hiredis-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b285ef6bf1581310b0d5e8f6ce64f790a1c40e89c660e1320b35f7515433672", size = 36883 }, { url = "https://files.pythonhosted.org/packages/cf/d5/cc88b23e466ee070e0109a3e7d7e7835608ad90f80d8415bf7c8c726e71d/hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dcfa684966f25b335072115de2f920228a3c2caf79d4bfa2b30f6e4f674a948", size = 47867 }, { url = "https://files.pythonhosted.org/packages/09/5b/848006ee860cf543a8b964c17ef04a61ea16967c9b5f173557286ae1afd2/hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a41be8af1fd78ca97bc948d789a09b730d1e7587d07ca53af05758f31f4b985d", size = 48254 }, { url = "https://files.pythonhosted.org/packages/91/41/ef57d7f6f324ea5052d707a510093ec61fde8c5f271029116490790168cf/hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:038756db735e417ab36ee6fd7725ce412385ed2bd0767e8179a4755ea11b804f", size = 55556 }, { url = "https://files.pythonhosted.org/packages/81/52/150658b3006241f2de243e2ccb7f94cfeb74a855435e872dbde7d87f6842/hiredis-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fcecbd39bd42cef905c0b51c9689c39d0cc8b88b1671e7f40d4fb213423aef3a", size = 21938 }, { url = "https://files.pythonhosted.org/packages/1a/d7/68088ce94cb4e346e4c0729788c9894238c27e8718283a21a4b76c6235bd/hiredis-3.0.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a131377493a59fb0f5eaeb2afd49c6540cafcfba5b0b3752bed707be9e7c4eaf", size = 39848 }, { url = "https://files.pythonhosted.org/packages/97/dd/e25dcef9004eaf433575056bf555db12e70f96f3784acc1f38f20d9d8258/hiredis-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d22c53f0ec5c18ecb3d92aa9420563b1c5d657d53f01356114978107b00b860", size = 36899 }, { url = "https://files.pythonhosted.org/packages/b9/0e/1b2e0cab33fbfbdb3a72aeec6e429252a87c6f5c3325fa55da090165a564/hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a91e9520fbc65a799943e5c970ffbcd67905744d8becf2e75f9f0a5e8414f0", size = 47971 }, { url = "https://files.pythonhosted.org/packages/3d/03/04a2ceeb865e4a52a7ddf4e2f247aa0b17e2c94e82dba9af5786b655633d/hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dc8043959b50141df58ab4f398e8ae84c6f9e673a2c9407be65fc789138f4a6", size = 48342 }, { url = "https://files.pythonhosted.org/packages/7d/79/9e8f4da2541486d6c7912e4df374eaf15b7c186e46af54fea521c941c5e8/hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51b99cfac514173d7b8abdfe10338193e8a0eccdfe1870b646009d2fb7cbe4b5", size = 55646 }, { url = "https://files.pythonhosted.org/packages/12/e8/af81ed090f44775917e65d648fbd07aeb8c734f0dcab8500f049e2a04772/hiredis-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fa1fcad89d8a41d8dc10b1e54951ec1e161deabd84ed5a2c95c3c7213bdb3514", size = 21941 }, { url = "https://files.pythonhosted.org/packages/14/98/dd848a92e3306be35cc8e868528b685b996c6dd02aa6f163c87325a5d061/hiredis-3.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:898636a06d9bf575d2c594129085ad6b713414038276a4bfc5db7646b8a5be78", size = 39797 }, { url = "https://files.pythonhosted.org/packages/52/6c/2dc71e2af62d9405ce9bcfbab3bbaba626f90dbb7c56990e657cf829def2/hiredis-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:466f836dbcf86de3f9692097a7a01533dc9926986022c6617dc364a402b265c5", size = 36848 }, { url = "https://files.pythonhosted.org/packages/56/9d/3e11b6167f792eb673c8ab9817fc55046ac4414f5339d81d9152f5c165cd/hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23142a8af92a13fc1e3f2ca1d940df3dcf2af1d176be41fe8d89e30a837a0b60", size = 47837 }, { url = "https://files.pythonhosted.org/packages/52/b4/dce71d1528c03d731cbe55d6f4b5ea52020481c53fa5b0d92dc37d9c26c1/hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:793c80a3d6b0b0e8196a2d5de37a08330125668c8012922685e17aa9108c33ac", size = 48224 }, { url = "https://files.pythonhosted.org/packages/61/0c/e17779b8789b35780054d76e0d1d2de043a02078be0996c9ff515e00be68/hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:467d28112c7faa29b7db743f40803d927c8591e9da02b6ce3d5fadc170a542a2", size = 55536 }, { url = "https://files.pythonhosted.org/packages/da/a6/8e64ab752619273d65d7630fdfc29353e0a48fe4b19599d072533ff7997e/hiredis-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dc384874a719c767b50a30750f937af18842ee5e288afba95a5a3ed703b1515a", size = 21917 }, ] [[package]] name = "hpack" version = "4.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/fda93fb4d957db19b0f6b370e79d586b3e8528b20252c729c476a2c02954/hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095", size = 49117 } wheels = [ { url = "https://files.pythonhosted.org/packages/d5/34/e8b383f35b77c402d28563d2b8f83159319b509bc5f760b15d60b0abf165/hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c", size = 32611 }, ] [[package]] name = "html5lib" version = "1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215 } wheels = [ { url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173 }, ] [[package]] name = "httpcore" version = "1.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } wheels = [ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, ] [[package]] name = "httptools" version = "0.6.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780 }, { url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297 }, { url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130 }, { url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148 }, { url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949 }, { url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591 }, { url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344 }, { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029 }, { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492 }, { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891 }, { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788 }, { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214 }, { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120 }, { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565 }, { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, { url = "https://files.pythonhosted.org/packages/c2/73/e4877dfa233da9912062e49efd74d9f5deae95b4b736eb99742f8d751074/httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba", size = 202417 }, { url = "https://files.pythonhosted.org/packages/04/06/24f105db5254d9689d9126ca09cd55c471241f26549041f33aea91a4c77e/httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc", size = 105139 }, { url = "https://files.pythonhosted.org/packages/32/c6/3623958d7899c439d5aeadcc936c3354baaf2d797e07670ccddbae5c4398/httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff", size = 455956 }, { url = "https://files.pythonhosted.org/packages/a0/cf/3de90444de495cbab24e648278a4fecb36c5bbf9ecdeeff09fca69e94ca9/httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490", size = 453707 }, { url = "https://files.pythonhosted.org/packages/33/92/f0928f8bae0a07d75bddff71835e554762974502165ea5ea78c624e3533e/httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43", size = 434037 }, { url = "https://files.pythonhosted.org/packages/d4/2b/618e8f2cf8b266a046c4524f4c214919762a9da4617e8b02da406e3747bc/httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440", size = 434347 }, { url = "https://files.pythonhosted.org/packages/a3/59/b7dc35b45ae31d692427f15870ff9ab082e667b96c5606fda2cd7b385687/httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f", size = 89888 }, { url = "https://files.pythonhosted.org/packages/51/b1/4fc6f52afdf93b7c4304e21f6add9e981e4f857c2fa622a55dfe21b6059e/httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003", size = 201123 }, { url = "https://files.pythonhosted.org/packages/c2/01/e6ecb40ac8fdfb76607c7d3b74a41b464458d5c8710534d8f163b0c15f29/httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab", size = 104507 }, { url = "https://files.pythonhosted.org/packages/dc/24/c70c34119d209bf08199d938dc9c69164f585ed3029237b4bdb90f673cb9/httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547", size = 449615 }, { url = "https://files.pythonhosted.org/packages/2b/62/e7f317fed3703bd81053840cacba4e40bcf424b870e4197f94bd1cf9fe7a/httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9", size = 448819 }, { url = "https://files.pythonhosted.org/packages/2a/13/68337d3be6b023260139434c49d7aa466aaa98f9aee7ed29270ac7dde6a2/httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076", size = 422093 }, { url = "https://files.pythonhosted.org/packages/fc/b3/3a1bc45be03dda7a60c7858e55b6cd0489a81613c1908fb81cf21d34ae50/httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd", size = 423898 }, { url = "https://files.pythonhosted.org/packages/05/72/2ddc2ae5f7ace986f7e68a326215b2e7c32e32fd40e6428fa8f1d8065c7e/httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6", size = 89552 }, ] [[package]] name = "httpx" version = "0.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "anyio", version = "4.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/10/df/676b7cf674dd1bdc71a64ad393c89879f75e4a0ab8395165b498262ae106/httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0", size = 141307 } wheels = [ { url = "https://files.pythonhosted.org/packages/8f/fb/a19866137577ba60c6d8b69498dc36be479b13ba454f691348ddf428f185/httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc", size = 73551 }, ] [[package]] name = "httpx-sse" version = "0.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, ] [[package]] name = "hypercorn" version = "0.17.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "h11" }, { name = "h2" }, { name = "priority" }, { name = "taskgroup", marker = "python_full_version < '3.11'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "wsproto" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/3a/df6c27642e0dcb7aff688ca4be982f0fb5d89f2afd3096dc75347c16140f/hypercorn-0.17.3.tar.gz", hash = "sha256:1b37802ee3ac52d2d85270700d565787ab16cf19e1462ccfa9f089ca17574165", size = 44409 } wheels = [ { url = "https://files.pythonhosted.org/packages/0e/3b/dfa13a8d96aa24e40ea74a975a9906cfdc2ab2f4e3b498862a57052f04eb/hypercorn-0.17.3-py3-none-any.whl", hash = "sha256:059215dec34537f9d40a69258d323f56344805efb462959e727152b0aa504547", size = 61742 }, ] [[package]] name = "hyperframe" version = "6.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/2a/4747bff0a17f7281abe73e955d60d80aae537a5d203f417fa1c2e7578ebb/hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914", size = 25008 } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/de/85a784bcc4a3779d1753a7ec2dee5de90e18c7bcf402e71b51fcf150b129/hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15", size = 12389 }, ] [[package]] name = "hyperlink" version = "21.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743 } wheels = [ { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638 }, ] [[package]] name = "hypothesis" version = "6.113.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/32/6513cd7256f38c19a6c8a1d5ce9792bcd35c7f11651989994731f0e97672/hypothesis-6.113.0.tar.gz", hash = "sha256:5556ac66fdf72a4ccd5d237810f7cf6bdcd00534a4485015ef881af26e20f7c7", size = 408897 } wheels = [ { url = "https://files.pythonhosted.org/packages/14/fa/4acb477b86a94571958bd337eae5baf334d21b8c98a04b594d0dad381ba8/hypothesis-6.113.0-py3-none-any.whl", hash = "sha256:d539180eb2bb71ed28a23dfe94e67c851f9b09f3ccc4125afad43f17e32e2bad", size = 469790 }, ] [[package]] name = "identify" version = "2.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097 } wheels = [ { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972 }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] name = "ijson" version = "3.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6c/83/28e9e93a3a61913e334e3a2e78ea9924bb9f9b1ac45898977f9d9dd6133f/ijson-3.3.0.tar.gz", hash = "sha256:7f172e6ba1bee0d4c8f8ebd639577bfe429dee0f3f96775a067b8bae4492d8a0", size = 60079 } wheels = [ { url = "https://files.pythonhosted.org/packages/ad/89/96e3608499b4a500b9bc27aa8242704e675849dd65bdfa8682b00a92477e/ijson-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7f7a5250599c366369fbf3bc4e176f5daa28eb6bc7d6130d02462ed335361675", size = 85009 }, { url = "https://files.pythonhosted.org/packages/e4/7e/1098503500f5316c5f7912a51c91aca5cbc609c09ce4ecd9c4809983c560/ijson-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f87a7e52f79059f9c58f6886c262061065eb6f7554a587be7ed3aa63e6b71b34", size = 57796 }, { url = "https://files.pythonhosted.org/packages/78/f7/27b8c27a285628719ff55b68507581c86b551eb162ce810fe51e3e1a25f2/ijson-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b73b493af9e947caed75d329676b1b801d673b17481962823a3e55fe529c8b8b", size = 57218 }, { url = "https://files.pythonhosted.org/packages/0c/c5/1698094cb6a336a223c30e1167cc1b15cdb4bfa75399c1a2eb82fa76cc3c/ijson-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5576415f3d76290b160aa093ff968f8bf6de7d681e16e463a0134106b506f49", size = 117153 }, { url = "https://files.pythonhosted.org/packages/4b/21/c206dda0945bd832cc9b0894596b0efc2cb1819a0ac61d8be1429ac09494/ijson-3.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e9ffe358d5fdd6b878a8a364e96e15ca7ca57b92a48f588378cef315a8b019e", size = 110781 }, { url = "https://files.pythonhosted.org/packages/f4/f5/2d733e64577109a9b255d14d031e44a801fa20df9ccc58b54a31e8ecf9e6/ijson-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8643c255a25824ddd0895c59f2319c019e13e949dc37162f876c41a283361527", size = 114527 }, { url = "https://files.pythonhosted.org/packages/8d/a8/78bfee312aa23417b86189a65f30b0edbceaee96dc6a616cc15f611187d1/ijson-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df3ab5e078cab19f7eaeef1d5f063103e1ebf8c26d059767b26a6a0ad8b250a3", size = 116824 }, { url = "https://files.pythonhosted.org/packages/5d/a4/aff410f7d6aa1a77ee2ab2d6a2d2758422726270cb149c908a9baf33cf58/ijson-3.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3dc1fb02c6ed0bae1b4bf96971258bf88aea72051b6e4cebae97cff7090c0607", size = 112647 }, { url = "https://files.pythonhosted.org/packages/77/ee/2b5122dc4713f5a954267147da36e7156240ca21b04ed5295bc0cabf0fbe/ijson-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e9afd97339fc5a20f0542c971f90f3ca97e73d3050cdc488d540b63fae45329a", size = 114156 }, { url = "https://files.pythonhosted.org/packages/b3/d7/ad3b266490b60c6939e8a07fd8e4b7e2002aea08eaa9572a016c3e3a9129/ijson-3.3.0-cp310-cp310-win32.whl", hash = "sha256:844c0d1c04c40fd1b60f148dc829d3f69b2de789d0ba239c35136efe9a386529", size = 48931 }, { url = "https://files.pythonhosted.org/packages/0b/68/b9e1c743274c8a23dddb12d2ed13b5f021f6d21669d51ff7fa2e9e6c19df/ijson-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:d654d045adafdcc6c100e8e911508a2eedbd2a1b5f93f930ba13ea67d7704ee9", size = 50965 }, { url = "https://files.pythonhosted.org/packages/fd/df/565ba72a6f4b2c833d051af8e2228cfa0b1fef17bb44995c00ad27470c52/ijson-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:501dce8eaa537e728aa35810656aa00460a2547dcb60937c8139f36ec344d7fc", size = 85041 }, { url = "https://files.pythonhosted.org/packages/f0/42/1361eaa57ece921d0239881bae6a5e102333be5b6e0102a05ec3caadbd5a/ijson-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:658ba9cad0374d37b38c9893f4864f284cdcc7d32041f9808fba8c7bcaadf134", size = 57829 }, { url = "https://files.pythonhosted.org/packages/f5/b0/143dbfe12e1d1303ea8d8cd6f40e95cea8f03bcad5b79708614a7856c22e/ijson-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2636cb8c0f1023ef16173f4b9a233bcdb1df11c400c603d5f299fac143ca8d70", size = 57217 }, { url = "https://files.pythonhosted.org/packages/0d/80/b3b60c5e5be2839365b03b915718ca462c544fdc71e7a79b7262837995ef/ijson-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd174b90db68c3bcca273e9391934a25d76929d727dc75224bf244446b28b03b", size = 121878 }, { url = "https://files.pythonhosted.org/packages/8d/eb/7560fafa4d40412efddf690cb65a9bf2d3429d6035e544103acbf5561dc4/ijson-3.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97a9aea46e2a8371c4cf5386d881de833ed782901ac9f67ebcb63bb3b7d115af", size = 115620 }, { url = "https://files.pythonhosted.org/packages/51/2b/5a34c7841388dce161966e5286931518de832067cd83e6f003d93271e324/ijson-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c594c0abe69d9d6099f4ece17763d53072f65ba60b372d8ba6de8695ce6ee39e", size = 119200 }, { url = "https://files.pythonhosted.org/packages/3e/b7/1d64fbec0d0a7b0c02e9ad988a89614532028ead8bb52a2456c92e6ee35a/ijson-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ff16c224d9bfe4e9e6bd0395826096cda4a3ef51e6c301e1b61007ee2bd24", size = 121107 }, { url = "https://files.pythonhosted.org/packages/d4/b9/01044f09850bc545ffc85b35aaec473d4f4ca2b6667299033d252c1b60dd/ijson-3.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0015354011303175eae7e2ef5136414e91de2298e5a2e9580ed100b728c07e51", size = 116658 }, { url = "https://files.pythonhosted.org/packages/fb/0d/53856b61f3d952d299d1695c487e8e28058d01fa2adfba3d6d4b4660c242/ijson-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034642558afa57351a0ffe6de89e63907c4cf6849070cc10a3b2542dccda1afe", size = 118186 }, { url = "https://files.pythonhosted.org/packages/95/2d/5bd86e2307dd594840ee51c4e32de953fee837f028acf0f6afb08914cd06/ijson-3.3.0-cp311-cp311-win32.whl", hash = "sha256:192e4b65495978b0bce0c78e859d14772e841724d3269fc1667dc6d2f53cc0ea", size = 48938 }, { url = "https://files.pythonhosted.org/packages/55/e1/4ba2b65b87f67fb19d698984d92635e46d9ce9dd748ce7d009441a586710/ijson-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:72e3488453754bdb45c878e31ce557ea87e1eb0f8b4fc610373da35e8074ce42", size = 50972 }, { url = "https://files.pythonhosted.org/packages/8a/4d/3992f7383e26a950e02dc704bc6c5786a080d5c25fe0fc5543ef477c1883/ijson-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:988e959f2f3d59ebd9c2962ae71b97c0df58323910d0b368cc190ad07429d1bb", size = 84550 }, { url = "https://files.pythonhosted.org/packages/1b/cc/3d4372e0d0b02a821b982f1fdf10385512dae9b9443c1597719dd37769a9/ijson-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b2f73f0d0fce5300f23a1383d19b44d103bb113b57a69c36fd95b7c03099b181", size = 57572 }, { url = "https://files.pythonhosted.org/packages/02/de/970d48b1ff9da5d9513c86fdd2acef5cb3415541c8069e0d92a151b84adb/ijson-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ee57a28c6bf523d7cb0513096e4eb4dac16cd935695049de7608ec110c2b751", size = 56902 }, { url = "https://files.pythonhosted.org/packages/5e/a0/4537722c8b3b05e82c23dfe09a3a64dd1e44a013a5ca58b1e77dfe48b2f1/ijson-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0155a8f079c688c2ccaea05de1ad69877995c547ba3d3612c1c336edc12a3a5", size = 127400 }, { url = "https://files.pythonhosted.org/packages/b2/96/54956062a99cf49f7a7064b573dcd756da0563ce57910dc34e27a473d9b9/ijson-3.3.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ab00721304af1ae1afa4313ecfa1bf16b07f55ef91e4a5b93aeaa3e2bd7917c", size = 118786 }, { url = "https://files.pythonhosted.org/packages/07/74/795319531c5b5504508f595e631d592957f24bed7ff51a15bc4c61e7b24c/ijson-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40ee3821ee90be0f0e95dcf9862d786a7439bd1113e370736bfdf197e9765bfb", size = 126288 }, { url = "https://files.pythonhosted.org/packages/69/6a/e0cec06fbd98851d5d233b59058c1dc2ea767c9bb6feca41aa9164fff769/ijson-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3b6987a0bc3e6d0f721b42c7a0198ef897ae50579547b0345f7f02486898f5", size = 129569 }, { url = "https://files.pythonhosted.org/packages/2a/4f/82c0d896d8dcb175f99ced7d87705057bcd13523998b48a629b90139a0dc/ijson-3.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:63afea5f2d50d931feb20dcc50954e23cef4127606cc0ecf7a27128ed9f9a9e6", size = 121508 }, { url = "https://files.pythonhosted.org/packages/2b/b6/8973474eba4a917885e289d9e138267d3d1f052c2d93b8c968755661a42d/ijson-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5c3e285e0735fd8c5a26d177eca8b52512cdd8687ca86ec77a0c66e9c510182", size = 127896 }, { url = "https://files.pythonhosted.org/packages/94/25/00e66af887adbbe70002e0479c3c2340bdfa17a168e25d4ab5a27b53582d/ijson-3.3.0-cp312-cp312-win32.whl", hash = "sha256:907f3a8674e489abdcb0206723e5560a5cb1fa42470dcc637942d7b10f28b695", size = 49272 }, { url = "https://files.pythonhosted.org/packages/25/a2/e187beee237808b2c417109ae0f4f7ee7c81ecbe9706305d6ac2a509cc45/ijson-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f890d04ad33262d0c77ead53c85f13abfb82f2c8f078dfbf24b78f59534dfdd", size = 51272 }, { url = "https://files.pythonhosted.org/packages/b6/c0/a597a720a9f4890121f063d898c707f564ac372fc7a3fc8d044d453566e5/ijson-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3e8d8de44effe2dbd0d8f3eb9840344b2d5b4cc284a14eb8678aec31d1b6bea8", size = 85061 }, { url = "https://files.pythonhosted.org/packages/58/9f/3b0ae9ed8ddb551b3ef10d11592d6fcb70e2a47279d8af5c80464b361be4/ijson-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9cd5c03c63ae06d4f876b9844c5898d0044c7940ff7460db9f4cd984ac7862b5", size = 57814 }, { url = "https://files.pythonhosted.org/packages/f7/1c/3b74fc0f71a830a1f6b258a414263f779d7f94b15ae70c12bae858b6655d/ijson-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04366e7e4a4078d410845e58a2987fd9c45e63df70773d7b6e87ceef771b51ee", size = 57224 }, { url = "https://files.pythonhosted.org/packages/54/b5/1a73769bb003bd3500d5ba720c471fc85b806a3184b214a7cccd6e7e0f0f/ijson-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de7c1ddb80fa7a3ab045266dca169004b93f284756ad198306533b792774f10a", size = 118327 }, { url = "https://files.pythonhosted.org/packages/45/ee/8d82cb62d6306b6f1d5fbbb0fea7652ca2f345dec2c5f38830b587bc2af1/ijson-3.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8851584fb931cffc0caa395f6980525fd5116eab8f73ece9d95e6f9c2c326c4c", size = 112236 }, { url = "https://files.pythonhosted.org/packages/d0/5a/8d56c9806a551b7dec97c081b3a23bf88ada527cef266647681b176e20fe/ijson-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdcfc88347fd981e53c33d832ce4d3e981a0d696b712fbcb45dcc1a43fe65c65", size = 115955 }, { url = "https://files.pythonhosted.org/packages/08/f8/7fa4370ec5b16aa74dcf149812d80c077a3aa73b819a4f6e1fc4bf44c43a/ijson-3.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3917b2b3d0dbbe3296505da52b3cb0befbaf76119b2edaff30bd448af20b5400", size = 117681 }, { url = "https://files.pythonhosted.org/packages/dd/a9/1f4f62c774763d2bf11cf8f3d378cd7836c7f3921c8e30d9934dd2776808/ijson-3.3.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e10c14535abc7ddf3fd024aa36563cd8ab5d2bb6234a5d22c77c30e30fa4fb2b", size = 113630 }, { url = "https://files.pythonhosted.org/packages/d2/ba/0e804b8bceca6027c6d3c6718ed5d280c4a3bdc2a5ade4c5438e5d12bea6/ijson-3.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3aba5c4f97f4e2ce854b5591a8b0711ca3b0c64d1b253b04ea7b004b0a197ef6", size = 114948 }, { url = "https://files.pythonhosted.org/packages/28/46/b57ccd4d5ee7b008a6b8ea3c0267e6c6f004bd804fbcdc2b07c55ce681f2/ijson-3.3.0-cp38-cp38-win32.whl", hash = "sha256:b325f42e26659df1a0de66fdb5cde8dd48613da9c99c07d04e9fb9e254b7ee1c", size = 48980 }, { url = "https://files.pythonhosted.org/packages/24/18/0707991e3b160b96e50d3425745986c1a0f8afd346b175a5716b71fa28cc/ijson-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:ff835906f84451e143f31c4ce8ad73d83ef4476b944c2a2da91aec8b649570e1", size = 50992 }, { url = "https://files.pythonhosted.org/packages/43/ba/d7a3259db956332f17ba93be2980db020e10c1bd01f610ff7d980b281fbd/ijson-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3c556f5553368dff690c11d0a1fb435d4ff1f84382d904ccc2dc53beb27ba62e", size = 85069 }, { url = "https://files.pythonhosted.org/packages/a4/79/97b47b9110fc5ef92d004e615526de6d16af436e7374098004fa79242440/ijson-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e4396b55a364a03ff7e71a34828c3ed0c506814dd1f50e16ebed3fc447d5188e", size = 57818 }, { url = "https://files.pythonhosted.org/packages/9d/e7/69ddad6389f4d96c095e89c80b765189facfa2cb51f72f3b6fdfe4dcb815/ijson-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6850ae33529d1e43791b30575070670070d5fe007c37f5d06aebc1dd152ab3f", size = 57228 }, { url = "https://files.pythonhosted.org/packages/88/84/ba713c8e4f13b0642d7295cc94924fb21e9f26c1fbf71d47fe16f03904f6/ijson-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36aa56d68ea8def26778eb21576ae13f27b4a47263a7a2581ab2ef58b8de4451", size = 116369 }, { url = "https://files.pythonhosted.org/packages/a0/27/ed16f80f7be403f2e4892b1c5eecf18c5bff57cbb23c4b059b9eb0b369cc/ijson-3.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7ec759c4a0fc820ad5dc6a58e9c391e7b16edcb618056baedbedbb9ea3b1524", size = 109994 }, { url = "https://files.pythonhosted.org/packages/5d/90/5071a6f491663d3bf1f4f59acfc6d29ea0e0d1aa13a16f06f03fcc4f3497/ijson-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b51bab2c4e545dde93cb6d6bb34bf63300b7cd06716f195dd92d9255df728331", size = 113745 }, { url = "https://files.pythonhosted.org/packages/de/e3/e39b7a24c156a5d70c39ffb8383231593e549d2e42dda834758f3934fea8/ijson-3.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:92355f95a0e4da96d4c404aa3cff2ff033f9180a9515f813255e1526551298c1", size = 115930 }, { url = "https://files.pythonhosted.org/packages/f3/7a/cd669bf1c65b6b99f4d326e425ef89c02abe62abc36c134e021d8193ecfd/ijson-3.3.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8795e88adff5aa3c248c1edce932db003d37a623b5787669ccf205c422b91e4a", size = 111869 }, { url = "https://files.pythonhosted.org/packages/dd/34/69074a83f3769f527c81952c002ae55e7c43814d1fb71621ada79f2e57b7/ijson-3.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8f83f553f4cde6d3d4eaf58ec11c939c94a0ec545c5b287461cafb184f4b3a14", size = 113322 }, { url = "https://files.pythonhosted.org/packages/e3/d8/2762aac7d749ed443a7c3e25ad071fe143f21ea5f3f33e184e2cf8026c86/ijson-3.3.0-cp39-cp39-win32.whl", hash = "sha256:ead50635fb56577c07eff3e557dac39533e0fe603000684eea2af3ed1ad8f941", size = 48961 }, { url = "https://files.pythonhosted.org/packages/b0/9a/16a68841edea8168a58b200d7b46a7670349ecd35a75bcb96fd84092f603/ijson-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:c8a9befb0c0369f0cf5c1b94178d0d78f66d9cebb9265b36be6e4f66236076b8", size = 50985 }, { url = "https://files.pythonhosted.org/packages/c3/28/2e1cf00abe5d97aef074e7835b86a94c9a06be4629a0e2c12600792b51ba/ijson-3.3.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2af323a8aec8a50fa9effa6d640691a30a9f8c4925bd5364a1ca97f1ac6b9b5c", size = 54308 }, { url = "https://files.pythonhosted.org/packages/04/d2/8c541c28da4f931bac8177e251efe2b6902f7c486d2d4bdd669eed4ff5c0/ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f64f01795119880023ba3ce43072283a393f0b90f52b66cc0ea1a89aa64a9ccb", size = 66010 }, { url = "https://files.pythonhosted.org/packages/d0/02/8fec0b9037a368811dba7901035e8e0973ebda308f57f30c42101a16a5f7/ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a716e05547a39b788deaf22725490855337fc36613288aa8ae1601dc8c525553", size = 66770 }, { url = "https://files.pythonhosted.org/packages/47/23/90c61f978c83647112460047ea0137bde9c7fe26600ce255bb3e17ea7a21/ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473f5d921fadc135d1ad698e2697025045cd8ed7e5e842258295012d8a3bc702", size = 64159 }, { url = "https://files.pythonhosted.org/packages/20/af/aab1a36072590af62d848f03981f1c587ca40a391fc61e418e388d8b0d46/ijson-3.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd26b396bc3a1e85f4acebeadbf627fa6117b97f4c10b177d5779577c6607744", size = 51095 }, { url = "https://files.pythonhosted.org/packages/23/96/1912c04d8fb7af01c641543c93959219f537bf0a3436d976257bbbff76ba/ijson-3.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:45ff05de889f3dc3d37a59d02096948ce470699f2368b32113954818b21aa74a", size = 54327 }, { url = "https://files.pythonhosted.org/packages/89/d0/06c80770772336518b5cbc03c4230068c6b8ffba4d4196d2f71cc5a24f64/ijson-3.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efb521090dd6cefa7aafd120581947b29af1713c902ff54336b7c7130f04c47", size = 65988 }, { url = "https://files.pythonhosted.org/packages/b4/50/3cde97b553df46eb7baf75e67a8440866f18111cd5e1f3c517dc5f95af4d/ijson-3.3.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c727691858fd3a1c085d9980d12395517fcbbf02c69fbb22dede8ee03422da", size = 66731 }, { url = "https://files.pythonhosted.org/packages/c7/2b/4de19c5e73e50d36259bd86e4d776d59779fdeda2238bd2a4744f87af797/ijson-3.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0420c24e50389bc251b43c8ed379ab3e3ba065ac8262d98beb6735ab14844460", size = 64264 }, { url = "https://files.pythonhosted.org/packages/c0/c6/d7824be98f0da83dbcb6d153e553c527d48e69e1cd005f8e30ff51b1a18a/ijson-3.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8fdf3721a2aa7d96577970f5604bd81f426969c1822d467f07b3d844fa2fecc7", size = 51166 }, { url = "https://files.pythonhosted.org/packages/ee/38/7e1988ff3b6eb4fc9f3639ac7bbb7ae3a37d574f212635e3bf0106b6d78d/ijson-3.3.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:891f95c036df1bc95309951940f8eea8537f102fa65715cdc5aae20b8523813b", size = 54336 }, { url = "https://files.pythonhosted.org/packages/e6/8d/556e94b4f7e0c68a35597036ad9329b3edadfc6da260c749e2b55b310798/ijson-3.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed1336a2a6e5c427f419da0154e775834abcbc8ddd703004108121c6dd9eba9d", size = 66028 }, { url = "https://files.pythonhosted.org/packages/ba/bb/3ef5b0298e8e4524ed9aa338ec224cb159b5f9b8cace05be3a6c5c01bd10/ijson-3.3.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0c819f83e4f7b7f7463b2dc10d626a8be0c85fbc7b3db0edc098c2b16ac968e", size = 66796 }, { url = "https://files.pythonhosted.org/packages/2e/c1/d1507639ad7a9f1673a16a6e0993524a65d85e4f65cde1097039c3dfdaba/ijson-3.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33afc25057377a6a43c892de34d229a86f89ea6c4ca3dd3db0dcd17becae0dbb", size = 64215 }, { url = "https://files.pythonhosted.org/packages/1b/36/92ea416ff6383e66d83a576347b7edd9b0aa22cd3bd16c42dbb3608a105b/ijson-3.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7914d0cf083471856e9bc2001102a20f08e82311dfc8cf1a91aa422f9414a0d6", size = 51107 }, ] [[package]] name = "imagesize" version = "1.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, ] [[package]] name = "importlib-metadata" version = "8.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, ] [[package]] name = "importlib-resources" version = "6.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp", marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/be/f3e8c6081b684f176b761e6a2fef02a0be939740ed6f54109a2951d806f3/importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065", size = 43372 } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/6a/4604f9ae2fa62ef47b9de2fa5ad599589d28c9fd1d335f32759813dfa91e/importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717", size = 36115 }, ] [[package]] name = "incremental" version = "24.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/27/87/156b374ff6578062965afe30cc57627d35234369b3336cf244b240c8d8e6/incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9", size = 28157 } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/38/221e5b2ae676a3938c2c1919131410c342b6efc2baffeda395dd66eeca8f/incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe", size = 20516 }, ] [[package]] name = "inflection" version = "0.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091 } wheels = [ { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454 }, ] [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] name = "jinja2" version = "3.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } wheels = [ { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, ] [[package]] name = "jsbeautifier" version = "1.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "editorconfig" }, { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/69/3e/dd37e1a7223247e3ef94714abf572415b89c4e121c4af48e9e4c392e2ca0/jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24", size = 75606 } [[package]] name = "lazy-model" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] sdist = { url = "https://files.pythonhosted.org/packages/47/9e/c60681be72f03845c209a86d5ce0404540c8d1818fc29bc64fc95220de5c/lazy-model-0.2.0.tar.gz", hash = "sha256:57c0e91e171530c4fca7aebc3ac05a163a85cddd941bf7527cc46c0ddafca47c", size = 8152 } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/13/e37962a20f7051b2d6d286c3feb85754f9ea8c4cac302927971e910cc9f6/lazy_model-0.2.0-py3-none-any.whl", hash = "sha256:5a3241775c253e36d9069d236be8378288a93d4fc53805211fd152e04cc9c342", size = 13719 }, ] [[package]] name = "libvalkey" version = "4.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/38/49/57857ba9d02ba4df3bc1e71044f599c82ea590e928328e6b512dbf720228/libvalkey-4.0.1.tar.gz", hash = "sha256:fe60ef535bc826fc35f4019228a0a46bdce8b41fd6013a7591e822a8a17c3170", size = 109005 } wheels = [ { url = "https://files.pythonhosted.org/packages/17/ac/c7b21f810c17527f77c8bd4145e21568a95a4eccc32bce8df4c5ba5d8863/libvalkey-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e180a893ac62e340a63e18c6dcc91fc756c9f2291b47b35ee1febec68c6d13c", size = 43044 }, { url = "https://files.pythonhosted.org/packages/e9/67/84c88cf0e8df1d68da9a2e7f6a79e818031a7ced1b70d66c60041e8dd7b5/libvalkey-4.0.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:f51c08cae774071ea354658f9a9bb7ffb7b1743661011a28668650b130e0d063", size = 82094 }, { url = "https://files.pythonhosted.org/packages/7a/b3/4a7bf5a0275674cb17e0204c05e16356c94406256d811317e6ba87e1f234/libvalkey-4.0.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:98a37d1cb5f4c3dde6646968b0a624d3051fd99176583a5245641050e931a682", size = 44753 }, { url = "https://files.pythonhosted.org/packages/25/2f/aee00783de3ad3fbbadca23692f854f4b9f4b545c55db19657a4330e1bb4/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01c4051c3b772bd3032ca66c96f229412622ef0bef344f9ad645221f56082573", size = 170126 }, { url = "https://files.pythonhosted.org/packages/1d/56/c8f80d3fa881cf79d20b537dcbc3ab5c8776db51cfa50b2a04a3191d027d/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf424fe1f45462ae4fea5f88b250ae86d7217a9662cfe5cd8a25208268129833", size = 165552 }, { url = "https://files.pythonhosted.org/packages/f1/0c/e697bc18740d95dd0a3104404a0fbaaf4f1d463ac6b7ed2f6fc0c8be4b6a/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea9bdd8fc54de6ceea9e28dc28b327a443423dd1d110bb9fa1d67f02626a8679", size = 181753 }, { url = "https://files.pythonhosted.org/packages/07/f6/49e0b1eb0cc9aee67d711386cbda604ea67752d17d9f94b62ea9866716af/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:641eed2e36408b8ba553c4606b2cfbee05286183249c76841188897d595c6639", size = 170657 }, { url = "https://files.pythonhosted.org/packages/b4/07/205b2e63936f880d0ff8cc7cc27059aa698a0d4f02dc9194854eadce985c/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbf76b1b51bd5fe23dd09d0b7599cf6ee7a074e73a1933910e5faa1741408708", size = 170331 }, { url = "https://files.pythonhosted.org/packages/35/93/31734676641cb36a3ac23ffedc89b328a63a2f00eba80fc0554e2dd93872/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:75918faf78b2728ef8a73438361c328d70757cdb0e8bf57fa636e0776f302d4e", size = 164756 }, { url = "https://files.pythonhosted.org/packages/b7/fd/1f8476e45792c1bd6bb364e7f04c0274caa37767d4ed12658b1d863874cf/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:aa678090591e1c28a5f0647ce69531752e8f75e83a03e8963500941475898ccf", size = 162825 }, { url = "https://files.pythonhosted.org/packages/23/03/c293870a74b88d8ce2c5fed2a049eb2ec2373e29ccd3eb3df32217746e00/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a1eff0939e0577ddc6b8b359a846c0a83cb7ed3b0688fe98f8f8cf3ba8aa04b7", size = 176181 }, { url = "https://files.pythonhosted.org/packages/6e/b0/8624fa9195c4af93044860c9685b7667c1b7dd9bff6b62f815e65a512f24/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b3ac608744fc2727eb87cdd7613f8d64b18a210b1661706d2b2de09fffd3d2d0", size = 167755 }, { url = "https://files.pythonhosted.org/packages/f6/66/74f722d90e37addf2b1235ce8294d50751c1a406a7dd05e7b4893f474daa/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e8801cf0274b2a6b0d19ea47de351e5ce67579b8503c4f9905ab53b52fbf270e", size = 165462 }, { url = "https://files.pythonhosted.org/packages/18/a1/16512251a897ad7022787ae395c2ecaf48449f7fce170d6656c5a27df795/libvalkey-4.0.1-cp310-cp310-win32.whl", hash = "sha256:a9438415f500c1b65fe258f102b004ef690db142a74d681d10fd82e344dc947f", size = 19468 }, { url = "https://files.pythonhosted.org/packages/5b/a8/7819672c42b470c67a994db05dd876b88ad97d5e4ae3152a7e49172b0b1b/libvalkey-4.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:cd3495a5c4c7f04c26bd5c34feb15c13da2dab5a349756a3f42f2a15521a5197", size = 21354 }, { url = "https://files.pythonhosted.org/packages/c4/7a/d7e4726c9a08c703fd4e824c7c644ae6c5f0ee3f1b99474a524f0149b77f/libvalkey-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f342da7200765da30e8a6a540722a8e9e689b0b0604e067290d308981d93826f", size = 43043 }, { url = "https://files.pythonhosted.org/packages/27/ae/30ba1da48e143da9c1fa0e4cf4899bbd4402b0a0c8f6c355aa76e89b6490/libvalkey-4.0.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:0db70261f8843007ea995f7adf0d619780380ac3abc4c1ac44ad4f3e885d5594", size = 82093 }, { url = "https://files.pythonhosted.org/packages/84/da/1c2b524ad44a7c24d7e62bb63d995bb8e59863c0f88844778c109604f83f/libvalkey-4.0.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:785c73ba7177777d9af01f48aa3344099815cdae3fef12c5d0f35b9b392f35bf", size = 44754 }, { url = "https://files.pythonhosted.org/packages/f7/1f/954f1ac80371dc50efaada4ca133219e2edb265c136c7fa2af1821caf5b2/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d56ed4c6c17bfb65bf4fe0745fdb3ae6bd1111af6171294497173e3a45226d7", size = 170152 }, { url = "https://files.pythonhosted.org/packages/cf/3f/1fbe055702041bcf2a85e056955219102e8b0aa3967482a5d68f7a3bb8c8/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d99b4adae993b9d8f057e150e5a2f938823d17286abcbc5cca0cb4741c530ddf", size = 165334 }, { url = "https://files.pythonhosted.org/packages/49/ad/ef273ef578fbac6e3b336c2138e457893654da0a40c75fcc76bf28be4761/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d803812b4933a1926479aff4057f06b4332977b796038b4309d98546c56edf5", size = 181816 }, { url = "https://files.pythonhosted.org/packages/2e/87/3b681951477e98e135dee6f8b570779875a96a0367ad886327610f9134ee/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b473dc3d005a9b57e4445cb2a9ab48f8a26ea90889458ef3cb4d3dd7b23b5a26", size = 170687 }, { url = "https://files.pythonhosted.org/packages/13/4c/d384395d2378e6481cdcb1fb7c6e6d0a3ae0feb3dffaefbd6c1eb39cb3d1/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:195f7e78cb6d2c391dac2d0fc1bf7e65555ebad856e0c36bcc4986e0b3b6c616", size = 170453 }, { url = "https://files.pythonhosted.org/packages/8f/d8/1c64a5704ef4f843e2ffab8d815f1c918445f92e38660a6d44c7c64d1fe1/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:79b446abdb18aefc984214de68ac5f50164550a00b703b81c2b9d9c1618f4a13", size = 164803 }, { url = "https://files.pythonhosted.org/packages/e1/65/6d3004b011a799781f379bdba531a3c19bc5f8cd929bafa96fc066a59b12/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3453cd138a43cdcce32cbbbdc99d99472fb7905e56df8ff2f73dac5be70f0657", size = 162855 }, { url = "https://files.pythonhosted.org/packages/fb/b8/35c8f8cedfeaf2ddb261f09e9a618a3e5ce44c8d4ea29e19ce4c1ae175d7/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a9acf658749ee324750643df040a62401d479de9a4507ca8f69bcf02df1b189", size = 176207 }, { url = "https://files.pythonhosted.org/packages/fd/ea/428c9404f41bce80d946ed904b5749a5b3ac2f2a6a1a48f4da1c9b58f8b2/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ebfe6976a10ab6fb84d885622a39ff580803f3244a048b75fb63a97048cb894c", size = 167761 }, { url = "https://files.pythonhosted.org/packages/2f/97/b0e5fc755c78ac23a6e7a5c81288e7b44131becc11af07ef087f54054adf/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:860862322eceb3ed2ff2031663ee42d9ff4146226af3734f818b54a70339c440", size = 165463 }, { url = "https://files.pythonhosted.org/packages/c7/fd/9700a1edec4ebacbcb7ccdf55ac6548f2a5693b016768d721c0d520753ad/libvalkey-4.0.1-cp311-cp311-win32.whl", hash = "sha256:dd96985818cc1ddc8882dda67fa1cf711db37d0a24a4cd70897fd83a7377a11c", size = 19475 }, { url = "https://files.pythonhosted.org/packages/85/25/d59dbdf8cb16d5c1f9215fc7cf66d2cbca6d05008eda1104b321df785647/libvalkey-4.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:4a1174d53d949f38128efc038b2d77cb79c4db127f5707ad350de0cda3aa9451", size = 21358 }, { url = "https://files.pythonhosted.org/packages/2b/62/fb85f94411890d233c74aad7b581bb65a0f809b5757446a280a8fa42a50d/libvalkey-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3d5da92598f8e6a82a5e3dedd49dae39fce6b3089030e17681e2b73df4a6d89", size = 43057 }, { url = "https://files.pythonhosted.org/packages/ed/cb/6f7614cab744f0e4e0eae583b2997bf22ffee4aa333e26786b91342986b6/libvalkey-4.0.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ce623bb8c37beb16d0f2c7c5b7080a3172dae4030e3bcd71326c7731883f766f", size = 82198 }, { url = "https://files.pythonhosted.org/packages/cf/de/9515ba0f436c3e54331b66cf7652c03fc73688e0b6f22853a6fc2cc8aa23/libvalkey-4.0.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:00adc978a791e944e2f6b442212cd3494a8d683ed215ff785dc9384968b212b6", size = 44839 }, { url = "https://files.pythonhosted.org/packages/21/f1/dd28a89f6c89e4fbcd95be18a04758f6f57fc69d38a7bc22a59377b277fc/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:827ea20b5ee1d99cf3d2c10da24c356c55836dd6ff1689b3fbf190b5bffe1605", size = 173314 }, { url = "https://files.pythonhosted.org/packages/c6/17/debc72596eb3e4c27a4ae1a5b5636e99b7b5e606c072c8816358ab69fb7f/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f81f7d806e5dd826532c0a4b6d8bc91a485fba65a3767cfdeb796b493ac59c8c", size = 167968 }, { url = "https://files.pythonhosted.org/packages/fd/00/451b234f5125e0b9396d7ca4b9d2ab9785e21475f4da60ff019e48912f73/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fe45323bbabee8d770127c0a763182a0d540a8c1afe6537d97dcc021fc26c4", size = 184371 }, { url = "https://files.pythonhosted.org/packages/09/c1/e10266e11034af9194feacec78221bb01db6961b662303a980c77a61f159/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c20ec7a26163dad23b0dfdbb81dd13ae3aa4083942b438c07dadaed88fa0ca6c", size = 173422 }, { url = "https://files.pythonhosted.org/packages/88/ba/fe3b25281e41546ea96c41b7899d2a83a262463432280e07daed44beb7f5/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c0d5b4b01c2e4e5bad8339d8b585f42c0b10fb05a6e4469c15c43d925be6285", size = 173617 }, { url = "https://files.pythonhosted.org/packages/05/b8/a75b6edcaabdc6245ee719357d307e2fccb375ca09d012327fbc1ef68771/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c151a43b250b6e6412c5b0a75093c49d248bbe541db91d2d7f04fd523ea616b3", size = 167110 }, { url = "https://files.pythonhosted.org/packages/a9/ae/602254b8865a0fb21df3f3cd57815ca7e6049cd79318bfb426449b661cee/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c8f59bb57f8e02e287da54a2e1250b9d591131922c006b873e5e2cad50fc36c", size = 164932 }, { url = "https://files.pythonhosted.org/packages/0f/c6/116f5432c8234630079f3dadcf48828db41c2bfcdfaeb36ef197a2efa380/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:89e79fade6e6953c786b0e2d255a2930733f5d9e7ef07cf47a4eb0db4eabba5e", size = 178905 }, { url = "https://files.pythonhosted.org/packages/ce/00/ec095e022b7e5c2035787ad7389e0a11157ceb98f4ceae7fc44805907be0/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56495ab791f539dc3ee30378f9783f017e000ad8b03751ad2426003f74eee0bc", size = 170350 }, { url = "https://files.pythonhosted.org/packages/a5/84/d8bd771b07854c58cbf8926d98fed1701a4dcbe97ef31e8fea63416fc461/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:df9d7ba691c49c632bdc953b5c0af5c50231d0ae3bb0397688f63257a12786c0", size = 168125 }, { url = "https://files.pythonhosted.org/packages/45/88/00d5d2d0960d0023c52d25d5ae87d47b5aec995241788be5c424826727aa/libvalkey-4.0.1-cp312-cp312-win32.whl", hash = "sha256:a39ad585b3d2d48d6f5b60941f9d6a5f3f30a396ae129db15bf626316f71594f", size = 19651 }, { url = "https://files.pythonhosted.org/packages/8f/57/a3f837524d63ed927f6ab3da3be2050f6c838279c796d5b44b617cad0047/libvalkey-4.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:7b39754c9cdf7fe704c636a2ea179c17229566e7c79af453df3a604b98879dc3", size = 21451 }, { url = "https://files.pythonhosted.org/packages/51/3c/8571abc9b8a78281312b99348bccb35dfa161a3a3e9b963afdd473beea40/libvalkey-4.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f72346eca7408cfd6d6f407e7e548040b310ed6fdaec7d9ea67b49f20dc90a9e", size = 43065 }, { url = "https://files.pythonhosted.org/packages/8e/08/197217ff273fde5670b84682a2d67fe372890d14595ae0a15284626975ba/libvalkey-4.0.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:66b4558ca5b8fa48fde40dfc79547779d78c5397906f5ea5671b9a75f0952ff4", size = 82206 }, { url = "https://files.pythonhosted.org/packages/51/e1/a806533c315cf758812e4f609548ccbb51c10221e2a3c6f9aa6e17633ee9/libvalkey-4.0.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:33d29f8d826b59e972a8502e8547d5fef7b1a1376fa0884cf1360c15977bca50", size = 44840 }, { url = "https://files.pythonhosted.org/packages/7b/c8/9908fdb4a5661bef8972cf94d149f5c5199c3835bba1a6f523225e2d6c37/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8adea3c5824937c1e94bb1d5bc30c57661e8bbcb1a79e8ead77b45bbe206f488", size = 173200 }, { url = "https://files.pythonhosted.org/packages/80/d2/3b6c235393137b16dacddb04ac6b632f3998cd10cb52b1e34b2eb7bdcabf/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50e956e1bc0ab21e2479fb5729456ae74de808a1be799b9163bf7a25029eeb41", size = 168270 }, { url = "https://files.pythonhosted.org/packages/79/b3/844123ae52d63d9ec97f4ad026054a0b616d83fd886adcfd2f8484939044/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56d6db545639a5fbb1c634621634a7f87845b7867056b1460a8299cc489e3364", size = 184290 }, { url = "https://files.pythonhosted.org/packages/7c/13/390d1f1fac2167e5e4531cbc090ec99e974271fc05e4c0c8597db593596d/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b83e133826ca50506c9f49b5c4fd64ef0dc3bbc6e85bb18b781a08320bf3f0db", size = 173279 }, { url = "https://files.pythonhosted.org/packages/6c/9f/74deb4ea77efb2cbf24c2a7364628b93f3bd02bff7203162c88e9bfe8f13/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e58b6dcea57df7ee8d80f914ed8895141fbb53d6f344b310ebe6cae3e407d0f", size = 173436 }, { url = "https://files.pythonhosted.org/packages/db/e6/9bb87d6d2fe4ba4da960104356c2b165766cb1d420ef1d7f0f671729f3ab/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35576248ac379e755cf40c4ebe6bf735f278d46b5e449d0c8ea9f66869e3a8d4", size = 167050 }, { url = "https://files.pythonhosted.org/packages/2e/aa/35eda3faa3a7e9a5702c1833cf2ff0fdb024661d8342b24393198db968a2/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2be9cd8533638be94956567602554bbffa65d6fc8e758cd628f317a58cbcafda", size = 164934 }, { url = "https://files.pythonhosted.org/packages/ba/54/6439403407317d1228f8b2d5e38917ff162cd86b371bf28953e727674b20/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a03058988844b5a56f296d0d5608bbbe38df1b875246d6c6d7b442af793b5197", size = 178943 }, { url = "https://files.pythonhosted.org/packages/f5/54/8628c445f9683d2b0f2df3cf2084ec48917d33ccf3f857b2b4b25c6ba3a8/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:885a2ca644a2fdaf555e9fdab2bbe7de0f91de4e2a07726751efa35417736d55", size = 170398 }, { url = "https://files.pythonhosted.org/packages/e1/86/9780ad4d818fbb63efb3763041109fbdbe20a901413effa70a8fcf0ec56b/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:90fb5252391a8a9a3bb2a4eadd3f24a8af1032821d818e620da16854fb52503e", size = 168104 }, { url = "https://files.pythonhosted.org/packages/61/b2/8ec653c1e1cb85a8135f6413be90e40c1d3c6732f5933f4d3990f1777d09/libvalkey-4.0.1-cp313-cp313-win32.whl", hash = "sha256:ac6d515ece9c769ce8a0692fcb0d2ceeb5a71503a7db0cbfef0c255b5fb56b19", size = 19657 }, { url = "https://files.pythonhosted.org/packages/61/92/f0b490a7bd7749aeed9e0dfca1f73e860a82ccb3ddb729591e4324c54367/libvalkey-4.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:125ff9fdba066a97e4bbdea69649184a5f5fcb832106bbaca81b80ff8dbee55c", size = 21461 }, { url = "https://files.pythonhosted.org/packages/eb/9b/16cf35d50004c918da27cd9f33e27a1037690b523e8d4e8244b497988d8a/libvalkey-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9f389b48f37e7d14fd42eb47e52b799c1624edafc2b9398b9fe2f4e204d072a4", size = 43052 }, { url = "https://files.pythonhosted.org/packages/f6/19/865dc1a17f8a4b4f5c63cb926bf30cfff61c69446b27f83a9c7b8da65d5e/libvalkey-4.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:200b135258f6ada4aaacf8499e2d2d3484b39a03b2178f64d4da16eb39bcbf77", size = 82130 }, { url = "https://files.pythonhosted.org/packages/b4/03/c12e79b6dfc1d9f90315288bf81e7c4df5ddbfeb22ade52c61d5eacb93ec/libvalkey-4.0.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:4aaa2b8ef0a3cc1a72abbb29667be2321ea5cd31f3b131ea0a35e4ee86caea6f", size = 44774 }, { url = "https://files.pythonhosted.org/packages/d7/84/c6fa981a5a922dccf2c6eebb0d9cad3fbb3122c7d21ad3d9f444f67cc97f/libvalkey-4.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91143449ee77ca072799759469df20ff538b82e0c538e3f3a4afbe5f1c7bfeaf", size = 172594 }, { url = "https://files.pythonhosted.org/packages/ad/28/e1745166d26c73cb7a6fa1df5f51f0326a4e7e79fd8a9948f81a750132e9/libvalkey-4.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e266a71b747cf5f8c8882966c58c080a61f8cb772bbf6dbb2d67530034ebd611", size = 167113 }, { url = "https://files.pythonhosted.org/packages/b1/4e/61c0b3392f8f97bc169c3f9eaee8053ec514e3ee85ed3fea0b3ee0cc6072/libvalkey-4.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9bb157a7fdc91b49d274eeb7961fcdb03d32380d1c25b9bcbb4dd490944539e", size = 183759 }, { url = "https://files.pythonhosted.org/packages/e5/d0/43c41bbbbc05e781396508ad689e3b5507701ba3c57da204bee0486bfb3b/libvalkey-4.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be5dae7eebe303f549b9cab63fb9d6b1a1730b00e5011b0cb3d3403dc2d70ad9", size = 172950 }, { url = "https://files.pythonhosted.org/packages/80/3e/1a03a45f9dfbb634da3db90ec96c92df56fa1a844cbd7a479f790e6b513f/libvalkey-4.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60466c4bfd4d55ce6233d951627a4eacdae6888de0885695b5b6f3deafde57bf", size = 172691 }, { url = "https://files.pythonhosted.org/packages/3c/2f/65e4a05ed217f5e59a5677c5c7fea110f3a343349f0c1b8425132f4a285c/libvalkey-4.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f448af77c485fabb2f93928ca759f9813bc8999f2fb0560d9c2e4870aa6e0edc", size = 165533 }, { url = "https://files.pythonhosted.org/packages/16/6a/4cb7e3770694029c8289e041734f96b0d0203a36d6330bc1276979815191/libvalkey-4.0.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:cc4354d075614a736e62d8283fe173df66e55b5260e6c29ff851a1d3680a5d1e", size = 163574 }, { url = "https://files.pythonhosted.org/packages/26/b7/d49605d0e0a0f998f9addf724fd20ac2142bbb3c714e80f34406d7cee1fc/libvalkey-4.0.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6445163e102f10217532b6547edcc239f2dd0bced13fd982da10b352d0771b21", size = 176969 }, { url = "https://files.pythonhosted.org/packages/ad/04/876ea16da27d5000d6b4d763a0fa0b43a06fb4e45b70acb70f12eeef3f84/libvalkey-4.0.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:aa162ddb08a5d9a3b2d08f6ebe92385a049077c9b4e168e5171c615fbc8155b8", size = 168622 }, { url = "https://files.pythonhosted.org/packages/2b/4d/e208e3335988ae2d35ca07f9e7b4718ccc2c0eef7e50ca6c4149c37c37df/libvalkey-4.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:48cc9f0fd6724780949bddbbadda9ee338b63a8923202bd7df0e2de4feea63bf", size = 166221 }, { url = "https://files.pythonhosted.org/packages/2b/81/828b1db1fb2dbcda215bb3806b89d2493e6c4a7ab4b529c7bcbdc6e1cc96/libvalkey-4.0.1-cp38-cp38-win32.whl", hash = "sha256:471ac81196e1bf5d00069be2bc6fee4f52744b0a3c219b51f3d3115a7903e190", size = 19490 }, { url = "https://files.pythonhosted.org/packages/7f/a8/c34155613194fa9cbf406dd0cb05b965b8280c52d00af12062ddb609b711/libvalkey-4.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:3ff7f2e78c0e195d862cb9bda3b3fe16183713220d5f2faae653d9b187249c9b", size = 21368 }, { url = "https://files.pythonhosted.org/packages/ce/17/d432510a70089a9781b81dd94d649d156f8af9db0c8b9603702b0ec95dcc/libvalkey-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9c5975aae213a3c7251c3e8989c78b2ee39be5eace45a7d99fadc6a4a5a7bc4", size = 43036 }, { url = "https://files.pythonhosted.org/packages/49/88/64f54ed1ce28fc1c27353ff676b8673820384af72bafa12930566c0d8843/libvalkey-4.0.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:c3ce037eeb03682c1dea8435bc0b9c118dba105cd8dca353b4a49cd741fff60f", size = 82089 }, { url = "https://files.pythonhosted.org/packages/e4/2c/a64b3aa6fceb32026f965d983251b59a30017dd973c243ed0050400cebba/libvalkey-4.0.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:d9ff6deda50245842b901501e282be0399fd6c5289ae9f7a6cc5c62a2e5303d7", size = 44751 }, { url = "https://files.pythonhosted.org/packages/a0/9a/aa4cc4075fa05d98607011efd98888bf7e9eb9c7278bc9e3ea14206cc3e8/libvalkey-4.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd77106af55d6c0fb588fe58c2daea2ccf05ffab3713266b66157add022b9623", size = 169574 }, { url = "https://files.pythonhosted.org/packages/bd/7c/870dc7b68a0166bc216c403157e25fdd29e34ed1429b6821903cac31ad81/libvalkey-4.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff5e5559b3aa04f1cd630b07f320ff0bd336471e5c74ef27a30e198b1a35b7c8", size = 164870 }, { url = "https://files.pythonhosted.org/packages/87/6c/525d00c790cfae5fc196d2518de878e15d4b54459dc71aed5137e8bf6282/libvalkey-4.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70deceeb55973d6b5911b06f7ceadeea022a2496a75aa9774453f68b70ab924b", size = 181172 }, { url = "https://files.pythonhosted.org/packages/b0/b6/b0e6a1f509a6a11b3f925038d4e3d0fd9da18d56d3be34ed41b8a44d6c8b/libvalkey-4.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce6b42ab06ba28d7fc8d24b8da9eccda4033e9342b6d4731c823998ec0294a09", size = 170052 }, { url = "https://files.pythonhosted.org/packages/ca/c0/bcedb0f40ee4d12694f3c7d6809d7de8b8205636fe291f1b7daf6094d8cc/libvalkey-4.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38f42b6632fdfd39c0e361d004c0c93f9059b6c3b36626f903a371abbb8af8b", size = 169800 }, { url = "https://files.pythonhosted.org/packages/57/2e/40dfde852a9dde4baf961531a294d02152d89c4bbd7fc72ccf798c92b951/libvalkey-4.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d2175fd547761e67f00141b7d22ea6ba730da6ae72f182b92d122ed6b9371f27", size = 164293 }, { url = "https://files.pythonhosted.org/packages/b7/4f/34c6bd1097b33cc0eeeb514b0992639f6e5df9edf86d424d9ffc09dd6d32/libvalkey-4.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:13856099dd591714975ee4399f1c6a1b87d4a7a79960681b641b57ee70bcc5a3", size = 162324 }, { url = "https://files.pythonhosted.org/packages/83/a8/31fd517128120e4df4aa0209519ac6a467d38ecbc898dae9100a1f288ac7/libvalkey-4.0.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:fa4dc913f28d799e755fbf2bd411bc0e2f342b9a4cdc2336aab68419b405e17d", size = 175654 }, { url = "https://files.pythonhosted.org/packages/8d/f6/b46976335a946e3f3c1cfe8155bcb2b3ad0b6496ec87a8ca90f4da561a56/libvalkey-4.0.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:be54d0ce94ff65ff8eede2ab78635e0f582f27db3e9143a1e132910896157684", size = 167234 }, { url = "https://files.pythonhosted.org/packages/3b/08/df3cdaed3bcd15a9470fbb2511de9b3237e3b956a1c8d835b75ff1d56c0e/libvalkey-4.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6c7500ddd11760372c3d9ace0efea0cf00834857be7f7da6321ba9a0ba01379f", size = 164994 }, { url = "https://files.pythonhosted.org/packages/ff/6b/7342449b847165e773ccf320189161ec26663dbc93fcc5099539a270ae09/libvalkey-4.0.1-cp39-cp39-win32.whl", hash = "sha256:e5b7cf073a416f2be03b6aebe19d626f36a1350da9170dc2947d8364a77d6c3d", size = 19489 }, { url = "https://files.pythonhosted.org/packages/cd/9d/64a33e7141ef8cb63078a3b3e4e4821adfff401198cfa536679ca78e0247/libvalkey-4.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:d050e7ac0906fc1650c5675b0a68da53181425937986559d129da665382e444a", size = 21367 }, { url = "https://files.pythonhosted.org/packages/39/7d/8dec58f9f2f0f4eb8b1b861a4ee5ef2c8e7b63a46f0ffbba274f5170125b/libvalkey-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:75528a996263e57880d12a5b1421d90f3b63d8f65e1229af02075079ba17be0a", size = 37210 }, { url = "https://files.pythonhosted.org/packages/77/d9/857376c3e0af4988d1b8ad13e8ee964dcfae9860e1917febd4f6b0819feb/libvalkey-4.0.1-pp310-pypy310_pp73-macosx_11_0_x86_64.whl", hash = "sha256:4ee4f42a117cc172286f3180fdd2db9d74e7d3f28f97466e29d69e7c126eb084", size = 39638 }, { url = "https://files.pythonhosted.org/packages/c2/ae/9c0fcf578a56d860a9e056e67fbdff54b33952a88b63c384bc05b0cfe215/libvalkey-4.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb120643254a1b44fee2d07a9cb2eef80e65635ac6cd82af7c76b4c09941575e", size = 48760 }, { url = "https://files.pythonhosted.org/packages/ff/3d/76fa39c775c33e356be5b7b687b85d7fea512e1fd314a17b75f143e85b67/libvalkey-4.0.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e62c472774dd38d9e5809d4bea07ec6309a088e2102ddce8beef5a3e0d68b76", size = 56262 }, { url = "https://files.pythonhosted.org/packages/5a/cb/60131ef56e5152829c834c9c55cc5facb0a1988ba909c23d43f13bb65e0d/libvalkey-4.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a73ce687cda35c1a07c24bfb09b3b877c3d74837b59674cded032d03d12c1e55", size = 48920 }, { url = "https://files.pythonhosted.org/packages/e8/ac/69d01a8e2ad5c94bd261784b8e8c2be3992b48492009baf56fcd16d1ab15/libvalkey-4.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1cd593e96281b80f5831292b9befe631a88415c68ad0748fe3d69101a093c501", size = 21366 }, { url = "https://files.pythonhosted.org/packages/28/15/39c3c69ee642ea1271be2c92ca8e628e251b898d98306953b70a37d46310/libvalkey-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50f5f7482aa64038aa93ba12c45bffd2950a5485288558adc80ebc409b20a21", size = 37244 }, { url = "https://files.pythonhosted.org/packages/92/82/8801ecc1d4f875b7f218cc6ad9345cf7ef3c0e9eec2ada6c7fc34b6c5609/libvalkey-4.0.1-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:bda1b59ed326fc0fe909b4be38297d37cba1ee332913f1ab1a8f17bd9791e2c9", size = 39663 }, { url = "https://files.pythonhosted.org/packages/ab/a7/0458e8387f860c96c64242c8c78118d228a54a619a64b59d918c5a44bdf6/libvalkey-4.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f111810607172cada546003d444e9f0d7b2c9d1a5f305e56d365bda89adbce8d", size = 48790 }, { url = "https://files.pythonhosted.org/packages/26/d6/e79606ad905d9ef0df36b5a4c3d933636467eef4f16dcbc719a6800de02d/libvalkey-4.0.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a2cf10a178140e94116ba467c209d38bc73962211a666e5b85e018194dd3c67e", size = 56353 }, { url = "https://files.pythonhosted.org/packages/cb/a9/c62cfa984c864088c22d9b326abdfd83b561d28f8f2eaf9863e6189d3349/libvalkey-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c1b982292085c1b14b487d348b6dd572a1e2cf9b892b96bae8b74d21304f4e4", size = 48991 }, { url = "https://files.pythonhosted.org/packages/c7/8e/322f5eedf6b9e14fa6b9749197b9f8a486736fa29b1b42f26ef0f7e5497c/libvalkey-4.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3a5f27e938173df8dda85c7f76d0f92cfb8a5a6f59ac094da133e70bd5526ccf", size = 21380 }, { url = "https://files.pythonhosted.org/packages/03/86/f6e93a4f19df53f381d66bcd8ef9aa256c660d0eb924e58b0fdccec250b5/libvalkey-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:55a5b78467c0605817e12469630e627c3cccccb44ff810c4943337fbf3979529", size = 37185 }, { url = "https://files.pythonhosted.org/packages/68/c1/cc3658e45979a9cfe0e03df32b863a9ed75d1cedbc12147644ba6e4c406a/libvalkey-4.0.1-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:af5a237ed6fec3c9977dd57248f11d9ab71a2d8a8954eb76f65171b7454f9f23", size = 39605 }, { url = "https://files.pythonhosted.org/packages/76/37/4e13d72109f2e946f9d472b6e9564c332a983d20ef1c1ddc54794d02dc17/libvalkey-4.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5ce282d3b888bbabb71eb065defb66383f0775fb1f12da42edfff1800d85336", size = 48703 }, { url = "https://files.pythonhosted.org/packages/2f/3c/76bfbca61ca46414c2c59a8006a88f04f8653c99a2f41ccc1b0663a8c005/libvalkey-4.0.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c2feb0686f51def52095d3429404db5092efb1edb336a961acc4887389c177a", size = 56226 }, { url = "https://files.pythonhosted.org/packages/29/bb/f245b3b517b9ca1f41a4e3dc3600fdec3c59765d18c1ca079de44b3e0f6e/libvalkey-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30b9048d4f3745ecaa7e82aa985dbe57b8f96c6f5586767b9afd63d1c3021295", size = 48891 }, { url = "https://files.pythonhosted.org/packages/2a/54/c256395784535d31cf398f5a32a4fc2dac31003c28491fe7868e3ea07cd5/libvalkey-4.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2e321a8af5ea12281898744f41cf7f8c4008770dcc12f8f0afbc5f0b3cd40c4f", size = 21367 }, ] [[package]] name = "litestar" version = "2.16.0" source = { editable = "." } dependencies = [ { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "anyio", version = "4.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "click" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "httpx" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "importlib-resources", marker = "python_full_version < '3.9'" }, { name = "litestar-htmx" }, { name = "msgspec", version = "0.18.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "msgspec", version = "0.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "multidict" }, { name = "multipart" }, { name = "polyfactory" }, { name = "pyyaml" }, { name = "rich" }, { name = "rich-click" }, { name = "typing-extensions" }, ] [package.optional-dependencies] annotated-types = [ { name = "annotated-types" }, ] attrs = [ { name = "attrs" }, ] brotli = [ { name = "brotli" }, ] cli = [ { name = "jsbeautifier" }, { name = "uvicorn", extra = ["standard"] }, { name = "uvloop", marker = "sys_platform != 'win32'" }, ] cryptography = [ { name = "cryptography" }, ] full = [ { name = "advanced-alchemy" }, { name = "annotated-types" }, { name = "attrs" }, { name = "brotli" }, { name = "cryptography" }, { name = "email-validator" }, { name = "fast-query-parsers" }, { name = "jinja2" }, { name = "jsbeautifier" }, { name = "mako" }, { name = "minijinja" }, { name = "opentelemetry-instrumentation-asgi" }, { name = "piccolo" }, { name = "picologging", marker = "python_full_version < '3.13'" }, { name = "prometheus-client" }, { name = "pydantic" }, { name = "pydantic-extra-types" }, { name = "pyjwt" }, { name = "redis", extra = ["hiredis"] }, { name = "structlog" }, { name = "uvicorn", extra = ["standard"] }, { name = "uvloop", marker = "sys_platform != 'win32'" }, { name = "valkey", extra = ["libvalkey"] }, ] jinja = [ { name = "jinja2" }, ] jwt = [ { name = "cryptography" }, { name = "pyjwt" }, ] mako = [ { name = "mako" }, ] minijinja = [ { name = "minijinja" }, ] opentelemetry = [ { name = "opentelemetry-instrumentation-asgi" }, ] piccolo = [ { name = "piccolo" }, ] picologging = [ { name = "picologging", marker = "python_full_version < '3.13'" }, ] prometheus = [ { name = "prometheus-client" }, ] pydantic = [ { name = "email-validator" }, { name = "pydantic" }, { name = "pydantic-extra-types" }, ] redis = [ { name = "redis", extra = ["hiredis"] }, ] sqlalchemy = [ { name = "advanced-alchemy" }, ] standard = [ { name = "fast-query-parsers" }, { name = "jinja2" }, { name = "jsbeautifier" }, { name = "uvicorn", extra = ["standard"] }, { name = "uvloop", marker = "sys_platform != 'win32'" }, ] structlog = [ { name = "structlog" }, ] valkey = [ { name = "valkey", extra = ["libvalkey"] }, ] [package.dev-dependencies] dev = [ { name = "aiosqlite" }, { name = "asyncpg" }, { name = "beanie" }, { name = "beautifulsoup4" }, { name = "daphne" }, { name = "fsspec" }, { name = "greenlet" }, { name = "httpx-sse" }, { name = "hypercorn" }, { name = "hypothesis" }, { name = "litestar", extra = ["full"] }, { name = "opentelemetry-sdk" }, { name = "psutil" }, { name = "psycopg", extra = ["binary"], marker = "python_full_version < '3.13'" }, { name = "psycopg", extra = ["c"], marker = "python_full_version >= '3.13' and sys_platform == 'linux'" }, { name = "psycopg", extra = ["pool"] }, { name = "psycopg2-binary" }, { name = "python-dotenv" }, { name = "starlette" }, { name = "trio" }, ] docs = [ { name = "auto-pytabs", extra = ["sphinx"] }, { name = "litestar-sphinx-theme" }, { name = "sphinx" }, { name = "sphinx-autobuild" }, { name = "sphinx-click" }, { name = "sphinx-copybutton" }, { name = "sphinx-design" }, { name = "sphinx-paramlinks" }, { name = "sphinx-toolbox" }, { name = "sphinxcontrib-mermaid" }, ] linting = [ { name = "asyncpg-stubs" }, { name = "codecov-cli" }, { name = "mypy" }, { name = "pre-commit" }, { name = "pyright" }, { name = "ruff" }, { name = "slotscheck" }, { name = "types-beautifulsoup4" }, { name = "types-psutil" }, { name = "types-pyyaml" }, { name = "types-redis" }, ] test = [ { name = "covdefaults" }, { name = "pytest" }, { name = "pytest-asyncio", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest-asyncio", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pytest-cov" }, { name = "pytest-lazy-fixtures" }, { name = "pytest-mock" }, { name = "pytest-rerunfailures" }, { name = "pytest-timeout" }, { name = "pytest-xdist" }, { name = "time-machine" }, ] [package.metadata] requires-dist = [ { name = "advanced-alchemy", marker = "extra == 'sqlalchemy'", specifier = ">=0.2.2" }, { name = "annotated-types", marker = "extra == 'annotated-types'" }, { name = "anyio", specifier = ">=3" }, { name = "attrs", marker = "extra == 'attrs'" }, { name = "brotli", marker = "extra == 'brotli'" }, { name = "click" }, { name = "cryptography", marker = "extra == 'cryptography'" }, { name = "cryptography", marker = "extra == 'jwt'" }, { name = "email-validator", marker = "extra == 'pydantic'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'", specifier = ">=1.2.2" }, { name = "fast-query-parsers", marker = "extra == 'standard'", specifier = ">=1.0.2" }, { name = "httpx", specifier = ">=0.22" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "importlib-resources", marker = "python_full_version < '3.9'", specifier = ">=5.12.0" }, { name = "jinja2", marker = "extra == 'jinja'", specifier = ">=3.1.2" }, { name = "jinja2", marker = "extra == 'standard'" }, { name = "jsbeautifier", marker = "extra == 'cli'" }, { name = "jsbeautifier", marker = "extra == 'standard'" }, { name = "litestar", extras = ["annotated-types", "attrs", "brotli", "cli", "cryptography", "jinja", "jwt", "mako", "minijinja", "opentelemetry", "piccolo", "picologging", "prometheus", "pydantic", "redis", "sqlalchemy", "standard", "structlog", "valkey"], marker = "python_full_version < '3.13' and extra == 'full'" }, { name = "litestar", extras = ["annotated-types", "attrs", "brotli", "cli", "cryptography", "jinja", "jwt", "mako", "minijinja", "opentelemetry", "piccolo", "prometheus", "pydantic", "redis", "sqlalchemy", "standard", "structlog", "valkey"], marker = "python_full_version >= '3.13' and extra == 'full'" }, { name = "litestar-htmx", specifier = ">=0.4.0" }, { name = "mako", marker = "extra == 'mako'", specifier = ">=1.2.4" }, { name = "minijinja", marker = "extra == 'minijinja'", specifier = ">=1.0.0" }, { name = "msgspec", specifier = ">=0.18.2" }, { name = "multidict", specifier = ">=6.0.2" }, { name = "multipart", specifier = ">=1.2.0" }, { name = "opentelemetry-instrumentation-asgi", marker = "extra == 'opentelemetry'" }, { name = "piccolo", marker = "extra == 'piccolo'" }, { name = "picologging", marker = "python_full_version < '3.13' and extra == 'picologging'" }, { name = "polyfactory", specifier = ">=2.6.3" }, { name = "prometheus-client", marker = "extra == 'prometheus'" }, { name = "pydantic", marker = "extra == 'pydantic'" }, { name = "pydantic-extra-types", marker = "python_full_version >= '3.9' and extra == 'pydantic'" }, { name = "pydantic-extra-types", marker = "python_full_version < '3.9' and extra == 'pydantic'", specifier = "!=2.9.0" }, { name = "pyjwt", marker = "extra == 'jwt'", specifier = ">=2.9.0" }, { name = "pyyaml" }, { name = "redis", extras = ["hiredis"], marker = "extra == 'redis'", specifier = ">=4.4.4" }, { name = "rich", specifier = ">=13.0.0" }, { name = "rich-click" }, { name = "structlog", marker = "extra == 'structlog'" }, { name = "typing-extensions" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'cli'" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'" }, { name = "uvloop", marker = "sys_platform != 'win32' and extra == 'cli'", specifier = ">=0.18.0" }, { name = "uvloop", marker = "sys_platform != 'win32' and extra == 'standard'", specifier = ">=0.18.0" }, { name = "valkey", extras = ["libvalkey"], marker = "extra == 'valkey'", specifier = ">=6.0.2" }, ] provides-extras = ["annotated-types", "attrs", "brotli", "cli", "cryptography", "full", "jinja", "jwt", "mako", "minijinja", "opentelemetry", "piccolo", "picologging", "prometheus", "pydantic", "redis", "sqlalchemy", "standard", "structlog", "valkey"] [package.metadata.requires-dev] dev = [ { name = "aiosqlite" }, { name = "asyncpg", specifier = ">=0.29.0" }, { name = "beanie", specifier = ">=1.21.0" }, { name = "beautifulsoup4" }, { name = "daphne", specifier = ">=4.0.0" }, { name = "fsspec" }, { name = "greenlet" }, { name = "httpx-sse" }, { name = "hypercorn", specifier = ">=0.16.0" }, { name = "hypothesis" }, { name = "litestar", extras = ["full"] }, { name = "opentelemetry-sdk" }, { name = "psutil", specifier = ">=5.9.8" }, { name = "psycopg", extras = ["pool"], marker = "python_full_version >= '3.13' and sys_platform != 'linux'" }, { name = "psycopg", extras = ["pool", "binary"], marker = "python_full_version < '3.13'", specifier = ">=3.1.10,<3.2" }, { name = "psycopg", extras = ["pool", "c"], marker = "python_full_version >= '3.13' and sys_platform == 'linux'" }, { name = "psycopg2-binary" }, { name = "python-dotenv" }, { name = "starlette" }, { name = "trio" }, ] docs = [ { name = "auto-pytabs", extras = ["sphinx"], specifier = ">=0.5.0" }, { name = "litestar-sphinx-theme", git = "https://github.com/litestar-org/litestar-sphinx-theme.git" }, { name = "sphinx", specifier = ">=7.1.2" }, { name = "sphinx-autobuild", specifier = ">=2021.3.14" }, { name = "sphinx-click", specifier = ">=4.4.0" }, { name = "sphinx-copybutton", specifier = ">=0.5.2" }, { name = "sphinx-design", specifier = ">=0.5.0" }, { name = "sphinx-paramlinks", specifier = ">=0.6.0" }, { name = "sphinx-toolbox", specifier = ">=3.5.0" }, { name = "sphinxcontrib-mermaid", specifier = ">=0.9.2" }, ] linting = [ { name = "asyncpg-stubs" }, { name = "codecov-cli" }, { name = "mypy" }, { name = "pre-commit" }, { name = "pyright", specifier = "==1.1.344" }, { name = "ruff", specifier = ">=0.2.1" }, { name = "slotscheck" }, { name = "types-beautifulsoup4" }, { name = "types-psutil" }, { name = "types-pyyaml" }, { name = "types-redis" }, ] test = [ { name = "covdefaults" }, { name = "pytest" }, { name = "pytest-asyncio", marker = "python_full_version < '3.9'", specifier = "<=0.24.0" }, { name = "pytest-asyncio", marker = "python_full_version >= '3.9'", specifier = ">0.24.0" }, { name = "pytest-cov" }, { name = "pytest-lazy-fixtures" }, { name = "pytest-mock" }, { name = "pytest-rerunfailures" }, { name = "pytest-timeout" }, { name = "pytest-xdist" }, { name = "time-machine" }, ] [[package]] name = "litestar-htmx" version = "0.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ff/bc/68dda03c35e5067a09a4e57f9b5af958d0718da00ae741483d8d95a8fb5a/litestar_htmx-0.4.0.tar.gz", hash = "sha256:b5f53aa25b665d673fe2e9d835ff711f7fe28fa8f57b87b61e4f6317b983632e", size = 101616 } wheels = [ { url = "https://files.pythonhosted.org/packages/b4/ea/e423f6dd967ae32157e2abc74bf4cd714adf94536ca9926358d4f59da175/litestar_htmx-0.4.0-py3-none-any.whl", hash = "sha256:81e784b91a5a5ca6061d86271e026de7d785d90a4367c6b9c8f0c724b26986c4", size = 9784 }, ] [[package]] name = "litestar-sphinx-theme" version = "0.2.0" source = { git = "https://github.com/litestar-org/litestar-sphinx-theme.git#76b1d0e4c8afff1ad135b1917fe09cf6c1cc6c9b" } dependencies = [ { name = "pydata-sphinx-theme" }, { name = "sphinx-design" }, ] [[package]] name = "livereload" version = "2.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tornado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/fa/049e6a4aa02c5fe21d053bafddb5c03a551f8c667c0a7399c3ef4aa42d36/livereload-2.7.0.tar.gz", hash = "sha256:f4ba199ef93248902841e298670eebfe1aa9e148e19b343bc57dbf1b74de0513", size = 22143 } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/51/7a862d416ed2b4dc90ea272e96a439a430bb4ca74ebcccfcc8dab4bac7e3/livereload-2.7.0-py3-none-any.whl", hash = "sha256:19bee55aff51d5ade6ede0dc709189a0f904d3b906d3ea71641ed548acff3246", size = 22530 }, ] [[package]] name = "mako" version = "1.3.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fa/0b/29bc5a230948bf209d3ed3165006d257e547c02c3c2a96f6286320dfe8dc/mako-1.3.6.tar.gz", hash = "sha256:9ec3a1583713479fae654f83ed9fa8c9a4c16b7bb0daba0e6bbebff50c0d983d", size = 390206 } wheels = [ { url = "https://files.pythonhosted.org/packages/48/22/bc14c6f02e6dccaafb3eba95764c8f096714260c2aa5f76f654fd16a23dd/Mako-1.3.6-py3-none-any.whl", hash = "sha256:a91198468092a2f1a0de86ca92690fb0cfc43ca90ee17e15d93662b4c04b241a", size = 78557 }, ] [[package]] name = "markdown-it-py" version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, ] [[package]] name = "markupsafe" version = "2.1.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } wheels = [ { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206 }, { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079 }, { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620 }, { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818 }, { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493 }, { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630 }, { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745 }, { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021 }, { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659 }, { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213 }, { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219 }, { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098 }, { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014 }, { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220 }, { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756 }, { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988 }, { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718 }, { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317 }, { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670 }, { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224 }, { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192 }, { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072 }, { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928 }, { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106 }, { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781 }, { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518 }, { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669 }, { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933 }, { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656 }, { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206 }, { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193 }, { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073 }, { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486 }, { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685 }, { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338 }, { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439 }, { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531 }, { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823 }, { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658 }, { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211 }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] [[package]] name = "minijinja" version = "2.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c8/0e/7b76e4a45d5665047bb288a3e5f2520f25a691be58a1662e5a0ca80ae5b5/minijinja-2.5.0.tar.gz", hash = "sha256:ca97a8d8053aa0111f855d6706883dbfdbda600bd2dd0e152c03077a42c003e3", size = 218425 } wheels = [ { url = "https://files.pythonhosted.org/packages/a3/d7/d5a36cc6e293a91fe8183155c8c1db26b8ca1f34f8126ae0e76cf6639e42/minijinja-2.5.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:888d88ab7328de337581656b220409aaa85c992d3ec25e50095196fb4f0752ae", size = 1693920 }, { url = "https://files.pythonhosted.org/packages/29/d4/2e8a5cfb701a779e69b5c6e917a96ba70d6fc7f5c358bd645c67cea55716/minijinja-2.5.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de15fc9ee8c6e0cd22799460af499520055139a8db3636e1c7136278215fe095", size = 856027 }, { url = "https://files.pythonhosted.org/packages/66/3f/22a11b8ba1f6c514ab50c64d2cbeccc20cf1bc43e1ab01361566e8365b31/minijinja-2.5.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:516e84a4f03b81002fd0eea966341b8c12fd9ebc4b222dc6ebc50a2b7535df13", size = 849556 }, { url = "https://files.pythonhosted.org/packages/84/3f/6ad712c2943f44781a72d6927063e9c8c4540555d9a5fd92154cf9de8145/minijinja-2.5.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369d6ed56b571feb25f84b87eec0fb286b1931bb472eed1f148ca69777c596d3", size = 915568 }, { url = "https://files.pythonhosted.org/packages/e5/8e/a008fa6a19e3d3d3e6de731acaa5c258d2bc4b60e3023c6b662f020d20ff/minijinja-2.5.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:025c0232bfbb92ff53bd474cfd230b687480fd6b4c4e9b4f5b354d91a62dc144", size = 990999 }, { url = "https://files.pythonhosted.org/packages/86/b8/208121205af6cc20f802a29df4312d75f332a7501c13ed73790e9fc3df82/minijinja-2.5.0-cp38-abi3-win32.whl", hash = "sha256:d393ca982b5189c2a8f42b82612170885b48f500e4afac6dc127e980b286b798", size = 820098 }, { url = "https://files.pythonhosted.org/packages/92/8b/67c5fc89ec9eec5872e222f28671c36963c635d1d22ad93ecd9ee766d279/minijinja-2.5.0-cp38-abi3-win_amd64.whl", hash = "sha256:1ddd1941d0fbf84dbce103f65a3e04eba7d958d08e399f6d3c63bce2fbc1ae0c", size = 868506 }, ] [[package]] name = "more-itertools" version = "10.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 } wheels = [ { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 }, ] [[package]] name = "motor" version = "3.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pymongo" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6a/d1/06af0527fd02d49b203db70dba462e47275a3c1094f830fdaf090f0cb20c/motor-3.6.0.tar.gz", hash = "sha256:0ef7f520213e852bf0eac306adf631aabe849227d8aec900a2612512fb9c5b8d", size = 278447 } wheels = [ { url = "https://files.pythonhosted.org/packages/b4/c2/bba4dce0dc56e49d95c270c79c9330ed19e6b71a2a633aecf53e7e1f04c9/motor-3.6.0-py3-none-any.whl", hash = "sha256:9f07ed96f1754963d4386944e1b52d403a5350c687edc60da487d66f98dbf894", size = 74802 }, ] [[package]] name = "msgpack" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 } wheels = [ { url = "https://files.pythonhosted.org/packages/4b/f9/a892a6038c861fa849b11a2bb0502c07bc698ab6ea53359e5771397d883b/msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", size = 150428 }, { url = "https://files.pythonhosted.org/packages/df/7a/d174cc6a3b6bb85556e6a046d3193294a92f9a8e583cdbd46dc8a1d7e7f4/msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", size = 84131 }, { url = "https://files.pythonhosted.org/packages/08/52/bf4fbf72f897a23a56b822997a72c16de07d8d56d7bf273242f884055682/msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", size = 81215 }, { url = "https://files.pythonhosted.org/packages/02/95/dc0044b439b518236aaf012da4677c1b8183ce388411ad1b1e63c32d8979/msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", size = 371229 }, { url = "https://files.pythonhosted.org/packages/ff/75/09081792db60470bef19d9c2be89f024d366b1e1973c197bb59e6aabc647/msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", size = 378034 }, { url = "https://files.pythonhosted.org/packages/32/d3/c152e0c55fead87dd948d4b29879b0f14feeeec92ef1fd2ec21b107c3f49/msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", size = 363070 }, { url = "https://files.pythonhosted.org/packages/d9/2c/82e73506dd55f9e43ac8aa007c9dd088c6f0de2aa19e8f7330e6a65879fc/msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", size = 359863 }, { url = "https://files.pythonhosted.org/packages/cb/a0/3d093b248837094220e1edc9ec4337de3443b1cfeeb6e0896af8ccc4cc7a/msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", size = 368166 }, { url = "https://files.pythonhosted.org/packages/e4/13/7646f14f06838b406cf5a6ddbb7e8dc78b4996d891ab3b93c33d1ccc8678/msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", size = 370105 }, { url = "https://files.pythonhosted.org/packages/67/fa/dbbd2443e4578e165192dabbc6a22c0812cda2649261b1264ff515f19f15/msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044", size = 68513 }, { url = "https://files.pythonhosted.org/packages/24/ce/c2c8fbf0ded750cb63cbcbb61bc1f2dfd69e16dca30a8af8ba80ec182dcd/msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f", size = 74687 }, { url = "https://files.pythonhosted.org/packages/b7/5e/a4c7154ba65d93be91f2f1e55f90e76c5f91ccadc7efc4341e6f04c8647f/msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", size = 150803 }, { url = "https://files.pythonhosted.org/packages/60/c2/687684164698f1d51c41778c838d854965dd284a4b9d3a44beba9265c931/msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", size = 84343 }, { url = "https://files.pythonhosted.org/packages/42/ae/d3adea9bb4a1342763556078b5765e666f8fdf242e00f3f6657380920972/msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", size = 81408 }, { url = "https://files.pythonhosted.org/packages/dc/17/6313325a6ff40ce9c3207293aee3ba50104aed6c2c1559d20d09e5c1ff54/msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", size = 396096 }, { url = "https://files.pythonhosted.org/packages/a8/a1/ad7b84b91ab5a324e707f4c9761633e357820b011a01e34ce658c1dda7cc/msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", size = 403671 }, { url = "https://files.pythonhosted.org/packages/bb/0b/fd5b7c0b308bbf1831df0ca04ec76fe2f5bf6319833646b0a4bd5e9dc76d/msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", size = 387414 }, { url = "https://files.pythonhosted.org/packages/f0/03/ff8233b7c6e9929a1f5da3c7860eccd847e2523ca2de0d8ef4878d354cfa/msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", size = 383759 }, { url = "https://files.pythonhosted.org/packages/1f/1b/eb82e1fed5a16dddd9bc75f0854b6e2fe86c0259c4353666d7fab37d39f4/msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", size = 394405 }, { url = "https://files.pythonhosted.org/packages/90/2e/962c6004e373d54ecf33d695fb1402f99b51832631e37c49273cc564ffc5/msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", size = 396041 }, { url = "https://files.pythonhosted.org/packages/f8/20/6e03342f629474414860c48aeffcc2f7f50ddaf351d95f20c3f1c67399a8/msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", size = 68538 }, { url = "https://files.pythonhosted.org/packages/aa/c4/5a582fc9a87991a3e6f6800e9bb2f3c82972912235eb9539954f3e9997c7/msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788", size = 74871 }, { url = "https://files.pythonhosted.org/packages/e1/d6/716b7ca1dbde63290d2973d22bbef1b5032ca634c3ff4384a958ec3f093a/msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", size = 152421 }, { url = "https://files.pythonhosted.org/packages/70/da/5312b067f6773429cec2f8f08b021c06af416bba340c912c2ec778539ed6/msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", size = 85277 }, { url = "https://files.pythonhosted.org/packages/28/51/da7f3ae4462e8bb98af0d5bdf2707f1b8c65a0d4f496e46b6afb06cbc286/msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", size = 82222 }, { url = "https://files.pythonhosted.org/packages/33/af/dc95c4b2a49cff17ce47611ca9ba218198806cad7796c0b01d1e332c86bb/msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", size = 392971 }, { url = "https://files.pythonhosted.org/packages/f1/54/65af8de681fa8255402c80eda2a501ba467921d5a7a028c9c22a2c2eedb5/msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", size = 401403 }, { url = "https://files.pythonhosted.org/packages/97/8c/e333690777bd33919ab7024269dc3c41c76ef5137b211d776fbb404bfead/msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", size = 385356 }, { url = "https://files.pythonhosted.org/packages/57/52/406795ba478dc1c890559dd4e89280fa86506608a28ccf3a72fbf45df9f5/msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", size = 383028 }, { url = "https://files.pythonhosted.org/packages/e7/69/053b6549bf90a3acadcd8232eae03e2fefc87f066a5b9fbb37e2e608859f/msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", size = 391100 }, { url = "https://files.pythonhosted.org/packages/23/f0/d4101d4da054f04274995ddc4086c2715d9b93111eb9ed49686c0f7ccc8a/msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", size = 394254 }, { url = "https://files.pythonhosted.org/packages/1c/12/cf07458f35d0d775ff3a2dc5559fa2e1fcd06c46f1ef510e594ebefdca01/msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", size = 69085 }, { url = "https://files.pythonhosted.org/packages/73/80/2708a4641f7d553a63bc934a3eb7214806b5b39d200133ca7f7afb0a53e8/msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", size = 75347 }, { url = "https://files.pythonhosted.org/packages/c8/b0/380f5f639543a4ac413e969109978feb1f3c66e931068f91ab6ab0f8be00/msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", size = 151142 }, { url = "https://files.pythonhosted.org/packages/c8/ee/be57e9702400a6cb2606883d55b05784fada898dfc7fd12608ab1fdb054e/msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", size = 84523 }, { url = "https://files.pythonhosted.org/packages/7e/3a/2919f63acca3c119565449681ad08a2f84b2171ddfcff1dba6959db2cceb/msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", size = 81556 }, { url = "https://files.pythonhosted.org/packages/7c/43/a11113d9e5c1498c145a8925768ea2d5fce7cbab15c99cda655aa09947ed/msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", size = 392105 }, { url = "https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", size = 399979 }, { url = "https://files.pythonhosted.org/packages/82/8c/cf64ae518c7b8efc763ca1f1348a96f0e37150061e777a8ea5430b413a74/msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", size = 383816 }, { url = "https://files.pythonhosted.org/packages/69/86/a847ef7a0f5ef3fa94ae20f52a4cacf596a4e4a010197fbcc27744eb9a83/msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", size = 380973 }, { url = "https://files.pythonhosted.org/packages/aa/90/c74cf6e1126faa93185d3b830ee97246ecc4fe12cf9d2d31318ee4246994/msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", size = 387435 }, { url = "https://files.pythonhosted.org/packages/7a/40/631c238f1f338eb09f4acb0f34ab5862c4e9d7eda11c1b685471a4c5ea37/msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", size = 399082 }, { url = "https://files.pythonhosted.org/packages/e9/1b/fa8a952be252a1555ed39f97c06778e3aeb9123aa4cccc0fd2acd0b4e315/msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", size = 69037 }, { url = "https://files.pythonhosted.org/packages/b6/bc/8bd826dd03e022153bfa1766dcdec4976d6c818865ed54223d71f07862b3/msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", size = 75140 }, { url = "https://files.pythonhosted.org/packages/77/68/6ddc40189295de4363af0597ecafb822ca7636ed1e91626f294cc8bc0d91/msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec", size = 375795 }, { url = "https://files.pythonhosted.org/packages/55/f6/d4859a158a915be52eecd52dee9761ab3a5d84c834a1d13ffc198e068a48/msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96", size = 381539 }, { url = "https://files.pythonhosted.org/packages/98/6c/3b89221b0f6b2fd92572bd752545fc96ca4e494b76e2a02be8da56451909/msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870", size = 369353 }, { url = "https://files.pythonhosted.org/packages/ed/a1/16bd86502f1572a14c6ccfa057306be7f94ea3081ffec652308036cefbd2/msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7", size = 364560 }, { url = "https://files.pythonhosted.org/packages/46/72/0454fa773fc4977ca70ae45471e38b1ab0cd831bef1990e9283d8683fe18/msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb", size = 374203 }, { url = "https://files.pythonhosted.org/packages/fd/2f/885932948ec2f51509691684842f5870f960d908373744070400ac56e2d0/msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f", size = 375978 }, { url = "https://files.pythonhosted.org/packages/37/60/1f79ed762cb2af7ab17bf8f6d7270e022aa26cff06facaf48a82b2c13473/msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b", size = 68763 }, { url = "https://files.pythonhosted.org/packages/a4/b7/1517b4d65caf3394c0e5f4e557dda8eaaed2ad00b4517b7d4c7c2bc86f77/msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb", size = 74910 }, { url = "https://files.pythonhosted.org/packages/f7/3b/544a5c5886042b80e1f4847a4757af3430f60d106d8d43bb7be72c9e9650/msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1", size = 150713 }, { url = "https://files.pythonhosted.org/packages/93/af/d63f25bcccd3d6f06fd518ba4a321f34a4370c67b579ca5c70b4a37721b4/msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48", size = 84277 }, { url = "https://files.pythonhosted.org/packages/92/9b/5c0dfb0009b9f96328664fecb9f8e4e9c8a1ae919e6d53986c1b813cb493/msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c", size = 81357 }, { url = "https://files.pythonhosted.org/packages/d1/7c/3a9ee6ec9fc3e47681ad39b4d344ee04ff20a776b594fba92d88d8b68356/msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468", size = 371256 }, { url = "https://files.pythonhosted.org/packages/f7/0a/8a213cecea7b731c540f25212ba5f9a818f358237ac51a44d448bd753690/msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74", size = 377868 }, { url = "https://files.pythonhosted.org/packages/1b/94/a82b0db0981e9586ed5af77d6cfb343da05d7437dceaae3b35d346498110/msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846", size = 363370 }, { url = "https://files.pythonhosted.org/packages/93/fc/6c7f0dcc1c913e14861e16eaf494c07fc1dde454ec726ff8cebcf348ae53/msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346", size = 358970 }, { url = "https://files.pythonhosted.org/packages/1f/c6/e4a04c0089deace870dabcdef5c9f12798f958e2e81d5012501edaff342f/msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b", size = 366358 }, { url = "https://files.pythonhosted.org/packages/b6/54/7d8317dac590cf16b3e08e3fb74d2081e5af44eb396f0effa13f17777f30/msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8", size = 370336 }, { url = "https://files.pythonhosted.org/packages/dc/6f/a5a1f43b6566831e9630e5bc5d86034a8884386297302be128402555dde1/msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd", size = 68683 }, { url = "https://files.pythonhosted.org/packages/5f/e8/2162621e18dbc36e2bc8492fd0e97b3975f5d89fe0472ae6d5f7fbdd8cf7/msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325", size = 74787 }, ] [[package]] name = "msgspec" version = "0.18.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9' and sys_platform != 'win32'", "python_full_version < '3.9' and sys_platform == 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/5e/fb/42b1865063fddb14dbcbb6e74e0a366ecf1ba371c4948664dde0b0e10f95/msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e", size = 216757 } wheels = [ { url = "https://files.pythonhosted.org/packages/49/54/34c2b70e0d42d876c04f6436c80777d786f25c7536830db5e4ec1aef8788/msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f", size = 202537 }, { url = "https://files.pythonhosted.org/packages/d4/b8/d00d7d03bba8b4eb0bbfdeb6c047163877b2916995f837113d273fd3b774/msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd", size = 192246 }, { url = "https://files.pythonhosted.org/packages/98/07/40bcd501d0f4e76694ca04a11689f3e06d9ef7a31d74e493a2cc34cd9198/msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177", size = 208523 }, { url = "https://files.pythonhosted.org/packages/23/1f/10f2bf07f8fcdc3b0c7bf1bfefdd28bd0353df9290c84e4b3ad8e93e0115/msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410", size = 210276 }, { url = "https://files.pythonhosted.org/packages/c7/e4/4bb5bcd89a74bbb246a21687dd62923c43007e28ad17db24ff58653456cb/msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a", size = 214659 }, { url = "https://files.pythonhosted.org/packages/32/f1/57187427a5a3379cb74aaae753314f9dcde14c259552ec0cb44bcf18db49/msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4", size = 216585 }, { url = "https://files.pythonhosted.org/packages/7d/d1/94919c9b837fc9a0e9dfc1b598a50298bd194146e7bc7d3f42f18826e9f6/msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7", size = 185677 }, { url = "https://files.pythonhosted.org/packages/15/20/278def3822dec807be1e2a734ba9547500ff06667be9dda00ab5d277d605/msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f", size = 200058 }, { url = "https://files.pythonhosted.org/packages/25/8c/75bfafb040934dd3eb46234a2bd4d8fcc7b646f77440866f954b60e0886b/msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa", size = 189108 }, { url = "https://files.pythonhosted.org/packages/0d/e6/5dd960a7678cbaf90dc910611a0e700775ee341876f029c3c987122afe84/msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b", size = 208138 }, { url = "https://files.pythonhosted.org/packages/6a/73/1b2f991dc26899d2f999c938cbc82c858b3cb7e3ccaad317b32760dbe1da/msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07", size = 209538 }, { url = "https://files.pythonhosted.org/packages/29/d4/2fb2d40b3bde566fd14bf02bf503eea20a912a02cdf7ff100629906c9094/msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c", size = 213571 }, { url = "https://files.pythonhosted.org/packages/59/5a/c2aeeefd78946713047637f0c422c0b8b31182eb9bbed0068e906cc8aca0/msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492", size = 215785 }, { url = "https://files.pythonhosted.org/packages/51/c6/0a8ae23c91ba1e6d58ddb089bba4ce8dad5815411b4a2bb40a5f15d2ab73/msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4", size = 185877 }, { url = "https://files.pythonhosted.org/packages/1d/b5/c8fbf1db814eb29eda402952374b594b2559419ba7ec6d0997a9e5687530/msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c", size = 202109 }, { url = "https://files.pythonhosted.org/packages/d7/9a/235d2dbab078a0b8e6f338205dc59be0b027ce000554ee6a9c41b19339e5/msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1", size = 190281 }, { url = "https://files.pythonhosted.org/packages/0e/f2/f864ed36a8a62c26b57c3e08d212bd8f3d12a3ca3ef64600be5452aa3c82/msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466", size = 210305 }, { url = "https://files.pythonhosted.org/packages/73/16/dfef780ced7d690dd5497846ed242ef3e27e319d59d1ddaae816a4f2c15e/msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca", size = 212510 }, { url = "https://files.pythonhosted.org/packages/c1/90/f5b3a788c4b3d92190e3345d1afa3dd107d5f16b8194e1f61b72582ee9bd/msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57", size = 214844 }, { url = "https://files.pythonhosted.org/packages/ce/0b/d4cc1b09f8dfcc6cc4cc9739c13a86e093fe70257b941ea9feb15df22996/msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6", size = 217113 }, { url = "https://files.pythonhosted.org/packages/3f/76/30d8f152299f65c85c46a2cbeaf95ad1d18516b5ce730acdaef696d4cfe6/msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0", size = 187184 }, { url = "https://files.pythonhosted.org/packages/5b/2b/262847e614393f265f00b8096d8f71871b27cb71f68f1250a9eac93cb1bc/msgspec-0.18.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a", size = 201291 }, { url = "https://files.pythonhosted.org/packages/86/6f/1da53a2ba5f312c3dca9e5f38912732e77f996a22945c8d62df7617c4733/msgspec-0.18.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf", size = 191604 }, { url = "https://files.pythonhosted.org/packages/f0/77/00e1e55607de1092dded768eae746cfdfd6f5aca4ad52b9bb11c3e3b1153/msgspec-0.18.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61", size = 210060 }, { url = "https://files.pythonhosted.org/packages/21/e0/1dff019ae22b7d47782d6f1180760828bc96fde368aea983d8e5d872833a/msgspec-0.18.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46", size = 212378 }, { url = "https://files.pythonhosted.org/packages/85/98/da3ad36c242fdf0e6cd9d63e5d47ca53577f23c180ef040f4b3aefb5b88e/msgspec-0.18.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681", size = 215541 }, { url = "https://files.pythonhosted.org/packages/13/cd/29b0de4e0e4a517fff7161fba034df19c45a5a0ef63b728d0e74dba4911d/msgspec-0.18.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c", size = 218414 }, { url = "https://files.pythonhosted.org/packages/1e/b1/1a92bf0dd6354316c9c3a0e6d1123873bb6f21efdb497980e71e843d2f85/msgspec-0.18.6-cp38-cp38-win_amd64.whl", hash = "sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be", size = 187715 }, { url = "https://files.pythonhosted.org/packages/cc/01/54e711813b04a668cbc6467e20ea747aec1aaf2c9afd83ed470d774d22d0/msgspec-0.18.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4", size = 202455 }, { url = "https://files.pythonhosted.org/packages/dd/b6/2a78cdd1ef872ad96c509fc4d732ffd86903861c9b4e0a47c85d0b37b0e3/msgspec-0.18.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e", size = 192001 }, { url = "https://files.pythonhosted.org/packages/87/fc/1e06294be19595fc72e99957bf191a8a51be88487e280841ac5925069537/msgspec-0.18.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c", size = 208372 }, { url = "https://files.pythonhosted.org/packages/b7/ee/9967075f4ea0ca3e841e1b98f0f65a6033c464e3542fe594e2e6dad10029/msgspec-0.18.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07", size = 210257 }, { url = "https://files.pythonhosted.org/packages/70/03/9a16fac8e3de1b1aa30e22db8a38710cbacdb1f25c54dd2fcc0c0fb10585/msgspec-0.18.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be", size = 214445 }, { url = "https://files.pythonhosted.org/packages/67/15/4b8e28bfd836cd0dbf7ac8feb52dc440d9ed028b798090b931aa6fac9636/msgspec-0.18.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece", size = 216412 }, { url = "https://files.pythonhosted.org/packages/cd/b2/283d010db6836db2fe059f7ee3c13823927229975ffbe1edcbeded85a556/msgspec-0.18.6-cp39-cp39-win_amd64.whl", hash = "sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090", size = 185801 }, ] [[package]] name = "msgspec" version = "0.19.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'win32'", "python_full_version >= '3.9' and python_full_version < '3.11' and sys_platform != 'win32'", "python_full_version >= '3.13' and sys_platform == 'linux'", "python_full_version >= '3.13' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version >= '3.9' and python_full_version < '3.11' and sys_platform == 'win32'", "python_full_version >= '3.13' and sys_platform == 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934 } wheels = [ { url = "https://files.pythonhosted.org/packages/13/40/817282b42f58399762267b30deb8ac011d8db373f8da0c212c85fbe62b8f/msgspec-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d8dd848ee7ca7c8153462557655570156c2be94e79acec3561cf379581343259", size = 190019 }, { url = "https://files.pythonhosted.org/packages/92/99/bd7ed738c00f223a8119928661167a89124140792af18af513e6519b0d54/msgspec-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0553bbc77662e5708fe66aa75e7bd3e4b0f209709c48b299afd791d711a93c36", size = 183680 }, { url = "https://files.pythonhosted.org/packages/e5/27/322badde18eb234e36d4a14122b89edd4e2973cdbc3da61ca7edf40a1ccd/msgspec-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe2c4bf29bf4e89790b3117470dea2c20b59932772483082c468b990d45fb947", size = 209334 }, { url = "https://files.pythonhosted.org/packages/c6/65/080509c5774a1592b2779d902a70b5fe008532759927e011f068145a16cb/msgspec-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e87ecfa9795ee5214861eab8326b0e75475c2e68a384002aa135ea2a27d909", size = 211551 }, { url = "https://files.pythonhosted.org/packages/6f/2e/1c23c6b4ca6f4285c30a39def1054e2bee281389e4b681b5e3711bd5a8c9/msgspec-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3c4ec642689da44618f68c90855a10edbc6ac3ff7c1d94395446c65a776e712a", size = 215099 }, { url = "https://files.pythonhosted.org/packages/83/fe/95f9654518879f3359d1e76bc41189113aa9102452170ab7c9a9a4ee52f6/msgspec-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2719647625320b60e2d8af06b35f5b12d4f4d281db30a15a1df22adb2295f633", size = 218211 }, { url = "https://files.pythonhosted.org/packages/79/f6/71ca7e87a1fb34dfe5efea8156c9ef59dd55613aeda2ca562f122cd22012/msgspec-0.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:695b832d0091edd86eeb535cd39e45f3919f48d997685f7ac31acb15e0a2ed90", size = 186174 }, { url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e", size = 187939 }, { url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551", size = 182202 }, { url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029 }, { url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682 }, { url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003 }, { url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833 }, { url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184 }, { url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485 }, { url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910 }, { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633 }, { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594 }, { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053 }, { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081 }, { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467 }, { url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498 }, { url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950 }, { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647 }, { url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563 }, { url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996 }, { url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087 }, { url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432 }, { url = "https://files.pythonhosted.org/packages/ea/d0/323f867eaec1f2236ba30adf613777b1c97a7e8698e2e881656b21871fa4/msgspec-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15c1e86fff77184c20a2932cd9742bf33fe23125fa3fcf332df9ad2f7d483044", size = 189926 }, { url = "https://files.pythonhosted.org/packages/a8/37/c3e1b39bdae90a7258d77959f5f5e36ad44b40e2be91cff83eea33c54d43/msgspec-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b5541b2b3294e5ffabe31a09d604e23a88533ace36ac288fa32a420aa38d229", size = 183873 }, { url = "https://files.pythonhosted.org/packages/cb/a2/48f2c15c7644668e51f4dce99d5f709bd55314e47acb02e90682f5880f35/msgspec-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f5c043ace7962ef188746e83b99faaa9e3e699ab857ca3f367b309c8e2c6b12", size = 209272 }, { url = "https://files.pythonhosted.org/packages/25/3c/aa339cf08b990c3f07e67b229a3a8aa31bf129ed974b35e5daa0df7d9d56/msgspec-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca06aa08e39bf57e39a258e1996474f84d0dd8130d486c00bec26d797b8c5446", size = 211396 }, { url = "https://files.pythonhosted.org/packages/c7/00/c7fb9d524327c558b2803973cc3f988c5100a1708879970a9e377bdf6f4f/msgspec-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e695dad6897896e9384cf5e2687d9ae9feaef50e802f93602d35458e20d1fb19", size = 215002 }, { url = "https://files.pythonhosted.org/packages/3f/bf/d9f9fff026c1248cde84a5ce62b3742e8a63a3c4e811f99f00c8babf7615/msgspec-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3be5c02e1fee57b54130316a08fe40cca53af92999a302a6054cd451700ea7db", size = 218132 }, { url = "https://files.pythonhosted.org/packages/00/03/b92011210f79794958167a3a3ea64a71135d9a2034cfb7597b545a42606d/msgspec-0.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:0684573a821be3c749912acf5848cce78af4298345cb2d7a8b8948a0a5a27cfe", size = 186301 }, ] [[package]] name = "multidict" version = "6.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } wheels = [ { url = "https://files.pythonhosted.org/packages/29/68/259dee7fd14cf56a17c554125e534f6274c2860159692a414d0b402b9a6d/multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60", size = 48628 }, { url = "https://files.pythonhosted.org/packages/50/79/53ba256069fe5386a4a9e80d4e12857ced9de295baf3e20c68cdda746e04/multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1", size = 29327 }, { url = "https://files.pythonhosted.org/packages/ff/10/71f1379b05b196dae749b5ac062e87273e3f11634f447ebac12a571d90ae/multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53", size = 29689 }, { url = "https://files.pythonhosted.org/packages/71/45/70bac4f87438ded36ad4793793c0095de6572d433d98575a5752629ef549/multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5", size = 126639 }, { url = "https://files.pythonhosted.org/packages/80/cf/17f35b3b9509b4959303c05379c4bfb0d7dd05c3306039fc79cf035bbac0/multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581", size = 134315 }, { url = "https://files.pythonhosted.org/packages/ef/1f/652d70ab5effb33c031510a3503d4d6efc5ec93153562f1ee0acdc895a57/multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56", size = 129471 }, { url = "https://files.pythonhosted.org/packages/a6/64/2dd6c4c681688c0165dea3975a6a4eab4944ea30f35000f8b8af1df3148c/multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429", size = 124585 }, { url = "https://files.pythonhosted.org/packages/87/56/e6ee5459894c7e554b57ba88f7257dc3c3d2d379cb15baaa1e265b8c6165/multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748", size = 116957 }, { url = "https://files.pythonhosted.org/packages/36/9e/616ce5e8d375c24b84f14fc263c7ef1d8d5e8ef529dbc0f1df8ce71bb5b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db", size = 128609 }, { url = "https://files.pythonhosted.org/packages/8c/4f/4783e48a38495d000f2124020dc96bacc806a4340345211b1ab6175a6cb4/multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056", size = 123016 }, { url = "https://files.pythonhosted.org/packages/3e/b3/4950551ab8fc39862ba5e9907dc821f896aa829b4524b4deefd3e12945ab/multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76", size = 133542 }, { url = "https://files.pythonhosted.org/packages/96/4d/f0ce6ac9914168a2a71df117935bb1f1781916acdecbb43285e225b484b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160", size = 130163 }, { url = "https://files.pythonhosted.org/packages/be/72/17c9f67e7542a49dd252c5ae50248607dfb780bcc03035907dafefb067e3/multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7", size = 126832 }, { url = "https://files.pythonhosted.org/packages/71/9f/72d719e248cbd755c8736c6d14780533a1606ffb3fbb0fbd77da9f0372da/multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0", size = 26402 }, { url = "https://files.pythonhosted.org/packages/04/5a/d88cd5d00a184e1ddffc82aa2e6e915164a6d2641ed3606e766b5d2f275a/multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d", size = 28800 }, { url = "https://files.pythonhosted.org/packages/93/13/df3505a46d0cd08428e4c8169a196131d1b0c4b515c3649829258843dde6/multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", size = 48570 }, { url = "https://files.pythonhosted.org/packages/f0/e1/a215908bfae1343cdb72f805366592bdd60487b4232d039c437fe8f5013d/multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", size = 29316 }, { url = "https://files.pythonhosted.org/packages/70/0f/6dc70ddf5d442702ed74f298d69977f904960b82368532c88e854b79f72b/multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", size = 29640 }, { url = "https://files.pythonhosted.org/packages/d8/6d/9c87b73a13d1cdea30b321ef4b3824449866bd7f7127eceed066ccb9b9ff/multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", size = 131067 }, { url = "https://files.pythonhosted.org/packages/cc/1e/1b34154fef373371fd6c65125b3d42ff5f56c7ccc6bfff91b9b3c60ae9e0/multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", size = 138507 }, { url = "https://files.pythonhosted.org/packages/fb/e0/0bc6b2bac6e461822b5f575eae85da6aae76d0e2a79b6665d6206b8e2e48/multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", size = 133905 }, { url = "https://files.pythonhosted.org/packages/ba/af/73d13b918071ff9b2205fcf773d316e0f8fefb4ec65354bbcf0b10908cc6/multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", size = 129004 }, { url = "https://files.pythonhosted.org/packages/74/21/23960627b00ed39643302d81bcda44c9444ebcdc04ee5bedd0757513f259/multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", size = 121308 }, { url = "https://files.pythonhosted.org/packages/8b/5c/cf282263ffce4a596ed0bb2aa1a1dddfe1996d6a62d08842a8d4b33dca13/multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", size = 132608 }, { url = "https://files.pythonhosted.org/packages/d7/3e/97e778c041c72063f42b290888daff008d3ab1427f5b09b714f5a8eff294/multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", size = 127029 }, { url = "https://files.pythonhosted.org/packages/47/ac/3efb7bfe2f3aefcf8d103e9a7162572f01936155ab2f7ebcc7c255a23212/multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", size = 137594 }, { url = "https://files.pythonhosted.org/packages/42/9b/6c6e9e8dc4f915fc90a9b7798c44a30773dea2995fdcb619870e705afe2b/multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", size = 134556 }, { url = "https://files.pythonhosted.org/packages/1d/10/8e881743b26aaf718379a14ac58572a240e8293a1c9d68e1418fb11c0f90/multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", size = 130993 }, { url = "https://files.pythonhosted.org/packages/45/84/3eb91b4b557442802d058a7579e864b329968c8d0ea57d907e7023c677f2/multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", size = 26405 }, { url = "https://files.pythonhosted.org/packages/9f/0b/ad879847ecbf6d27e90a6eabb7eff6b62c129eefe617ea45eae7c1f0aead/multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", size = 28795 }, { url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 }, { url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 }, { url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 }, { url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 }, { url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 }, { url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 }, { url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 }, { url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 }, { url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 }, { url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 }, { url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 }, { url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 }, { url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 }, { url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 }, { url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 }, { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, { url = "https://files.pythonhosted.org/packages/3e/6a/af41f3aaf5f00fd86cc7d470a2f5b25299b0c84691163b8757f4a1a205f2/multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392", size = 48597 }, { url = "https://files.pythonhosted.org/packages/d9/d6/3d4082760ed11b05734f8bf32a0615b99e7d9d2b3730ad698a4d7377c00a/multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a", size = 29338 }, { url = "https://files.pythonhosted.org/packages/9d/7f/5d1ce7f47d44393d429922910afbe88fcd29ee3069babbb47507a4c3a7ea/multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2", size = 29562 }, { url = "https://files.pythonhosted.org/packages/ce/ec/c425257671af9308a9b626e2e21f7f43841616e4551de94eb3c92aca75b2/multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc", size = 130980 }, { url = "https://files.pythonhosted.org/packages/d8/d7/d4220ad2633a89b314593e9b85b5bc9287a7c563c7f9108a4a68d9da5374/multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478", size = 136694 }, { url = "https://files.pythonhosted.org/packages/a1/2a/13e554db5830c8d40185a2e22aa8325516a5de9634c3fb2caf3886a829b3/multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4", size = 131616 }, { url = "https://files.pythonhosted.org/packages/2e/a9/83692e37d8152f104333132105b67100aabfb2e96a87f6bed67f566035a7/multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d", size = 129664 }, { url = "https://files.pythonhosted.org/packages/cc/1c/1718cd518fb9da7e8890d9d1611c1af0ea5e60f68ff415d026e38401ed36/multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6", size = 121855 }, { url = "https://files.pythonhosted.org/packages/2b/92/f6ed67514b0e3894198f0eb42dcde22f0851ea35f4561a1e4acf36c7b1be/multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2", size = 127928 }, { url = "https://files.pythonhosted.org/packages/f7/30/c66954115a4dc4dc3c84e02c8ae11bb35a43d79ef93122c3c3a40c4d459b/multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd", size = 122793 }, { url = "https://files.pythonhosted.org/packages/62/c9/d386d01b43871e8e1631eb7b3695f6af071b7ae1ab716caf371100f0eb24/multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6", size = 132762 }, { url = "https://files.pythonhosted.org/packages/69/ff/f70cb0a2f7a358acf48e32139ce3a150ff18c961ee9c714cc8c0dc7e3584/multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492", size = 127872 }, { url = "https://files.pythonhosted.org/packages/89/5b/abea7db3ba4cd07752a9b560f9275a11787cd13f86849b5d99c1ceea921d/multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd", size = 126161 }, { url = "https://files.pythonhosted.org/packages/22/03/acc77a4667cca4462ee974fc39990803e58fa573d5a923d6e82b7ef6da7e/multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167", size = 26338 }, { url = "https://files.pythonhosted.org/packages/90/bf/3d0c1cc9c8163abc24625fae89c0ade1ede9bccb6eceb79edf8cff3cca46/multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef", size = 28736 }, { url = "https://files.pythonhosted.org/packages/e7/c9/9e153a6572b38ac5ff4434113af38acf8d5e9957897cdb1f513b3d6614ed/multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c", size = 48550 }, { url = "https://files.pythonhosted.org/packages/76/f5/79565ddb629eba6c7f704f09a09df085c8dc04643b12506f10f718cee37a/multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1", size = 29298 }, { url = "https://files.pythonhosted.org/packages/60/1b/9851878b704bc98e641a3e0bce49382ae9e05743dac6d97748feb5b7baba/multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c", size = 29641 }, { url = "https://files.pythonhosted.org/packages/89/87/d451d45aab9e422cb0fb2f7720c31a4c1d3012c740483c37f642eba568fb/multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c", size = 126202 }, { url = "https://files.pythonhosted.org/packages/fa/b4/27cbe9f3e2e469359887653f2e45470272eef7295139916cc21107c6b48c/multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f", size = 133925 }, { url = "https://files.pythonhosted.org/packages/4d/a3/afc841899face8adfd004235ce759a37619f6ec99eafd959650c5ce4df57/multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875", size = 129039 }, { url = "https://files.pythonhosted.org/packages/5e/41/0d0fb18c1ad574f807196f5f3d99164edf9de3e169a58c6dc2d6ed5742b9/multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255", size = 124072 }, { url = "https://files.pythonhosted.org/packages/00/22/defd7a2e71a44e6e5b9a5428f972e5b572e7fe28e404dfa6519bbf057c93/multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30", size = 116532 }, { url = "https://files.pythonhosted.org/packages/91/25/f7545102def0b1d456ab6449388eed2dfd822debba1d65af60194904a23a/multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057", size = 128173 }, { url = "https://files.pythonhosted.org/packages/45/79/3dbe8d35fc99f5ea610813a72ab55f426cb9cf482f860fa8496e5409be11/multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657", size = 122654 }, { url = "https://files.pythonhosted.org/packages/97/cb/209e735eeab96e1b160825b5d0b36c56d3862abff828fc43999bb957dcad/multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28", size = 133197 }, { url = "https://files.pythonhosted.org/packages/e4/3a/a13808a7ada62808afccea67837a79d00ad6581440015ef00f726d064c2d/multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972", size = 129754 }, { url = "https://files.pythonhosted.org/packages/77/dd/8540e139eafb240079242da8f8ffdf9d3f4b4ad1aac5a786cd4050923783/multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43", size = 126402 }, { url = "https://files.pythonhosted.org/packages/86/99/e82e1a275d8b1ea16d3a251474262258dbbe41c05cce0c01bceda1fc8ea5/multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada", size = 26421 }, { url = "https://files.pythonhosted.org/packages/86/1c/9fa630272355af7e4446a2c7550c259f11ee422ab2d30ff90a0a71cf3d9e/multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a", size = 28791 }, { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, ] [[package]] name = "multipart" version = "1.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/df/91/6c93b6a95e6a99ef929a99d019fbf5b5f7fd3368389a0b1ec7ce0a23565b/multipart-1.2.1.tar.gz", hash = "sha256:829b909b67bc1ad1c6d4488fcdc6391c2847842b08323addf5200db88dbe9480", size = 36507 } wheels = [ { url = "https://files.pythonhosted.org/packages/cc/d1/3598d1e73385baaab427392856f915487db7aa10abadd436f8f2d3e3b0f9/multipart-1.2.1-py3-none-any.whl", hash = "sha256:c03dc203bc2e67f6b46a599467ae0d87cf71d7530504b2c1ff4a9ea21d8b8c8c", size = 13730 }, ] [[package]] name = "mypy" version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 } wheels = [ { url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731 }, { url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276 }, { url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706 }, { url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586 }, { url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318 }, { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 }, { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 }, { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 }, { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 }, { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 }, { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 }, { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 }, { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 }, { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 }, { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 }, { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 }, { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 }, { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 }, { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 }, { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 }, { url = "https://files.pythonhosted.org/packages/5e/2a/13e9ad339131c0fba5c70584f639005a47088f5eed77081a3d00479df0ca/mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a", size = 10955147 }, { url = "https://files.pythonhosted.org/packages/94/39/02929067dc16b72d78109195cfed349ac4ec85f3d52517ac62b9a5263685/mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb", size = 10138373 }, { url = "https://files.pythonhosted.org/packages/4a/cc/066709bb01734e3dbbd1375749f8789bf9693f8b842344fc0cf52109694f/mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b", size = 12543621 }, { url = "https://files.pythonhosted.org/packages/f5/a2/124df839025348c7b9877d0ce134832a9249968e3ab36bb826bab0e9a1cf/mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74", size = 13050348 }, { url = "https://files.pythonhosted.org/packages/45/86/cc94b1e7f7e756a63043cf425c24fb7470013ee1c032180282db75b1b335/mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6", size = 9615311 }, { url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906 }, { url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657 }, { url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394 }, { url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591 }, { url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690 }, { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, ] [[package]] name = "mypy-extensions" version = "1.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, ] [[package]] name = "natsort" version = "8.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575 } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268 }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] [[package]] name = "opentelemetry-api" version = "1.28.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecated" }, { name = "importlib-metadata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/51/34/e4e9245c868c6490a46ffedf6bd5b0f512bbc0a848b19e3a51f6bbad648c/opentelemetry_api-1.28.2.tar.gz", hash = "sha256:ecdc70c7139f17f9b0cf3742d57d7020e3e8315d6cffcdf1a12a905d45b19cc0", size = 62796 } wheels = [ { url = "https://files.pythonhosted.org/packages/4d/58/b17393cdfc149e14ee84c662abf921993dcce8058628359ef1f49e2abb97/opentelemetry_api-1.28.2-py3-none-any.whl", hash = "sha256:6fcec89e265beb258fe6b1acaaa3c8c705a934bd977b9f534a2b7c0d2d4275a6", size = 64302 }, ] [[package]] name = "opentelemetry-instrumentation" version = "0.49b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "packaging" }, { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/1f/9fa51f6f64f4d179f4e3370eb042176ff7717682428552f5e1f4c5efcc09/opentelemetry_instrumentation-0.49b2.tar.gz", hash = "sha256:8cf00cc8d9d479e4b72adb9bd267ec544308c602b7188598db5a687e77b298e2", size = 26480 } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/e3/ad23372525653b0221212d5e2a71bd97aae64cc35f90cbf0c70de57dfa4e/opentelemetry_instrumentation-0.49b2-py3-none-any.whl", hash = "sha256:f6d782b0ef9fef4a4c745298651c65f5c532c34cd4c40d230ab5b9f3b3b4d151", size = 30693 }, ] [[package]] name = "opentelemetry-instrumentation-asgi" version = "0.49b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] sdist = { url = "https://files.pythonhosted.org/packages/84/42/079079bd7c0423bfab987a6457e34468b6ddccf501d3c91d2795c200d65d/opentelemetry_instrumentation_asgi-0.49b2.tar.gz", hash = "sha256:2af5faf062878330714efe700127b837038c4d9d3b70b451ab2424d5076d6c1c", size = 24106 } wheels = [ { url = "https://files.pythonhosted.org/packages/3f/82/06a56e786de3ea0ef4703ed313d9d8395fb4bc9ae740cc71415178ae8bff/opentelemetry_instrumentation_asgi-0.49b2-py3-none-any.whl", hash = "sha256:c8ede13ed781402458a800411cb7ec16a25386dc21de8e5b9a568b386a1dc5f4", size = 16305 }, ] [[package]] name = "opentelemetry-sdk" version = "1.28.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4b/f4/840a5af4efe48d7fb4c456ad60fd624673e871a60d6494f7ff8a934755d4/opentelemetry_sdk-1.28.2.tar.gz", hash = "sha256:5fed24c5497e10df30282456fe2910f83377797511de07d14cec0d3e0a1a3110", size = 157272 } wheels = [ { url = "https://files.pythonhosted.org/packages/da/8b/4f2b418496c08016d4384f9b1c4725a8af7faafa248d624be4bb95993ce1/opentelemetry_sdk-1.28.2-py3-none-any.whl", hash = "sha256:93336c129556f1e3ccd21442b94d3521759541521861b2214c499571b85cb71b", size = 118757 }, ] [[package]] name = "opentelemetry-semantic-conventions" version = "0.49b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecated" }, { name = "opentelemetry-api" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/0a/e3b93f94aa3223c6fd8e743502a1fefd4fb3a753d8f501ce2a418f7c0bd4/opentelemetry_semantic_conventions-0.49b2.tar.gz", hash = "sha256:44e32ce6a5bb8d7c0c617f84b9dc1c8deda1045a07dc16a688cc7cbeab679997", size = 95213 } wheels = [ { url = "https://files.pythonhosted.org/packages/b1/be/6661c8f76708bb3ba38c90be8fa8d7ffe17ccbc5cbbc229334f5535f6448/opentelemetry_semantic_conventions-0.49b2-py3-none-any.whl", hash = "sha256:51e7e1d0daa958782b6c2a8ed05e5f0e7dd0716fc327ac058777b8659649ee54", size = 159199 }, ] [[package]] name = "opentelemetry-util-http" version = "0.49b2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/28/ac5b1a0fd210ecb6c86c5e04256ba09c8308eb41e116097b9e2714d4b8dd/opentelemetry_util_http-0.49b2.tar.gz", hash = "sha256:5958c7009f79146bbe98b0fdb23d9d7bf1ea9cd154a1c199029b1a89e0557199", size = 7861 } wheels = [ { url = "https://files.pythonhosted.org/packages/19/22/9128f10d1c2868ee42df7e10937d00f154a69bee87c416ca9b20a6af6c54/opentelemetry_util_http-0.49b2-py3-none-any.whl", hash = "sha256:e325d6511c6bee7b43170eb0c93261a210ec57e20ab1d7a99838515ef6d2bf58", size = 6941 }, ] [[package]] name = "outcome" version = "1.3.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 } wheels = [ { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692 }, ] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, ] [[package]] name = "piccolo" version = "1.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "black" }, { name = "colorama" }, { name = "inflection" }, { name = "jinja2" }, { name = "pydantic", extra = ["email"] }, { name = "targ" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/16/6661f9e716ec31d6d3f2057890dc20a00166d0befd436db546edc5830b8e/piccolo-1.21.0.tar.gz", hash = "sha256:0e639b0aa3a43d8a7644b396770acf1bf473dc125de6ce565cc5998a5395f5d5", size = 276901 } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/66/8e743fea2940f4ce8b17834e64a8ff09a8eda38d91eae15208953dcbd4a6/piccolo-1.21.0-py3-none-any.whl", hash = "sha256:763e033547dcdb5ef602dc562fc75296101d78e0d3e1289135d25820afb3b118", size = 392851 }, ] [[package]] name = "picologging" version = "0.9.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2d/6d/3fd3423e0391cf7bfb78d6d7510da77068aeb73a0c6db22a6a0078413cab/picologging-0.9.3.tar.gz", hash = "sha256:6921f86ea0875ac85e252188627e9f04b872895327a7028e06c825ddb888a825", size = 103751 } wheels = [ { url = "https://files.pythonhosted.org/packages/a8/13/ef1eb916f920a04ceebf2419ed3a3a7ec86afe88d0ab535b1692489ed788/picologging-0.9.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:3d3219765be2430c4e434ff66a90147bd29db56cd24be00c5999d9827b08d479", size = 163223 }, { url = "https://files.pythonhosted.org/packages/cc/59/89fdceb1267e62e27b61e0b06b20bc76fea17b375a2a0d9ab8bd4d635d07/picologging-0.9.3-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1c4755c06c37b53e70ebdc9cd2b39c835221a12c5ef149cfaad9b6073b41ee89", size = 98747 }, { url = "https://files.pythonhosted.org/packages/3f/eb/651438c3733250bf2f437a18268a8ea8dcdab8e4b795a00b4fb7bd21efd5/picologging-0.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b582222a768f2bc28ca4a34aa41a008c6aa3430cf1f128b2440e93f691d0d714", size = 165551 }, { url = "https://files.pythonhosted.org/packages/3c/40/6d830b59d01d2ab8fb22fc8430d734a3176992cd66c52e1eca7012af3f8d/picologging-0.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f833de82e5fd9a981f64ebfadc3f4d12fe75ef31897e796ba04f6e28455e42c9", size = 177158 }, { url = "https://files.pythonhosted.org/packages/55/86/ba8c330808709ca0d34a89efa49b487577b4dfc3f03cb6fd7e1960f1d01e/picologging-0.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b21e46794ef29fda0033a2bcfc38ec7a5fea78a6353a5f74f7a6374c0c0d3d15", size = 165298 }, { url = "https://files.pythonhosted.org/packages/59/39/ba0d22a86e49058f89a1a2fa250b2e51404b22f038a843f3fcc3d697bd24/picologging-0.9.3-cp310-cp310-win32.whl", hash = "sha256:3ef12744c17fb670e5028315019589fe21527d1546463ef9baf9a454dcd3d7c4", size = 78095 }, { url = "https://files.pythonhosted.org/packages/97/0d/9ecf95e091c06363041f4822faa75cc137a77e7c4d029243bd5f032f6a8e/picologging-0.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:09f27425175ae23cca0fb49bb55622073589cb57f73bdd2c7f72f4cf05419c3e", size = 86671 }, { url = "https://files.pythonhosted.org/packages/6a/bf/af16546887682e76979a5920fdf170b715c485b0c0a37954115837dad046/picologging-0.9.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:fbd88e3dde71d72ac72dfe6a23a52e13fcdf2c9cf033e13a8f2718746fed942b", size = 163401 }, { url = "https://files.pythonhosted.org/packages/eb/88/97907480df34edcc8382a82bdcb7b2090d55b2de4b64e270e5c732447da0/picologging-0.9.3-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:542628ccdc7a1eb321e5a7533803577a943c7d84e62456b681037f7bbc8abe26", size = 98808 }, { url = "https://files.pythonhosted.org/packages/5e/a4/1c3acd317d54ad1cadfc729f63eb6205bc90027e5ea3f6c366e9bc2ae17f/picologging-0.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78ed29eea9f28d28edb39fa81b448fd27bdf510d4e4c79fe64e08742c782a965", size = 165660 }, { url = "https://files.pythonhosted.org/packages/97/f3/67e19ea6595ad31ebd8bd398f3fe1899709bc7f439fce1694a154e2d7e20/picologging-0.9.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a38eb07ef48218712ce9f860b4a27e5086a89a89af7c5bcc1eaed6fb1e3eafd", size = 177284 }, { url = "https://files.pythonhosted.org/packages/da/55/4c67d07472870e5d81f5fd1b38fe0b0ab9f08ad140ae4a6ae3d2aa029832/picologging-0.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3495969c6a8e1e2fa8d2bc89b8478bce10716bb6617f50822667909e79478cb4", size = 165449 }, { url = "https://files.pythonhosted.org/packages/76/05/b0f47219c5230943e1d03bc92bbd74af94ef7bf471e425163296f5b0f05e/picologging-0.9.3-cp311-cp311-win32.whl", hash = "sha256:c97f0ab43ee32924b33cadd176c5bfe404e85e2bc6ae5e4c713c6d4cb4c7e6f3", size = 78141 }, { url = "https://files.pythonhosted.org/packages/1c/38/e1e54b0ddc798173a209b932e939f3f37d2bb4d8cc24c528745a32a08035/picologging-0.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:f8d448e063b8a2cbe3c76d2d7ca2beb523779a31a8b14ee0a452ec1247ce56b8", size = 86727 }, { url = "https://files.pythonhosted.org/packages/65/4a/cfd4d86d23ed1535e6728a13e56a1374105430dad894bb7395e78c0cd974/picologging-0.9.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:70e6957df044af10ff35d293b8ebad3f04bbfa99f88bcf4fd2a8248d1cd46320", size = 164148 }, { url = "https://files.pythonhosted.org/packages/c6/71/7adec10b32be86f54666006e75ee7c941d7823fd4b89289660d4261e8c6d/picologging-0.9.3-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:32d18fb7c089afe9c2a8cb1493021417c872f807d74f3fd6da85e43226664ccd", size = 99348 }, { url = "https://files.pythonhosted.org/packages/02/d3/06225fd4e5d09e35aedd82cf2c608c8a7cbe6da0b977c166af1895f6f370/picologging-0.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:025a6262280374413142648cf5be7944136c54a5cc84b0ff5b2416928d7aec96", size = 166198 }, { url = "https://files.pythonhosted.org/packages/c5/69/84a6cb4a9f2f427ba3e207c6a640db7d5f2557ae56cd10a64cdfdf9541ea/picologging-0.9.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:feaf33a2fd8a9431f3f4de9693569952ebce6e4c92cabe642327130a1d378b6c", size = 178075 }, { url = "https://files.pythonhosted.org/packages/96/b9/b7bc99e4193688d1316189e3b8c9788105361f2d9b00fd64ef7911a3a7b9/picologging-0.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06d315458ff92a2df6ce07540b390212d179e3ef2d08b48c356df72be8d0ce2d", size = 166276 }, { url = "https://files.pythonhosted.org/packages/9b/6e/21abb1f3efdb4797b792d5afcb215f38971eff412c7d4e6ee5d85d86bcb9/picologging-0.9.3-cp312-cp312-win32.whl", hash = "sha256:502a17cbf7303499e1edcb01b3503caeac204aa5d5f888d4b5ecdb113f0c25ee", size = 78613 }, { url = "https://files.pythonhosted.org/packages/34/29/1f3aa01d4b2f97583d7c98259f49cdb425e4980518a7c8348d552dd6a30b/picologging-0.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:16f111cdf7a210eb07bd06932c9a8ff06dc830e213c8a0efbb769e586d1f3efb", size = 87018 }, { url = "https://files.pythonhosted.org/packages/3c/42/a3e939b9d4439f1b8277a070a45d110d8e9254c509c163a9ce5a161aa569/picologging-0.9.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:6672feda1d95e81694b447c27ce51d0beec86f779ed6803e2b4a522086ac7765", size = 162516 }, { url = "https://files.pythonhosted.org/packages/62/c2/0e1971c9a756ac09a08abbdcd281b87641dc2ee37ec97a10ff682433930e/picologging-0.9.3-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:94f71cf9a868047db5414f8358c7df51f3c1913a8f1e892f79ad334e63c85860", size = 98312 }, { url = "https://files.pythonhosted.org/packages/09/d8/3603d409f8230acd4b03ae2060beb6c3b877524ec56df75cc4802f3a5e76/picologging-0.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:936a23a8b98fac51a0bfc3ec23dd2c156fc6b8839a522287ca295f59a5c6dfef", size = 165440 }, { url = "https://files.pythonhosted.org/packages/60/28/5f857c479576e370b9f3165deacd2fd57f99b44f7b764e29c22406aaca82/picologging-0.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce7aad1ccc61d65bafcfda7429d902bce53a706cd8b1ade48a4eb80900f0654e", size = 176970 }, { url = "https://files.pythonhosted.org/packages/a4/c7/7730a79530ca5f053cb46ca56396a5e20a463f479c83e5ff30ad941d5c33/picologging-0.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251d69cf4f9d8e7f9cfb113a58d37d493555ee6380af5e4e11599c4f4b60f735", size = 165161 }, { url = "https://files.pythonhosted.org/packages/38/0a/a4c43b40625adeac827f94ab1d10f46a685fccdc77a92c0767c552dc0aae/picologging-0.9.3-cp38-cp38-win32.whl", hash = "sha256:f66744aa21d4741ba5e6ecc21babaa3b2cb4dc6e944a02cd5d428ebf24992751", size = 77847 }, { url = "https://files.pythonhosted.org/packages/ea/2b/6c1a0bd18e013af827c23443b8108aa18ab0f04e8635c9bb22e030f6ddac/picologging-0.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:ccc190c6eb8e75fd6ce3e7a34f923731ad5a8c58e26e5a83cbc922174cc79e3b", size = 86440 }, { url = "https://files.pythonhosted.org/packages/22/6d/3fde8f7cac72f76365b962b8a9178d56e0806fb040761a6ed4cd4157b253/picologging-0.9.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b4c7f4bf9a5fa291e0f8f8b669f6ea35943ccf2a962903472591ae377de58825", size = 163221 }, { url = "https://files.pythonhosted.org/packages/cb/8c/c1054b46f18e81ba2f145b557b28aef0c09510375dae3ff6ec4e98262f6f/picologging-0.9.3-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:ea5663f5907910405edf75d438c05b24b66fd23cb997d7d28dfaabdea9b78211", size = 98745 }, { url = "https://files.pythonhosted.org/packages/29/87/760b848042b1087fa87e5b5a48cd77bf943716965270332bd92aba68b70f/picologging-0.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a629db4cbbb96e66d01d34c93d197c04cb71838830c6652033ee8d2ebe76d01f", size = 165538 }, { url = "https://files.pythonhosted.org/packages/45/d4/234dddc7102630273bd10b6bc8d0e4e046841edc390491ae9d306052c2b2/picologging-0.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:759f21dda1bf9ee52c7fa91c1faf47aa3585f4f604d5d38de02f3bcd32d7369f", size = 177133 }, { url = "https://files.pythonhosted.org/packages/9f/4d/a243d32fd5613a948a2f3ba3a1307fe5942fe96173e09809856a071ff890/picologging-0.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d4bb95e708ad973217cc2cbb6e058f777ae2539a5cf131824c66fd701adfe71", size = 165272 }, { url = "https://files.pythonhosted.org/packages/11/2c/c8da2fd086e2118c70094648e9f62014ac676b28821069ef011e09f3d89a/picologging-0.9.3-cp39-cp39-win32.whl", hash = "sha256:35e11311c71678813112e03649fa8dacad2f4ee5c3a424e515876df96c66b89b", size = 78146 }, { url = "https://files.pythonhosted.org/packages/e5/ac/0a0d965301cb78c60b25ebf963e07773d74e7cc9be9edbc9c6ce902ccf80/picologging-0.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:e1ab2768f4c178df653bd6cc94ca31d9b94c6d4a90012598a185e80ea66f1a8f", size = 86760 }, ] [[package]] name = "platformdirs" version = "4.3.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] [[package]] name = "polyfactory" version = "2.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "faker" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4f/0c/12b4e50ab0d165f34ae65fbf26bd93debc8d6c4e00ea62a0b086c9eb58d0/polyfactory-2.18.1.tar.gz", hash = "sha256:17c9db18afe4fb8d7dd8e5ba296e69da0fcf7d0f3b63d1840eb10d135aed5aad", size = 185001 } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/80/e0bfd57b64009f476112fa81056eb64d9c95bbbbf5bb3257ad010f89907a/polyfactory-2.18.1-py3-none-any.whl", hash = "sha256:1a2b0715e08bfe9f14abc838fc013ab8772cb90e66f2e601e15e1127f0bc1b18", size = 59335 }, ] [[package]] name = "pre-commit" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/b3/4ae08d21eb097162f5aad37f4585f8069a86402ed7f5362cc9ae097f9572/pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32", size = 177079 } wheels = [ { url = "https://files.pythonhosted.org/packages/6c/75/526915fedf462e05eeb1c75ceaf7e3f9cde7b5ce6f62740fe5f7f19a0050/pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660", size = 203698 }, ] [[package]] name = "priority" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f5/3c/eb7c35f4dcede96fca1842dac5f4f5d15511aa4b52f3a961219e68ae9204/priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0", size = 24792 } wheels = [ { url = "https://files.pythonhosted.org/packages/5e/5f/82c8074f7e84978129347c2c6ec8b6c59f3584ff1a20bc3c940a3e061790/priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa", size = 8946 }, ] [[package]] name = "prometheus-client" version = "0.21.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e1/54/a369868ed7a7f1ea5163030f4fc07d85d22d7a1d270560dab675188fb612/prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e", size = 78634 } wheels = [ { url = "https://files.pythonhosted.org/packages/84/2d/46ed6436849c2c88228c3111865f44311cff784b4aabcdef4ea2545dbc3d/prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166", size = 54686 }, ] [[package]] name = "psutil" version = "6.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/26/10/2a30b13c61e7cf937f4adf90710776b7918ed0a9c434e2c38224732af310/psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a", size = 508565 } wheels = [ { url = "https://files.pythonhosted.org/packages/01/9e/8be43078a171381953cfee33c07c0d628594b5dbfc5157847b85022c2c1b/psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688", size = 247762 }, { url = "https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e", size = 248777 }, { url = "https://files.pythonhosted.org/packages/65/8e/bcbe2025c587b5d703369b6a75b65d41d1367553da6e3f788aff91eaf5bd/psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38", size = 284259 }, { url = "https://files.pythonhosted.org/packages/58/4d/8245e6f76a93c98aab285a43ea71ff1b171bcd90c9d238bf81f7021fb233/psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b", size = 287255 }, { url = "https://files.pythonhosted.org/packages/27/c2/d034856ac47e3b3cdfa9720d0e113902e615f4190d5d1bdb8df4b2015fb2/psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a", size = 288804 }, { url = "https://files.pythonhosted.org/packages/ea/55/5389ed243c878725feffc0d6a3bc5ef6764312b6fc7c081faaa2cfa7ef37/psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e", size = 250386 }, { url = "https://files.pythonhosted.org/packages/11/91/87fa6f060e649b1e1a7b19a4f5869709fbf750b7c8c262ee776ec32f3028/psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be", size = 254228 }, ] [[package]] name = "psycopg" version = "3.1.20" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-zoneinfo", marker = "python_full_version < '3.9'" }, { name = "typing-extensions" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/6d/0939210f3ba089b360cf0d3741494719152567bc81303cca2c0f1e67c78a/psycopg-3.1.20.tar.gz", hash = "sha256:32f5862ab79f238496236f97fe374a7ab55b4b4bb839a74802026544735f9a07", size = 147567 } wheels = [ { url = "https://files.pythonhosted.org/packages/2d/e9/126bbfd5dded758bb109526c5f5f2c2538fe293b15b6fa208db7078c72c4/psycopg-3.1.20-py3-none-any.whl", hash = "sha256:898a29f49ac9c903d554f5a6cdc44a8fc564325557c18f82e51f39c1f4fc2aeb", size = 179473 }, ] [package.optional-dependencies] binary = [ { name = "psycopg-binary", marker = "python_full_version < '3.13' and implementation_name != 'pypy'" }, ] c = [ { name = "psycopg-c", marker = "python_full_version >= '3.13' and implementation_name != 'pypy' and sys_platform == 'linux'" }, ] pool = [ { name = "psycopg-pool" }, ] [[package]] name = "psycopg-binary" version = "3.1.20" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/49/cc/18faf6fe8fceaa67222f273657e6cd6183d4e43f387f7741da00b98b8111/psycopg_binary-3.1.20-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:8dadeddb9d2dced49f2371f222db1d78b0a1c0f515c6e9c9e65c8f958c288ce1", size = 3344434 }, { url = "https://files.pythonhosted.org/packages/a6/51/f952b53f267047da31e9d5724d98eb179574398d13b044668f468bca0f6f/psycopg_binary-3.1.20-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:67f285eaf706712d1ac46f4a7fc27226ee6184f411e45aff4044284ac34fe3a3", size = 3474729 }, { url = "https://files.pythonhosted.org/packages/96/50/f86d517dc07b100b6d11535cbf01dd0cf12b6b2caabb8996644d8e21a75c/psycopg_binary-3.1.20-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da1b831f0a33e69bf79c4f39587167720c58c046d46ad86232f12c3e17e7c865", size = 4436412 }, { url = "https://files.pythonhosted.org/packages/dd/e2/95a68198caa3b12ab3b7cde8c769fedf8d7952b9117c0affbd82c41c9cee/psycopg_binary-3.1.20-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c067284df02ea7bcede5f89cc1ed76511ceaf7e560e0f79528125f1a3ef38832", size = 4235923 }, { url = "https://files.pythonhosted.org/packages/ce/cd/163f69306eb00341fdeac2ecd8ea8cbc9193a3548517f53f9222f5ecbf39/psycopg_binary-3.1.20-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8dbff9808ba07fba4afa0a6c823ab411f1cf9f1e27ea684bd307ed268f61a39", size = 4482436 }, { url = "https://files.pythonhosted.org/packages/56/8d/f1991468805a711f72011b2b7df3c6d165671a3dd53a87a45d646cdf36cc/psycopg_binary-3.1.20-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808828fc485f23082f974811cad8aa75120a6dde248453c4fba60e8780bf1841", size = 4179768 }, { url = "https://files.pythonhosted.org/packages/ac/91/ad39a6b31640960f23cb4dd8d81dc32c4c246637a5288a018bd46da838ff/psycopg_binary-3.1.20-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eb8479dd184b2e6bbf8aae52ca946efff0d852b2ead386c26fa6de8c92257a9b", size = 3103511 }, { url = "https://files.pythonhosted.org/packages/3e/6d/b3e34ddfe1adaea0b89fc20b435f61b7bb840aad67f50c0e547d2b64dd0c/psycopg_binary-3.1.20-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78b5932f0f6f97e143272fea16753ecd9a00cb65db2c60ac3710bea6e739e09d", size = 3082174 }, { url = "https://files.pythonhosted.org/packages/66/46/35a2fc272fbec7607546db7d19a2ef6f7d8c8a5ed9f0624e35e4f1618be9/psycopg_binary-3.1.20-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1623073d3f6449223ec4843cf4e36d05258567d93284f9a9b97618a87b2ae4", size = 3184566 }, { url = "https://files.pythonhosted.org/packages/cd/31/5a1b61fa674cc5ae9c1163db537c6097761f55f9bb1224a68519d5b4a4d6/psycopg_binary-3.1.20-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c26863471abba88396281649df34dd29e70f37d695af73ef98a4a9038bbff674", size = 3222041 }, { url = "https://files.pythonhosted.org/packages/00/e5/c5af8a6094acd80ac074ff2cc3fd9eb32909b6c679f41408a5b43b5ecd05/psycopg_binary-3.1.20-cp310-cp310-win_amd64.whl", hash = "sha256:bfc5955e3035f141a567ccc608ba65d01b97f9179ba8061f4b7ce80fe0edb327", size = 2894494 }, { url = "https://files.pythonhosted.org/packages/f8/1c/45e5f240765e80076b08c3ed02c5dfeb5e97d549769b81f8382485d70a15/psycopg_binary-3.1.20-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:802989350fcbc783732bfef660afb34439a62727642a05e8bb9acf7d68993627", size = 3350503 }, { url = "https://files.pythonhosted.org/packages/52/b8/acf96d388692d0bbf2346286f8b175778bc24046aca9181f50d9df9f4714/psycopg_binary-3.1.20-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:01b0e39128715fc37fed6cdc50ab58278eacb75709af503eb607654030975f09", size = 3480091 }, { url = "https://files.pythonhosted.org/packages/41/d4/20604282ff08823d0e90cf092738ea21b339f56a172d8583565b272fc4be/psycopg_binary-3.1.20-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77af1086bedfa0729465565c636de3519079ba523d7b7ee6e8b9486beb1ee905", size = 4434555 }, { url = "https://files.pythonhosted.org/packages/73/e0/3917b766508bb749e08225492d45ba7463b559de1c8a41d3f8f3cf0927cb/psycopg_binary-3.1.20-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9b9562395d441e225f354e8c6303ee6993a93aaeb0dbb5b94368f3249ab2388", size = 4231402 }, { url = "https://files.pythonhosted.org/packages/b4/9b/251435896f7459beda355ef3e3919b6b20d067582cd6838ba248d3cff188/psycopg_binary-3.1.20-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e814d69e5447a93e7b98117ec95a8ce606d3742092fd120960551ed67c376fea", size = 4484218 }, { url = "https://files.pythonhosted.org/packages/a1/12/b2057f9bb8b5f408139266a5b48bfd7578340296d7314d964b9f09e5b18f/psycopg_binary-3.1.20-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf1c2061600235ae9b11d7ad357cab89ac583a76bdb0199f7a29ac947939c20", size = 4176668 }, { url = "https://files.pythonhosted.org/packages/80/9c/a62fe4167427a06e69882d274ba90903507afc89caf6bcc3671790a20875/psycopg_binary-3.1.20-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:50f1d807b4167f973a6f67bca39bf656b737f7426be158a1dc9cb0000d020744", size = 3102502 }, { url = "https://files.pythonhosted.org/packages/98/83/bceca23dd830d4069949e70dec9feb03c114cc551b104f0e2b48b1e598c6/psycopg_binary-3.1.20-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4cf6ec1490232a5b208dae94a8269dc739e6762684c8658a0f3570402db934ae", size = 3080005 }, { url = "https://files.pythonhosted.org/packages/fc/83/bab7c8495e0eb11bf710663afb2849c2d3c91a2bf61b2bd597941f57f80b/psycopg_binary-3.1.20-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:309c09ec50a9c5c8492c2922ee666df1e30a08b08a9b63083d0daa414eccd09c", size = 3182315 }, { url = "https://files.pythonhosted.org/packages/ca/9b/bd4970faed24ae4a850ee8c6ebd621e98fd86e2962e13038603a726e2504/psycopg_binary-3.1.20-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e2c33a01799f93ef8c11a023df66280e39ca3c3249a2581adb2a0e5e80801088", size = 3222552 }, { url = "https://files.pythonhosted.org/packages/5d/0b/7ab0744f282df53968f5066d5fd8bf3f994f90bf2a8003ab40278818d0f2/psycopg_binary-3.1.20-cp311-cp311-win_amd64.whl", hash = "sha256:2c67532057fda72579b02d9d61e9cc8975982844bd5c3c9dc7f84ce8bcac859c", size = 2899115 }, { url = "https://files.pythonhosted.org/packages/94/12/6e909d3a20f7bfa6915c1fdf64ab47bb9ca44b837adb468841aad51bab6c/psycopg_binary-3.1.20-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:ef08de60f1b8503a6f6b6f5bee612de36373c09bc0e3f84409fab09e1ff72107", size = 3326944 }, { url = "https://files.pythonhosted.org/packages/e1/4e/dc425f5c8c102045486f2fa39c3cb379b073557d6bd2cf5d06de81036d7c/psycopg_binary-3.1.20-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a4847fa31c8d3a6dd3536cf1e130dfcc454ed26be471ef274e4358bf7f709cda", size = 3475444 }, { url = "https://files.pythonhosted.org/packages/cd/cd/6484cbdb82dc29bfe43ae8c401a0be309402c304d1aaabcccf1e21908663/psycopg_binary-3.1.20-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b72e9c8c79dcc30e34e996079cfe0374b7c7233d2b5f6f25a0bc8872fe2babef", size = 4412872 }, { url = "https://files.pythonhosted.org/packages/25/d3/d403dc61f9d8b56683a6a1db47ab156807d2e1c442b044fba5763e786893/psycopg_binary-3.1.20-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836246f3c486ef7edfce6cf6cc760173e244826ebecd54c1b63c91d4cc0341f7", size = 4216654 }, { url = "https://files.pythonhosted.org/packages/d3/ff/389198638ad10ec0e80fcc97b5c8092987214d9ac529b1224bf0f7e221da/psycopg_binary-3.1.20-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:015f70b17539ec0ecfb0f87bcaface0c7fa1289b6e7e2313dc7cdfdc513e3235", size = 4451310 }, { url = "https://files.pythonhosted.org/packages/84/94/9ae70af00caf9ce98f857a883ff64c5d236dfea5b7b4b8528d28e80515aa/psycopg_binary-3.1.20-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f52498dc7b41fee74e971823ede4519e3a9597d416f7a2044dbe4b98cc61ff35", size = 4153667 }, { url = "https://files.pythonhosted.org/packages/b8/57/b8a34174803683ef0f3f2fe18304f7048d31bab431f21cf511598b894ed7/psycopg_binary-3.1.20-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:92b61bae0ac881580faa1c89bf2167db7041cb01cc0bd686244f9c20a010036a", size = 3081906 }, { url = "https://files.pythonhosted.org/packages/bf/e7/5df8c4794f13004787cd7ddfe456eec90f49d1b99f1a10947f7ba2a67487/psycopg_binary-3.1.20-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3532b8677666aadb64a4e31f6e97fe4ab71b862ab100d337faf497198339fd4d", size = 3061376 }, { url = "https://files.pythonhosted.org/packages/8e/c6/ec4abb814f54af4b659896ce10386be0c538dad8111b3daeaf672b4daa03/psycopg_binary-3.1.20-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f7df27f50a7db84c28e58be3df41f39618161096c3379ad68bc665a454c53e93", size = 3150174 }, { url = "https://files.pythonhosted.org/packages/0c/50/7b4382e5f5d256ac720ee0bd6470c7aa7d28f78570bd44d5e0b1c29eeb96/psycopg_binary-3.1.20-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12b33c511f0be79d5a68231a10972ef9c68d954d30d176679472057ecc22891a", size = 3198871 }, { url = "https://files.pythonhosted.org/packages/76/2f/eda1b86c01d2803ac05714b94283af1e5012437dcc63dfe0679cc4d445ad/psycopg_binary-3.1.20-cp312-cp312-win_amd64.whl", hash = "sha256:6f3c0b05fc3cbd4d99aaacf5c7afa13b086df5777b9fefb78d31bf81fc70bd04", size = 2884414 }, { url = "https://files.pythonhosted.org/packages/d3/67/919392d6bd182a33f92d8db02d459d4a3153a30bf137d8d7900c035064e9/psycopg_binary-3.1.20-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:f1c78e40ba9a808b6f870f94efc3cfbf479169bf6c4f46c2b1e258a4b035b2ba", size = 3345000 }, { url = "https://files.pythonhosted.org/packages/1e/b2/59ad867ebef0fa93fc89e5d243689f494f4309ee1f64736cd5cb232c7ea3/psycopg_binary-3.1.20-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44306e4b1acef590dc063d63317dc0ac34fce89756723efd22bd770c1a04850c", size = 4437360 }, { url = "https://files.pythonhosted.org/packages/b4/d7/535edefbe0838aaba77ddd13ddb3c21e80c23e4721c33b4aa0e7bae8af39/psycopg_binary-3.1.20-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8c72fe67722ab78c7f4466c79539247306cde260367a4ac42e6302c26a7d6d2", size = 4237652 }, { url = "https://files.pythonhosted.org/packages/11/c1/851ecbc3d261bb142c6775fd36aed8588f35baaf192ce1b89c07bc4fa702/psycopg_binary-3.1.20-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e1f9fa0a6404c7e405bed4f3237e4b4e9292d711deff0d870dcf66f87f0aad7", size = 4487045 }, { url = "https://files.pythonhosted.org/packages/06/c7/8c8c89b1d7d37d47e31016316e0eb0ea3ec7ba084fb87bbd8ea648ae99e3/psycopg_binary-3.1.20-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1851c4ed763969a613246024d37153357308eeb78889dcd6d739b7240dacd4e", size = 4181015 }, { url = "https://files.pythonhosted.org/packages/6f/d9/7259f5dc2b9e53c00914342f6b3a772d5460fd22a9f6f9ce2e61e0b2bc8f/psycopg_binary-3.1.20-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4fc1a6bc9cf8c23d87f3c3f79517b0ee15789f183ef84d077d68c5e1fad4677a", size = 3106010 }, { url = "https://files.pythonhosted.org/packages/8a/16/3125dc69e4affe1aeee8be551f983c9b4935151d36d442415dc7acc2fb2e/psycopg_binary-3.1.20-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:b0d3ee6ae9760545cc78d03eaa858898cc40a59ca4cc2047f198cac2d1a000cc", size = 3083839 }, { url = "https://files.pythonhosted.org/packages/12/48/344c13597330b31d25423906cbaf85147938e1dfb78f98de14cd912b8b18/psycopg_binary-3.1.20-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8285827339f6221861c05c3a292e2464e114c1d0f93ef03c5756c16f3a755520", size = 3189184 }, { url = "https://files.pythonhosted.org/packages/3b/17/c96487d0d70a7bf9aa972d9d68b1fafae6f478c21a907e04ea2f1ccf1d49/psycopg_binary-3.1.20-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e1656794434574d01955f2ceaaef88b4f5edd2099205c383680fa7d8ec18496b", size = 3227429 }, { url = "https://files.pythonhosted.org/packages/7d/a2/ef22ee10fcebb0eac95917d795fd8f42c1903be172d79c41eaee6234e4a4/psycopg_binary-3.1.20-cp38-cp38-win_amd64.whl", hash = "sha256:0f5313ccad37d3f3d87fc8615feeb85b6f99975a338d135d641f2d0921a393dc", size = 2902065 }, { url = "https://files.pythonhosted.org/packages/8a/72/fdb0a4eef4c17b2918eb43994944a9a2c5c849195021a7323372ec96d394/psycopg_binary-3.1.20-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:6902c01cf483dd60565833b04ea6a2ef4151cf9fdb88d461f914b49379470675", size = 3345720 }, { url = "https://files.pythonhosted.org/packages/16/d2/1f95b18afaad6a4bc153c16d0a71cd73ffcc7190a53f82b8cd50bfffdbd2/psycopg_binary-3.1.20-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:381e096a0c7f8bcb00ca5121f335d9a2298c3a12d4d6043a4b07d9efa1816606", size = 4437674 }, { url = "https://files.pythonhosted.org/packages/4e/6e/aa8c620e2235a0172d783abfd15c0c9b724a28d26e679aef3532a2a02d43/psycopg_binary-3.1.20-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ecd8cb66716ff5be7b1ca9c318c4a843807819a245f7c87e0aadd0d0283bc36", size = 4238118 }, { url = "https://files.pythonhosted.org/packages/07/84/bc4b3fe82779c121549650e1e8ef0831c957b82936a8a9b3b8aa7478049c/psycopg_binary-3.1.20-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7462bddd3ffd9875b6344a10c379bba820b93a7c4ca962d2e5e9673a0cf46cf5", size = 4483525 }, { url = "https://files.pythonhosted.org/packages/40/dc/11977825f32a1ac60ee8fd3c3f9c7323ce39a3b4f4140361718248f56626/psycopg_binary-3.1.20-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43e16e3d76021db831c95d2ade55de0d059b1f17732ba818265c6fcb3b662cb1", size = 4180183 }, { url = "https://files.pythonhosted.org/packages/8c/3b/eed601fb8bf227576cf0bee3c300a9fa1f8964902ccf2b6f1d4e7789239a/psycopg_binary-3.1.20-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:75779e3b9d86576491653e78122a0bdedb791fdf65fe1d5caa5d002560912425", size = 3105224 }, { url = "https://files.pythonhosted.org/packages/50/e8/a60fdfd76dc28b611003ac0224461031c09d259ce2920e70ea8efe599772/psycopg_binary-3.1.20-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4b9487593e511c5a6b7a0165e5bddf57efcc4d40173f2ac52e51659637840094", size = 3083096 }, { url = "https://files.pythonhosted.org/packages/b7/c0/11297017a58b27bc6df1f9c6e74409512c23cc055b9dcdb58e4deefb20c7/psycopg_binary-3.1.20-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b03f6f7e512c6a8e37b10814bcb53dcd2ca0c02512a661b3aefffd7b6009e412", size = 3184296 }, { url = "https://files.pythonhosted.org/packages/42/ff/2f06d75396431a459d0fa8debdb66a7f04070e51b04eae0046e76966ebb0/psycopg_binary-3.1.20-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8a92fc898af4080e3cf562c02e3a8a9cb897dc70976c91e05fd064bff76928ea", size = 3223204 }, { url = "https://files.pythonhosted.org/packages/01/10/01ec61e06f6bb2305ab66f05f0501431ce5edbb61700e1cd5fa73cf05884/psycopg_binary-3.1.20-cp39-cp39-win_amd64.whl", hash = "sha256:47dd369cb4b263d29aed12ee23b37c03e58bfe656843692d109896c258c554b0", size = 2896197 }, ] [[package]] name = "psycopg-c" version = "3.1.20" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5b/49/6960b5e5fc82ab4dbde845bbd430d9f9e049847178b3bbe7f1c49c145667/psycopg_c-3.1.20.tar.gz", hash = "sha256:a8dadb012fce8918b0c35d9e5be3d6ba4495067117ee45fa49644e46be3c43c8", size = 562110 } [[package]] name = "psycopg-pool" version = "3.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/71/01d4e589dc5fd1f21368b7d2df183ed0e5bbc160ce291d745142b229797b/psycopg_pool-3.2.4.tar.gz", hash = "sha256:61774b5bbf23e8d22bedc7504707135aaf744679f8ef9b3fe29942920746a6ed", size = 29749 } wheels = [ { url = "https://files.pythonhosted.org/packages/bb/28/2b56ac94c236ee033c7b291bcaa6a83089d0cc0fe7830c35f6521177c199/psycopg_pool-3.2.4-py3-none-any.whl", hash = "sha256:f6a22cff0f21f06d72fb2f5cb48c618946777c49385358e0c88d062c59cbd224", size = 38240 }, ] [[package]] name = "psycopg2-binary" version = "2.9.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764 } wheels = [ { url = "https://files.pythonhosted.org/packages/7a/81/331257dbf2801cdb82105306042f7a1637cc752f65f2bb688188e0de5f0b/psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", size = 3043397 }, { url = "https://files.pythonhosted.org/packages/e7/9a/7f4f2f031010bbfe6a02b4a15c01e12eb6b9b7b358ab33229f28baadbfc1/psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", size = 3274806 }, { url = "https://files.pythonhosted.org/packages/e5/57/8ddd4b374fa811a0b0a0f49b6abad1cde9cb34df73ea3348cc283fcd70b4/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", size = 2851361 }, { url = "https://files.pythonhosted.org/packages/f9/66/d1e52c20d283f1f3a8e7e5c1e06851d432f123ef57b13043b4f9b21ffa1f/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", size = 3080836 }, { url = "https://files.pythonhosted.org/packages/a0/cb/592d44a9546aba78f8a1249021fe7c59d3afb8a0ba51434d6610cc3462b6/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", size = 3264552 }, { url = "https://files.pythonhosted.org/packages/64/33/c8548560b94b7617f203d7236d6cdf36fe1a5a3645600ada6efd79da946f/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", size = 3019789 }, { url = "https://files.pythonhosted.org/packages/b0/0e/c2da0db5bea88a3be52307f88b75eec72c4de62814cbe9ee600c29c06334/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", size = 2871776 }, { url = "https://files.pythonhosted.org/packages/15/d7/774afa1eadb787ddf41aab52d4c62785563e29949613c958955031408ae6/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", size = 2820959 }, { url = "https://files.pythonhosted.org/packages/5e/ed/440dc3f5991a8c6172a1cde44850ead0e483a375277a1aef7cfcec00af07/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", size = 2919329 }, { url = "https://files.pythonhosted.org/packages/03/be/2cc8f4282898306732d2ae7b7378ae14e8df3c1231b53579efa056aae887/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", size = 2957659 }, { url = "https://files.pythonhosted.org/packages/d0/12/fb8e4f485d98c570e00dad5800e9a2349cfe0f71a767c856857160d343a5/psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", size = 1024605 }, { url = "https://files.pythonhosted.org/packages/22/4f/217cd2471ecf45d82905dd09085e049af8de6cfdc008b6663c3226dc1c98/psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", size = 1163817 }, { url = "https://files.pythonhosted.org/packages/9c/8f/9feb01291d0d7a0a4c6a6bab24094135c2b59c6a81943752f632c75896d6/psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", size = 3043397 }, { url = "https://files.pythonhosted.org/packages/15/30/346e4683532011561cd9c8dfeac6a8153dd96452fee0b12666058ab7893c/psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", size = 3274806 }, { url = "https://files.pythonhosted.org/packages/66/6e/4efebe76f76aee7ec99166b6c023ff8abdc4e183f7b70913d7c047701b79/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", size = 2851370 }, { url = "https://files.pythonhosted.org/packages/7f/fd/ff83313f86b50f7ca089b161b8e0a22bb3c319974096093cd50680433fdb/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", size = 3080780 }, { url = "https://files.pythonhosted.org/packages/e6/c4/bfadd202dcda8333a7ccafdc51c541dbdfce7c2c7cda89fa2374455d795f/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", size = 3264583 }, { url = "https://files.pythonhosted.org/packages/5d/f1/09f45ac25e704ac954862581f9f9ae21303cc5ded3d0b775532b407f0e90/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", size = 3019831 }, { url = "https://files.pythonhosted.org/packages/9e/2e/9beaea078095cc558f215e38f647c7114987d9febfc25cb2beed7c3582a5/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", size = 2871822 }, { url = "https://files.pythonhosted.org/packages/01/9e/ef93c5d93f3dc9fc92786ffab39e323b9aed066ba59fdc34cf85e2722271/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", size = 2820975 }, { url = "https://files.pythonhosted.org/packages/a5/f0/049e9631e3268fe4c5a387f6fc27e267ebe199acf1bc1bc9cbde4bd6916c/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", size = 2919320 }, { url = "https://files.pythonhosted.org/packages/dc/9a/bcb8773b88e45fb5a5ea8339e2104d82c863a3b8558fbb2aadfe66df86b3/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", size = 2957617 }, { url = "https://files.pythonhosted.org/packages/e2/6b/144336a9bf08a67d217b3af3246abb1d027095dab726f0687f01f43e8c03/psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", size = 1024618 }, { url = "https://files.pythonhosted.org/packages/61/69/3b3d7bd583c6d3cbe5100802efa5beacaacc86e37b653fc708bf3d6853b8/psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", size = 1163816 }, { url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771 }, { url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336 }, { url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637 }, { url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097 }, { url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776 }, { url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968 }, { url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334 }, { url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722 }, { url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132 }, { url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312 }, { url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191 }, { url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031 }, { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699 }, { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245 }, { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631 }, { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140 }, { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762 }, { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967 }, { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326 }, { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 }, { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 }, { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 }, { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 }, { url = "https://files.pythonhosted.org/packages/03/a7/7aa45bea9c790da0ec4765902d714ee7c43b73ccff34916261090849b715/psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4", size = 3043405 }, { url = "https://files.pythonhosted.org/packages/0e/ea/e0197035d74cc1065e94f2ebf7cdd9fa4aa00bb06b1850091568345441cd/psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8", size = 2851210 }, { url = "https://files.pythonhosted.org/packages/23/bf/9be0b2dd105299860e6b001ad7519e36208944609c8382d5aa2dfc58294c/psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864", size = 3080972 }, { url = "https://files.pythonhosted.org/packages/04/19/bd5324737573f1278d65a2abb907332b31b42622421232c42909c8802378/psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5", size = 3264718 }, { url = "https://files.pythonhosted.org/packages/23/ac/e39fa755f7c99aed7a2ff5f0550519248aa8f9d39c2b0705dfc3b4f13a27/psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa", size = 3019807 }, { url = "https://files.pythonhosted.org/packages/93/0d/4be488917130cde91431d859fce2b004417bce96a5fbb854d813ab9c2bde/psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92", size = 2871932 }, { url = "https://files.pythonhosted.org/packages/6f/db/45ca7735a461ea2669ee579afa9e23af9d0f9453ca34c357dd648625ed39/psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44", size = 2820988 }, { url = "https://files.pythonhosted.org/packages/90/2b/1123431e34df437768fd0d1fbb2ddde36bf44d8b3288cf1512ff66306bc3/psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863", size = 2919351 }, { url = "https://files.pythonhosted.org/packages/a0/9d/d4ef15458a9b879ea3bdde77c93b16ea49762cc281f44cfd8850bb537050/psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3", size = 2957589 }, { url = "https://files.pythonhosted.org/packages/a2/bc/e77648009b6e61af327c607543f65fdf25bcfb4100f5a6f3bdb62ddac03c/psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", size = 3043437 }, { url = "https://files.pythonhosted.org/packages/e0/e8/5a12211a1f5b959f3e3ccd342eace60c1f26422f53e06d687821dc268780/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", size = 2851340 }, { url = "https://files.pythonhosted.org/packages/47/ed/5932b0458a7fc61237b653df050513c8d18a6f4083cc7f90dcef967f7bce/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", size = 3080905 }, { url = "https://files.pythonhosted.org/packages/71/df/8047d85c3d23864aca4613c3be1ea0fe61dbe4e050a89ac189f9dce4403e/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", size = 3264640 }, { url = "https://files.pythonhosted.org/packages/f3/de/6157e4ef242920e8f2749f7708d5cc8815414bdd4a27a91996e7cd5c80df/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", size = 3019812 }, { url = "https://files.pythonhosted.org/packages/25/f9/0fc49efd2d4d6db3a8d0a3f5749b33a0d3fdd872cad49fbf5bfce1c50027/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", size = 2871933 }, { url = "https://files.pythonhosted.org/packages/57/bc/2ed1bd182219065692ed458d218d311b0b220b20662d25d913bc4e8d3549/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", size = 2820990 }, { url = "https://files.pythonhosted.org/packages/71/2a/43f77a9b8ee0b10e2de784d97ddc099d9fe0d9eec462a006e4d2cc74756d/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", size = 2919352 }, { url = "https://files.pythonhosted.org/packages/57/86/d2943df70469e6afab3b5b8e1367fccc61891f46de436b24ddee6f2c8404/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", size = 2957614 }, { url = "https://files.pythonhosted.org/packages/85/21/195d69371330983aa16139e60ba855d0a18164c9295f3a3696be41bbcd54/psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", size = 1025341 }, { url = "https://files.pythonhosted.org/packages/ad/53/73196ebc19d6fbfc22427b982fbc98698b7b9c361e5e7707e3a3247cf06d/psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", size = 1163958 }, ] [[package]] name = "pyasn1" version = "0.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, ] [[package]] name = "pyasn1-modules" version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1d/67/6afbf0d507f73c32d21084a79946bfcfca5fbc62a72057e9c23797a737c9/pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c", size = 310028 } wheels = [ { url = "https://files.pythonhosted.org/packages/77/89/bc88a6711935ba795a679ea6ebee07e128050d6382eaa35a0a47c8032bdc/pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", size = 181537 }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] [[package]] name = "pydantic" version = "2.10.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/86/a03390cb12cf64e2a8df07c267f3eb8d5035e0f9a04bb20fb79403d2a00e/pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa", size = 785401 } wheels = [ { url = "https://files.pythonhosted.org/packages/d5/74/da832196702d0c56eb86b75bfa346db9238617e29b0b7ee3b8b4eccfe654/pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e", size = 456364 }, ] [package.optional-dependencies] email = [ { name = "email-validator" }, ] [[package]] name = "pydantic-core" version = "2.27.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 } wheels = [ { url = "https://files.pythonhosted.org/packages/6e/ce/60fd96895c09738648c83f3f00f595c807cb6735c70d3306b548cc96dd49/pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", size = 1897984 }, { url = "https://files.pythonhosted.org/packages/fd/b9/84623d6b6be98cc209b06687d9bca5a7b966ffed008d15225dd0d20cce2e/pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b", size = 1807491 }, { url = "https://files.pythonhosted.org/packages/01/72/59a70165eabbc93b1111d42df9ca016a4aa109409db04304829377947028/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", size = 1831953 }, { url = "https://files.pythonhosted.org/packages/7c/0c/24841136476adafd26f94b45bb718a78cb0500bd7b4f8d667b67c29d7b0d/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", size = 1856071 }, { url = "https://files.pythonhosted.org/packages/53/5e/c32957a09cceb2af10d7642df45d1e3dbd8596061f700eac93b801de53c0/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", size = 2038439 }, { url = "https://files.pythonhosted.org/packages/e4/8f/979ab3eccd118b638cd6d8f980fea8794f45018255a36044dea40fe579d4/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", size = 2787416 }, { url = "https://files.pythonhosted.org/packages/02/1d/00f2e4626565b3b6d3690dab4d4fe1a26edd6a20e53749eb21ca892ef2df/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", size = 2134548 }, { url = "https://files.pythonhosted.org/packages/9d/46/3112621204128b90898adc2e721a3cd6cf5626504178d6f32c33b5a43b79/pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", size = 1989882 }, { url = "https://files.pythonhosted.org/packages/49/ec/557dd4ff5287ffffdf16a31d08d723de6762bb1b691879dc4423392309bc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", size = 1995829 }, { url = "https://files.pythonhosted.org/packages/6e/b2/610dbeb74d8d43921a7234555e4c091cb050a2bdb8cfea86d07791ce01c5/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", size = 2091257 }, { url = "https://files.pythonhosted.org/packages/8c/7f/4bf8e9d26a9118521c80b229291fa9558a07cdd9a968ec2d5c1026f14fbc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", size = 2143894 }, { url = "https://files.pythonhosted.org/packages/1f/1c/875ac7139c958f4390f23656fe696d1acc8edf45fb81e4831960f12cd6e4/pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", size = 1816081 }, { url = "https://files.pythonhosted.org/packages/d7/41/55a117acaeda25ceae51030b518032934f251b1dac3704a53781383e3491/pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", size = 1981109 }, { url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553 }, { url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220 }, { url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727 }, { url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282 }, { url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437 }, { url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899 }, { url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022 }, { url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969 }, { url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625 }, { url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089 }, { url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496 }, { url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 }, { url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 }, { url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 }, { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 }, { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 }, { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 }, { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 }, { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 }, { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 }, { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 }, { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 }, { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 }, { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 }, { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 }, { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 }, { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 }, { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 }, { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 }, { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 }, { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 }, { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 }, { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 }, { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 }, { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 }, { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 }, { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 }, { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 }, { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 }, { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, { url = "https://files.pythonhosted.org/packages/97/bb/c62074a65a32ed279bef44862e89fabb5ab1a81df8a9d383bddb4f49a1e0/pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62", size = 1901535 }, { url = "https://files.pythonhosted.org/packages/9b/59/e224c93f95ffd4f5d37f1d148c569eda8ae23446ab8daf3a211ac0533e08/pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab", size = 1781287 }, { url = "https://files.pythonhosted.org/packages/11/e2/33629134e577543b9335c5ca9bbfd2348f5023fda956737777a7a3b86788/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864", size = 1834575 }, { url = "https://files.pythonhosted.org/packages/fe/16/82e0849b3c6deb0330c07f1a8d55708d003ec8b1fd38ac84c7a830e25252/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067", size = 1857948 }, { url = "https://files.pythonhosted.org/packages/6b/4e/cdee588a7440bc58b6351e8b8dc2432e38b1144b5ae6625bfbdfb7fa76d9/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd", size = 2041138 }, { url = "https://files.pythonhosted.org/packages/1d/0e/73e0d1dff37a29c31e5b3e8587d228ced736cc7af9f81f6d7d06aa47576c/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5", size = 2783820 }, { url = "https://files.pythonhosted.org/packages/9a/b1/f164d05be347b99b91327ea9dd1118562951d2c86e1ea943ef73636b0810/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78", size = 2138035 }, { url = "https://files.pythonhosted.org/packages/72/44/cf1f20d3036d7e1545eafde0af4f3172075573a407a3a20313115c8990ff/pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f", size = 1991778 }, { url = "https://files.pythonhosted.org/packages/5d/4c/486d8ddd595892e7d791f26dfd3e51bd8abea478eb7747fe2bbe890a2177/pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36", size = 1996644 }, { url = "https://files.pythonhosted.org/packages/33/2a/9a1cd4c8aca242816be431583a3250797f2932fad32d35ad5aefcea179bc/pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a", size = 2091778 }, { url = "https://files.pythonhosted.org/packages/8f/61/03576dac806c49e76a714c23f501420b0aeee80f97b995fc4b28fe63a010/pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b", size = 2146020 }, { url = "https://files.pythonhosted.org/packages/72/82/e236d762052d24949aabad3952bc2c8635a470d6f3cbdd69498692afa679/pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618", size = 1819443 }, { url = "https://files.pythonhosted.org/packages/6e/89/26816cad528ca5d4af9be33aa91507504c4576100e53b371b5bc6d3c797b/pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4", size = 1979478 }, { url = "https://files.pythonhosted.org/packages/bc/6a/d741ce0c7da75ce9b394636a406aace00ad992ae417935ef2ad2e67fb970/pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967", size = 1898376 }, { url = "https://files.pythonhosted.org/packages/bd/68/6ba18e30f10c7051bc55f1dffeadbee51454b381c91846104892a6d3b9cd/pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60", size = 1777246 }, { url = "https://files.pythonhosted.org/packages/36/b8/6f1b7c5f068c00dfe179b8762bc1d32c75c0e9f62c9372174b1b64a74aa8/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854", size = 1832148 }, { url = "https://files.pythonhosted.org/packages/d9/83/83ff64d599847f080a93df119e856e3bd93063cced04b9a27eb66d863831/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9", size = 1856371 }, { url = "https://files.pythonhosted.org/packages/72/e9/974e6c73f59627c446833ecc306cadd199edab40abcfa093372a5a5c0156/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd", size = 2038686 }, { url = "https://files.pythonhosted.org/packages/5e/bb/5e912d02dcf29aebb2da35e5a1a26088c39ffc0b1ea81242ee9db6f1f730/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be", size = 2785725 }, { url = "https://files.pythonhosted.org/packages/85/d7/936846087424c882d89c853711687230cd60179a67c79c34c99b64f92625/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e", size = 2135177 }, { url = "https://files.pythonhosted.org/packages/82/72/5a386e5ce8d3e933c3f283e61357474181c39383f38afffc15a6152fa1c5/pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792", size = 1989877 }, { url = "https://files.pythonhosted.org/packages/ce/5c/b1c417a5fd67ce132d78d16a6ba7629dc7f188dbd4f7c30ef58111ee5147/pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01", size = 1996006 }, { url = "https://files.pythonhosted.org/packages/dd/04/4e18f2c42b29929882f30e4c09a3a039555158995a4ac730a73585198a66/pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9", size = 2091441 }, { url = "https://files.pythonhosted.org/packages/06/84/5a332345b7efb5ab361f916eaf7316ef010e72417e8c7dd3d34462ee9840/pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131", size = 2144471 }, { url = "https://files.pythonhosted.org/packages/54/58/23caa58c35d36627156789c0fb562264c12cfdb451c75eb275535188a96f/pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3", size = 1816563 }, { url = "https://files.pythonhosted.org/packages/f7/9c/e83f08adc8e222b43c7f11d98b27eba08f21bcb259bcbf74743ce903c49c/pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c", size = 1983137 }, { url = "https://files.pythonhosted.org/packages/7c/60/e5eb2d462595ba1f622edbe7b1d19531e510c05c405f0b87c80c1e89d5b1/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", size = 1894016 }, { url = "https://files.pythonhosted.org/packages/61/20/da7059855225038c1c4326a840908cc7ca72c7198cb6addb8b92ec81c1d6/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", size = 1771648 }, { url = "https://files.pythonhosted.org/packages/8f/fc/5485cf0b0bb38da31d1d292160a4d123b5977841ddc1122c671a30b76cfd/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", size = 1826929 }, { url = "https://files.pythonhosted.org/packages/a1/ff/fb1284a210e13a5f34c639efc54d51da136074ffbe25ec0c279cf9fbb1c4/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", size = 1980591 }, { url = "https://files.pythonhosted.org/packages/f1/14/77c1887a182d05af74f6aeac7b740da3a74155d3093ccc7ee10b900cc6b5/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", size = 1981326 }, { url = "https://files.pythonhosted.org/packages/06/aa/6f1b2747f811a9c66b5ef39d7f02fbb200479784c75e98290d70004b1253/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", size = 1989205 }, { url = "https://files.pythonhosted.org/packages/7a/d2/8ce2b074d6835f3c88d85f6d8a399790043e9fdb3d0e43455e72d19df8cc/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", size = 2079616 }, { url = "https://files.pythonhosted.org/packages/65/71/af01033d4e58484c3db1e5d13e751ba5e3d6b87cc3368533df4c50932c8b/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", size = 2133265 }, { url = "https://files.pythonhosted.org/packages/33/72/f881b5e18fbb67cf2fb4ab253660de3c6899dbb2dba409d0b757e3559e3d/pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", size = 2001864 }, { url = "https://files.pythonhosted.org/packages/85/3e/f6f75ba36678fee11dd07a7729e9ed172ecf31e3f50a5d636e9605eee2af/pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f", size = 1894250 }, { url = "https://files.pythonhosted.org/packages/d3/2d/a40578918e2eb5b4ee0d206a4fb6c4040c2bf14e28d29fba9bd7e7659d16/pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31", size = 1772035 }, { url = "https://files.pythonhosted.org/packages/7f/ee/0377e9f4ca5a47e8885f670a65c0a647ddf9ce98d50bf7547cf8e1ee5771/pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3", size = 1827025 }, { url = "https://files.pythonhosted.org/packages/fe/0b/a24d9ef762d05bebdfafd6d5d176b990728fa9ec8ea7b6040d6fb5f3caaa/pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154", size = 1980927 }, { url = "https://files.pythonhosted.org/packages/00/bd/deadc1722eb7dfdf787a3bbcd32eabbdcc36931fd48671a850e1b9f2cd77/pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd", size = 1980918 }, { url = "https://files.pythonhosted.org/packages/f0/05/5d09d0b0e92053d538927308ea1d35cb25ab543d9c3e2eb2d7653bc73690/pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a", size = 1989990 }, { url = "https://files.pythonhosted.org/packages/5b/7e/f7191346d1c3ac66049f618ee331359f8552a8b68a2daf916003c30b6dc8/pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97", size = 2079871 }, { url = "https://files.pythonhosted.org/packages/f3/65/2caf4f7ad65413a137d43cb9578c54d1abd3224be786ad840263c1bf9e0f/pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2", size = 2133569 }, { url = "https://files.pythonhosted.org/packages/fd/ab/718d9a1c41bb8d3e0e04d15b68b8afc135f8fcf552705b62f226225065c7/pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840", size = 2002035 }, ] [[package]] name = "pydantic-extra-types" version = "2.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f4/92/8542f406466d11bf348b795d498906034f9bb9016f09e906ff7fee6444be/pydantic_extra_types-2.10.0.tar.gz", hash = "sha256:552c47dd18fe1d00cfed75d9981162a2f3203cf7e77e55a3d3e70936f59587b9", size = 44559 } wheels = [ { url = "https://files.pythonhosted.org/packages/38/41/0b0cc8b59c31a04bdfde2ae71fccbb13c11fadafc8bd41a2af3e76db7e44/pydantic_extra_types-2.10.0-py3-none-any.whl", hash = "sha256:b19943914e6286548254f5079d1da094e9c0583ee91a8e611e9df24bfd07dbcd", size = 34185 }, ] [[package]] name = "pydata-sphinx-theme" version = "0.14.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accessible-pygments" }, { name = "babel" }, { name = "beautifulsoup4" }, { name = "docutils" }, { name = "packaging" }, { name = "pygments" }, { name = "sphinx" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/47/1bc31c4bc8b395cd37d8ceaf720abe10cf64c857fb9ce55856a6dd958484/pydata_sphinx_theme-0.14.4.tar.gz", hash = "sha256:f5d7a2cb7a98e35b9b49d3b02cec373ad28958c2ed5c9b1ffe6aff6c56e9de5b", size = 2410500 } wheels = [ { url = "https://files.pythonhosted.org/packages/1b/bf/3f8dc653e3015fa0656587e101013754d9bf926f395cbe0892f7e87158dd/pydata_sphinx_theme-0.14.4-py3-none-any.whl", hash = "sha256:ac15201f4c2e2e7042b0cad8b30251433c1f92be762ddcefdb4ae68811d918d9", size = 4682140 }, ] [[package]] name = "pygments" version = "2.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, ] [[package]] name = "pyjwt" version = "2.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fb/68/ce067f09fca4abeca8771fe667d89cc347d1e99da3e093112ac329c6020e/pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c", size = 78825 } wheels = [ { url = "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", size = 22344 }, ] [[package]] name = "pymongo" version = "4.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fb/43/d5e8993bd43e6f9cbe985e8ae1398eb73309e88694ac2ea618eacbc9cea2/pymongo-4.9.2.tar.gz", hash = "sha256:3e63535946f5df7848307b9031aa921f82bb0cbe45f9b0c3296f2173f9283eb0", size = 1889366 } wheels = [ { url = "https://files.pythonhosted.org/packages/38/af/1ce26b971e520de621239842f2be302749eb752a5cb29dd253f4c210eb0a/pymongo-4.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab8d54529feb6e29035ba8f0570c99ad36424bc26486c238ad7ce28597bc43c8", size = 833709 }, { url = "https://files.pythonhosted.org/packages/a6/bd/7bc8224ae96fd9ffe8b2a193469200b9c75787178c5b1955bd20e5d024c7/pymongo-4.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f928bdc152a995cbd0b563fab201b2df873846d11f7a41d1f8cc8a01b35591ab", size = 833974 }, { url = "https://files.pythonhosted.org/packages/87/2e/3cc96aec7a1d6151677bb108af606ea220205a47255ed53255bfe1d8f31f/pymongo-4.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6e7251d59fa3dcbb1399a71a3aec63768cebc6b22180b671601c2195fe1f90a", size = 1405440 }, { url = "https://files.pythonhosted.org/packages/e8/9c/2d5db2fcabc873daead275729c17ddeb2b437010858fe101e8d59a276209/pymongo-4.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e759ed0459e7264a11b6896016f616341a8e4c6ab7f71ae651bd21ffc7e9524", size = 1454720 }, { url = "https://files.pythonhosted.org/packages/6f/84/b382e7f817fd39dcd02ae69e21afd538251acf5de1904606a9908d8895fe/pymongo-4.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f3fc60f242191840ccf02b898bc615b5141fbb70064f38f7e60fcaa35d3b5efd", size = 1431625 }, { url = "https://files.pythonhosted.org/packages/87/f5/653f9af6a7625353138bded4548a5a48729352b963fc2a059e07241b37c2/pymongo-4.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c798351666ac97a0ddaa823689061c3af949c2d6acf7fb2d9ab0a7f465ced79", size = 1409027 }, { url = "https://files.pythonhosted.org/packages/36/26/f4159209cf6229ce0a5ac37f093dab49495c51daad8ca835279f0058b060/pymongo-4.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aac78b5fdd49ed8cae49adf76befacb02293a23b412676775c4715148e166d85", size = 1378524 }, { url = "https://files.pythonhosted.org/packages/57/3c/78c60e721a975b836922467410dd4b9616ac84f096eec00f7bde9e889b2b/pymongo-4.9.2-cp310-cp310-win32.whl", hash = "sha256:bf77bf175c315e299a91332c2bbebc097c4d4fcc8713e513a9861684aa39023a", size = 810564 }, { url = "https://files.pythonhosted.org/packages/71/cf/790c8da7fdd55e5e824b08eaf63355732bbf278ebcb98615e723feb05702/pymongo-4.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:c42b5aad8971256365bfd0a545fb1c7a199c93db80decd298ea2f987419e2a6d", size = 825019 }, { url = "https://files.pythonhosted.org/packages/a8/b4/7af80304a0798526fac959e3de651b0747472c049c8b89a6c15fed2026f6/pymongo-4.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:99e40f44877b32bf4b3c46ceed2228f08c222cf7dec8a4366dd192a1429143fa", size = 887499 }, { url = "https://files.pythonhosted.org/packages/33/ee/5389229774f842bd92a123fd3ea4f2d72b474bde9315ff00e889fe104a0d/pymongo-4.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f6834d575ed87edc7dfcab4501d961b6a423b3839edd29ecb1382eee7736777", size = 887755 }, { url = "https://files.pythonhosted.org/packages/d4/fd/3f0ae0fd3a7049ec67ab8f952020bc9fad841791d52d8c51405bd91b3c9b/pymongo-4.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3010018f5672e5b7e8d096dea9f1ea6545b05345ff0eb1754f6ee63785550773", size = 1647336 }, { url = "https://files.pythonhosted.org/packages/00/b7/0472d51778e9e22b2ffd5ae9a401888525c4872cb2073f1bff8d5ae9659b/pymongo-4.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69394ee9f0ce38ff71266bad01b7e045cd75e58500ebad5d72187cbabf2e652a", size = 1713193 }, { url = "https://files.pythonhosted.org/packages/8c/ac/aa41cb291107bb16bae286d7b9f2c868e393765830bc173609ae4dc9a3ae/pymongo-4.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87b18094100f21615d9db99c255dcd9e93e476f10fb03c1d3632cf4b82d201d2", size = 1681720 }, { url = "https://files.pythonhosted.org/packages/dc/70/ac12eb58bd46a7254daaa4d39e7c4109983ee2227dac44df6587954fe345/pymongo-4.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3039e093d28376d6a54bdaa963ca12230c8a53d7b19c8e6368e19bcfbd004176", size = 1652109 }, { url = "https://files.pythonhosted.org/packages/d3/20/38f71e0f1c7878b287305b2965cebe327fc5626ecca83ea52a272968cbe2/pymongo-4.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ab42d9ee93fe6b90020c42cba5bfb43a2b4660951225d137835efc21940da48", size = 1611503 }, { url = "https://files.pythonhosted.org/packages/9b/4c/d3b26e1040c9538b9c8aed005ec18af7515c6dd3091aabfbf6c30a3b3b1a/pymongo-4.9.2-cp311-cp311-win32.whl", hash = "sha256:a663ca60e187a248d370c58961e40f5463077d2b43831eb92120ea28a79ecf96", size = 855570 }, { url = "https://files.pythonhosted.org/packages/40/3d/7de1a4cf51bf2b10bb9f43ffa208acad0d64c18994ca8d83f490edef6834/pymongo-4.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:24e7b6887bbfefd05afed26a99a2c69459e2daa351a43a410de0d6c0ee3cce4e", size = 874715 }, { url = "https://files.pythonhosted.org/packages/a1/08/7d95aab0463dc5a2c460a0b4e50a45a743afbe20986f47f87a9a88f43c0c/pymongo-4.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8083bbe8cb10bb33dca4d93f8223dd8d848215250bb73867374650bac5fe69e1", size = 941617 }, { url = "https://files.pythonhosted.org/packages/bb/28/40613d8d97fc33bf2b9187446a6746925623aa04a9a27c9b058e97076f7a/pymongo-4.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b8c636bf557c7166e3799bbf1120806ca39e3f06615b141c88d9c9ceae4d8c", size = 941394 }, { url = "https://files.pythonhosted.org/packages/df/b2/7f1a0d75f538c0dcaa004ea69e28706fa3ca72d848e0a5a7dafd30939fff/pymongo-4.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8aac5dce28454f47576063fbad31ea9789bba67cab86c95788f97aafd810e65b", size = 1907396 }, { url = "https://files.pythonhosted.org/packages/ba/70/9304bae47a361a4b12adb5be714bad41478c0e5bc3d6cf403b328d6398a0/pymongo-4.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1d5e7123af1fddf15b2b53e58f20bf5242884e671bcc3860f5e954fe13aeddd", size = 1986029 }, { url = "https://files.pythonhosted.org/packages/ae/51/ac0378d001995c4a705da64a4a2b8e1732f95de5080b752d69f452930cc7/pymongo-4.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe97c847b56d61e533a7af0334193d6b28375b9189effce93129c7e4733794a9", size = 1949088 }, { url = "https://files.pythonhosted.org/packages/1a/30/e93dc808039dc29fc47acee64f128aa650aacae3e4b57b68e01ff1001cda/pymongo-4.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ad54433a996e2d1985a9cd8fc82538ca8747c95caae2daf453600cc8c317f9", size = 1910516 }, { url = "https://files.pythonhosted.org/packages/2b/34/895b9cad3bd5342d5ab51a853ed3a814840ce281d55c6928968e9f3f49f5/pymongo-4.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98b9cade40f5b13e04492a42ae215c3721099be1014ddfe0fbd23f27e4f62c0c", size = 1860499 }, { url = "https://files.pythonhosted.org/packages/24/7e/167818f324bf2122d45551680671a3c6406a345d3fcace4e737f57bda4e4/pymongo-4.9.2-cp312-cp312-win32.whl", hash = "sha256:dde6068ae7c62ea8ee2c5701f78c6a75618cada7e11f03893687df87709558de", size = 901282 }, { url = "https://files.pythonhosted.org/packages/12/6b/b7ffa7114177fc1c60ae529512b82629ff7e25d19be88e97f2d0ddd16717/pymongo-4.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:e1ab6cd7cd2d38ffc7ccdc79fdc166c7a91a63f844a96e3e6b2079c054391c68", size = 924925 }, { url = "https://files.pythonhosted.org/packages/5b/d6/b57ef5f376e2e171218a98b8c30dfd001aa5cac6338aa7f3ca76e6315667/pymongo-4.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ad79d6a74f439a068caf9a1e2daeabc20bf895263435484bbd49e90fbea7809", size = 995233 }, { url = "https://files.pythonhosted.org/packages/32/80/4ec79e36e99f86a063d297a334883fb5115ad70e9af46142b8dc33f636fa/pymongo-4.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:877699e21703717507cbbea23e75b419f81a513b50b65531e1698df08b2d7094", size = 995025 }, { url = "https://files.pythonhosted.org/packages/c4/fd/8f5464321fdf165700f10aec93b07a75c3537be593291ac2f8c8f5f69bd0/pymongo-4.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc9322ce7cf116458a637ac10517b0c5926a8211202be6dbdc51dab4d4a9afc8", size = 2167429 }, { url = "https://files.pythonhosted.org/packages/da/42/0f749d805d17f5b17f48f2ee1aaf2a74e67939607b87b245e5ec9b4c1452/pymongo-4.9.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cca029f46acf475504eedb33c7839f030c4bc4f946dcba12d9a954cc48850b79", size = 2258834 }, { url = "https://files.pythonhosted.org/packages/b8/52/b0c1b8e9cbeae234dd1108a906f30b680755533b7229f9f645d7e7adad25/pymongo-4.9.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c8c861e77527eec5a4b7363c16030dd0374670b620b08a5300f97594bbf5a40", size = 2216412 }, { url = "https://files.pythonhosted.org/packages/4d/20/53395473a1023bb6a670b68fbfa937664c75b354c2444463075ff43523e2/pymongo-4.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fc70326ae71b3c7b8d6af82f46bb71dafdba3c8f335b29382ae9cf263ef3a5c", size = 2168891 }, { url = "https://files.pythonhosted.org/packages/01/b7/fa4030279d8a4a9c0a969a719b6b89da8a59795b5cdf129ef553fce6d1f2/pymongo-4.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba9d2f6df977fee24437f82f7412460b0628cd6b961c4235c9cff71577a5b61f", size = 2109380 }, { url = "https://files.pythonhosted.org/packages/f3/55/f252972a039fc6bfca748625c5080d6f88801eb61f118fe79cde47342d6a/pymongo-4.9.2-cp313-cp313-win32.whl", hash = "sha256:b3254769e708bc4aa634745c262081d13c841a80038eff3afd15631540a1d227", size = 946962 }, { url = "https://files.pythonhosted.org/packages/7b/36/88d8438699ba09b714dece00a4a7462330c1d316f5eaa28db450572236f6/pymongo-4.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:169b85728cc17800344ba17d736375f400ef47c9fbb4c42910c4b3e7c0247382", size = 975113 }, { url = "https://files.pythonhosted.org/packages/bd/b0/3b07394be7a9282981f3ec6e9918f8528d9dcff7dea523cd86a03cbddc76/pymongo-4.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3f28afd783be3cebef1235a45340589169d7774cd9909ba0249e2f851ff511d", size = 726089 }, { url = "https://files.pythonhosted.org/packages/0a/34/7054e272a48a11a8ae376b1ab3f61370d50b448eae520cb2036da39d490c/pymongo-4.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a0b2e7fedc5911cd44590b5fd8e3714029f378f37f3c0c2043f67150b588d4a", size = 726398 }, { url = "https://files.pythonhosted.org/packages/a3/6f/3b6b28d0202b942d0a1cc6217dbdd36c8a24cad036c58b06449672d31acf/pymongo-4.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5af264b9a973859123e3129d131d7246f57659304400e3e6b35ed6eaf099854d", size = 927238 }, { url = "https://files.pythonhosted.org/packages/b3/52/57294161b7f42553228ef72742231869a635e550d7e7a344055d0a30e254/pymongo-4.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65c6b2e2a6db38f49433021dda0802ad081118224b2264500ef03a2d82ae26a7", size = 943454 }, { url = "https://files.pythonhosted.org/packages/bc/61/900db838a8e993a912d8058c7396a3ece5ccfab3d6c063e3dbf174b94c93/pymongo-4.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:410ea165f2f819118eed764c5faa35fa71aeff5ce8b5046af99ed158a5661e9e", size = 936839 }, { url = "https://files.pythonhosted.org/packages/46/db/bd9d4d8ed19de90c07b53d1405ad8a3f479d1df7f18bfe1e7a37a5933f2f/pymongo-4.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c3c71337d4c923f719cb56253af9244e90353a2454088ee4f184bfb0dd446a4", size = 928431 }, { url = "https://files.pythonhosted.org/packages/32/c6/fe5e207fce0c1e7a8ef342dc5b38b32172e1ed9f2660ef1297687be0b2b0/pymongo-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77528a2b928fe3f1f655cefa195e6718ab1ccd1a456aba486d76318e526a7fac", size = 917943 }, { url = "https://files.pythonhosted.org/packages/76/c0/40b3915f09211693df897ce8fe555974729abb07427109f6d53c5070878b/pymongo-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fdbd558d90b55d7c39c096a79f8a725f1f02b658211924ab98dbc03ecad01095", size = 919094 }, { url = "https://files.pythonhosted.org/packages/3a/a6/7ebd35e409b05a61864c938e8d73ee8fbf46b2facf0578086c07df74ab9a/pymongo-4.9.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e3ff4201ea707f57bf381f61df0e9cd6e896627a59f98a5d1c4a1bd14a2544cb", size = 926651 }, { url = "https://files.pythonhosted.org/packages/92/cf/f727361f21ffa412573e98f8e55585945c7dab91aa3dbddcfa5f85177ec6/pymongo-4.9.2-cp38-cp38-win32.whl", hash = "sha256:ae227bba43e2e6fc8c3440a70b3b8f9ab2b0eb0906d0d2cf814dd9490c572e2a", size = 720644 }, { url = "https://files.pythonhosted.org/packages/d6/43/78a57401e276f29d1468c4623113f92bd01b019de2c8315d7313325e5d37/pymongo-4.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:a92c96886048d3ebae62dbcfc775c7f2b965270160e3cb6aab4e06750e030b05", size = 725630 }, { url = "https://files.pythonhosted.org/packages/e2/0c/8101588ad2da1f023c77597aba612176051f731bf417e557275edc4102b9/pymongo-4.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e54e2c6f1dec45c57a587b4c13c16666d5f7c031a642ae177140d1e0551a947e", size = 779913 }, { url = "https://files.pythonhosted.org/packages/66/93/2d237514aad615b94a617cc7092cdee959d99a883d156c25df2a550b6310/pymongo-4.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a49d9292f22a0395c0fd2822a06e385910f1f902c3a9feafc1d0bfc27cd2df6b", size = 780185 }, { url = "https://files.pythonhosted.org/packages/6d/c1/4300586e96af2652fe3592c1eaa70574989b4a21f2704938a857083cdcda/pymongo-4.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80a1ee9b72eebd96619ebe0beb718a5bcf2a70f464edf315f97b9315ed6854a9", size = 1165465 }, { url = "https://files.pythonhosted.org/packages/04/ea/3f577b203ecad8bd59f5aef0822e7cbfd4c76fe6e05c83a4c87c405c603a/pymongo-4.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea9c47f86a322280381e9ddba7491e664ea80bf75df247ea2346faf7626e4e4c", size = 1198286 }, { url = "https://files.pythonhosted.org/packages/84/af/29248c6eaeb1055d14c8d457624b963bcd9e30be8421dd4fc2ed2e8989fd/pymongo-4.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf963104dfd7235bebc44cef40b4b12c6638bb03b3a828cb495498e286b6edd0", size = 1183784 }, { url = "https://files.pythonhosted.org/packages/56/06/ddff399f79d410efd832e6673c06d94ab7901c9698734bbb512d4d630272/pymongo-4.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f13330bdf4a57ef70bdd6282721547ec464f773203be47bac1efc4abd74a9190", size = 1168068 }, { url = "https://files.pythonhosted.org/packages/cd/f0/b057d94a627f2a6468707a342ea6c788d60c729cf8d4a698333a13b70c8b/pymongo-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fb10d7069f1e7d7d6a458b1c5e9d1454be6eca2d9885bec25c1202e22c88d2a", size = 1147611 }, { url = "https://files.pythonhosted.org/packages/9f/f7/22ef54abf9dd083fe64153c26575bf3aec966ba50ec76b5e00e2a2ee75b3/pymongo-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd832de5df92caa68ee66c872708951d7e0c1f7b289b74189f2ccf1832c56dda", size = 1132542 }, { url = "https://files.pythonhosted.org/packages/d7/59/059fa4d81fb4f8a1cf6f04dd03a9f78119d56fd05fd656761e89b6c8d4cb/pymongo-4.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3f55efe0f77198c055800e605268bfd77a3f0223d1a80b55b771d0c350bc3ade", size = 1165854 }, { url = "https://files.pythonhosted.org/packages/fd/61/67a7ed51f0ce8eedc25c917dc582478caa9606d39e508441742bbcd8f674/pymongo-4.9.2-cp39-cp39-win32.whl", hash = "sha256:f2f43e5d6e739aa78c7053bdf351453c0e53d7667a3cac73255c2169631e052a", size = 765568 }, { url = "https://files.pythonhosted.org/packages/e3/cf/8c7a0b3d4d44ecf62bd0590d4d6a7d4e268b77de7f01f7dd362576f667d1/pymongo-4.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:31c35d3dac5a1b0f65b3da2a19dc7fb88271c86329c75cfea775d5381ade6c06", size = 775323 }, ] [[package]] name = "pyopenssl" version = "24.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c1/d4/1067b82c4fc674d6f6e9e8d26b3dff978da46d351ca3bac171544693e085/pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36", size = 178944 } wheels = [ { url = "https://files.pythonhosted.org/packages/42/22/40f9162e943f86f0fc927ebc648078be87def360d9d8db346619fb97df2b/pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a", size = 56111 }, ] [[package]] name = "pyright" version = "1.1.344" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/9e/e8e54e9dd3f1e63e8573d9a3830be06e904629bc15a90ca052532b2656f4/pyright-1.1.344.tar.gz", hash = "sha256:ab7c962f00dd8141a5a0192c1060fb34b92d1f9047ad70dda45229938051922b", size = 17486 } wheels = [ { url = "https://files.pythonhosted.org/packages/91/37/a118d8cc0381363c1a7f511a9f640c60df13329d35b4ee42c79dd5c87736/pyright-1.1.344-py3-none-any.whl", hash = "sha256:ab7117a911ce25fcd317f42272579f9ae53a6abc8b8a15f6aa069a11281953ee", size = 18225 }, ] [[package]] name = "pytest" version = "8.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } wheels = [ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, ] [[package]] name = "pytest-asyncio" version = "0.24.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9' and sys_platform != 'win32'", "python_full_version < '3.9' and sys_platform == 'win32'", ] dependencies = [ { name = "pytest", marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855 } wheels = [ { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024 }, ] [[package]] name = "pytest-asyncio" version = "0.25.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'win32'", "python_full_version >= '3.9' and python_full_version < '3.11' and sys_platform != 'win32'", "python_full_version >= '3.13' and sys_platform == 'linux'", "python_full_version >= '3.13' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version >= '3.9' and python_full_version < '3.11' and sys_platform == 'win32'", "python_full_version >= '3.13' and sys_platform == 'win32'", ] dependencies = [ { name = "pytest", marker = "python_full_version >= '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/df/adcc0d60f1053d74717d21d58c0048479e9cab51464ce0d2965b086bd0e2/pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f", size = 53950 } wheels = [ { url = "https://files.pythonhosted.org/packages/61/d8/defa05ae50dcd6019a95527200d3b3980043df5aa445d40cb0ef9f7f98ab/pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075", size = 19400 }, ] [[package]] name = "pytest-cov" version = "5.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } wheels = [ { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, ] [[package]] name = "pytest-lazy-fixtures" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/21/15/a33df3d8d0f44bde7382c9201a06fa277d8442d783ab46874b616aae9239/pytest_lazy_fixtures-1.1.1.tar.gz", hash = "sha256:0c561f0d29eea5b55cf29b9264a3241999ffdb74c6b6e8c4ccc0bd2c934d01ed", size = 6978 } wheels = [ { url = "https://files.pythonhosted.org/packages/60/3a/9354c3765bc1459fc18f6d5cf62709b3a606de55bd02d0483ceefd7fd98f/pytest_lazy_fixtures-1.1.1-py3-none-any.whl", hash = "sha256:a4b396a361faf56c6305535fd0175ce82902ca7cf668c4d812a25ed2bcde8183", size = 6928 }, ] [[package]] name = "pytest-mock" version = "3.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } wheels = [ { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, ] [[package]] name = "pytest-rerunfailures" version = "14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/a4/6de45fe850759e94aa9a55cda807c76245af1941047294df26c851dfb4a9/pytest-rerunfailures-14.0.tar.gz", hash = "sha256:4a400bcbcd3c7a4ad151ab8afac123d90eca3abe27f98725dc4d9702887d2e92", size = 21350 } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/e7/e75bd157331aecc190f5f8950d7ea3d2cf56c3c57fb44da70e60b221133f/pytest_rerunfailures-14.0-py3-none-any.whl", hash = "sha256:4197bdd2eaeffdbf50b5ea6e7236f47ff0e44d1def8dae08e409f536d84e7b32", size = 12709 }, ] [[package]] name = "pytest-timeout" version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/0d/04719abc7a4bdb3a7a1f968f24b0f5253d698c9cc94975330e9d3145befb/pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9", size = 17697 } wheels = [ { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, ] [[package]] name = "pytest-xdist" version = "3.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/c4/3c310a19bc1f1e9ef50075582652673ef2bfc8cd62afef9585683821902f/pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d", size = 84060 } wheels = [ { url = "https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7", size = 46108 }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] [[package]] name = "python-dotenv" version = "1.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, ] [[package]] name = "pytz" version = "2024.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } wheels = [ { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, { url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", size = 183218 }, { url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", size = 728067 }, { url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", size = 757812 }, { url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", size = 746531 }, { url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", size = 800820 }, { url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", size = 145514 }, { url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", size = 162702 }, { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, ] [[package]] name = "redis" version = "5.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/17/2f4a87ffa4cd93714cf52edfa3ea94589e9de65f71e9f99cbcfa84347a53/redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0", size = 4607878 } wheels = [ { url = "https://files.pythonhosted.org/packages/12/f5/ffa560ecc4bafbf25f7961c3d6f50d627a90186352e27e7d0ba5b1f6d87d/redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897", size = 261428 }, ] [package.optional-dependencies] hiredis = [ { name = "hiredis" }, ] [[package]] name = "requests" version = "2.32.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] [[package]] name = "rich" version = "13.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } wheels = [ { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, ] [[package]] name = "rich-click" version = "1.8.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/f4/e48dc2850662526a26fb0961aacb0162c6feab934312b109b748ae4efee2/rich_click-1.8.4.tar.gz", hash = "sha256:0f49471f04439269d0e66a6f43120f52d11d594869a2a0be600cfb12eb0616b9", size = 38247 } wheels = [ { url = "https://files.pythonhosted.org/packages/84/f3/72f93d8494ee641bde76bfe1208cf4abc44c6f9448673762f6077bc162d6/rich_click-1.8.4-py3-none-any.whl", hash = "sha256:2d2841b3cebe610d5682baa1194beaf78ab00c4fa31931533261b5eba2ee80b7", size = 35071 }, ] [[package]] name = "ruamel-yaml" version = "0.18.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml-clib", marker = "python_full_version < '3.13' and platform_python_implementation == 'CPython'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/81/4dfc17eb6ebb1aac314a3eb863c1325b907863a1b8b1382cdffcb6ac0ed9/ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b", size = 143362 } wheels = [ { url = "https://files.pythonhosted.org/packages/73/67/8ece580cc363331d9a53055130f86b096bf16e38156e33b1d3014fffda6b/ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636", size = 117761 }, ] [[package]] name = "ruamel-yaml-clib" version = "0.2.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/46/ab/bab9eb1566cd16f060b54055dd39cf6a34bfa0240c53a7218c43e974295b/ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512", size = 213824 } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/01/37ac131614f71b98e9b148b2d7790662dcee92217d2fb4bac1aa377def33/ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d", size = 148236 }, { url = "https://files.pythonhosted.org/packages/61/ee/4874c9fc96010fce85abefdcbe770650c5324288e988d7a48b527a423815/ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462", size = 133996 }, { url = "https://files.pythonhosted.org/packages/d3/62/c60b034d9a008bbd566eeecf53a5a4c73d191c8de261290db6761802b72d/ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412", size = 526680 }, { url = "https://files.pythonhosted.org/packages/90/8c/6cdb44f548b29eb6328b9e7e175696336bc856de2ff82e5776f860f03822/ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f", size = 605853 }, { url = "https://files.pythonhosted.org/packages/88/30/fc45b45d5eaf2ff36cffd215a2f85e9b90ac04e70b97fd4097017abfb567/ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334", size = 655206 }, { url = "https://files.pythonhosted.org/packages/af/dc/133547f90f744a0c827bac5411d84d4e81da640deb3af1459e38c5f3b6a0/ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d", size = 689649 }, { url = "https://files.pythonhosted.org/packages/23/1d/589139191b187a3c750ae8d983c42fd799246d5f0dd84451a0575c9bdbe9/ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d", size = 100044 }, { url = "https://files.pythonhosted.org/packages/4f/5b/744df20285a75ac4c606452ce9a0fcc42087d122f42294518ded1017697c/ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31", size = 117825 }, { url = "https://files.pythonhosted.org/packages/b1/15/971b385c098e8d0d170893f5ba558452bb7b776a0c90658b8f4dd0e3382b/ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069", size = 148870 }, { url = "https://files.pythonhosted.org/packages/01/b0/4ddef56e9f703d7909febc3a421d709a3482cda25826816ec595b73e3847/ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248", size = 134475 }, { url = "https://files.pythonhosted.org/packages/a4/f7/22d6b620ed895a05d40802d8281eff924dc6190f682d933d4efff60db3b5/ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b", size = 544020 }, { url = "https://files.pythonhosted.org/packages/7c/e4/0d19d65e340f93df1c47f323d95fa4b256bb28320290f5fddef90837853a/ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe", size = 642643 }, { url = "https://files.pythonhosted.org/packages/c9/ff/f781eb5e2ae011e586d5426e2086a011cf1e0f59704a6cad1387975c5a62/ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899", size = 695832 }, { url = "https://files.pythonhosted.org/packages/e3/41/f62e67ac651358b8f0d60cfb12ab2daf99b1b69eeaa188d0cec809d943a6/ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9", size = 730923 }, { url = "https://files.pythonhosted.org/packages/9f/f0/19ab8acbf983cd1b37f47d27ceb8b10a738d60d36316a54bad57e0d73fbb/ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7", size = 99999 }, { url = "https://files.pythonhosted.org/packages/ec/54/d8a795997921d87224c65d44499ca595a833093fb215b133f920c1062956/ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb", size = 118008 }, { url = "https://files.pythonhosted.org/packages/7a/a2/eb5e9d088cb9d15c24d956944c09dca0a89108ad6e2e913c099ef36e3f0d/ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1", size = 144636 }, { url = "https://files.pythonhosted.org/packages/66/98/8de4f22bbfd9135deb3422e96d450c4bc0a57d38c25976119307d2efe0aa/ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2", size = 135684 }, { url = "https://files.pythonhosted.org/packages/30/d3/5fe978cd01a61c12efd24d65fa68c6f28f28c8073a06cf11db3a854390ca/ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92", size = 734571 }, { url = "https://files.pythonhosted.org/packages/55/b3/e2531a050758b717c969cbf76c103b75d8a01e11af931b94ba656117fbe9/ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62", size = 643946 }, { url = "https://files.pythonhosted.org/packages/0d/aa/06db7ca0995b513538402e11280282c615b5ae5f09eb820460d35fb69715/ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9", size = 692169 }, { url = "https://files.pythonhosted.org/packages/27/38/4cf4d482b84ecdf51efae6635cc5483a83cf5ca9d9c13e205a750e251696/ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d", size = 740325 }, { url = "https://files.pythonhosted.org/packages/6f/67/c62c6eea53a4feb042727a3d6c18f50dc99683c2b199c06bd2a9e3db8e22/ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa", size = 98639 }, { url = "https://files.pythonhosted.org/packages/10/d2/52a3d810d0b5b3720725c0504a27b3fced7b6f310fe928f7019d79387bc1/ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b", size = 115305 }, { url = "https://files.pythonhosted.org/packages/18/52/8dc27bbd9ef1d4695975b8dc132c27c431d0186037ad3c731a6dd1c154b9/ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615", size = 146177 }, { url = "https://files.pythonhosted.org/packages/08/4c/5770b8f318fe404a455141a7a33a5568c27a1f944724e82354c8f3554db2/ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337", size = 133289 }, { url = "https://files.pythonhosted.org/packages/5a/45/644d839c09c0717c2d7f26b705560ad74b3056085b3bc7f9c2ac2081317b/ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1", size = 641518 }, { url = "https://files.pythonhosted.org/packages/22/fa/b2a8fd49c92693e9b9b6b11eef4c2a8aedaca2b521ab3e020aa4778efc23/ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91", size = 596029 }, { url = "https://files.pythonhosted.org/packages/5c/f0/702e56e12497da7960ed8a6972e5edc50545757c40f1a86a41a5217da7e9/ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28", size = 724558 }, { url = "https://files.pythonhosted.org/packages/87/a6/efb1add3bac06c25aa4c8ff8c6d3e5e91c539f6600832dd63ff98e2b44cc/ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d", size = 767665 }, { url = "https://files.pythonhosted.org/packages/1d/fe/a638c3ad6e74f4b15c8c1aa7de61a0cfe58c629d48ea59cf07dce5eaee1e/ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe", size = 100578 }, { url = "https://files.pythonhosted.org/packages/24/ce/6f587283caaff93d0b9cac2f244fcda686897e83401bb1aa91803db7bf94/ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312", size = 118511 }, { url = "https://files.pythonhosted.org/packages/56/a9/e3be88fcebe04016c57207260f2b07c5ecacab86e9f585d10daaa2a4074f/ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001", size = 148719 }, { url = "https://files.pythonhosted.org/packages/b2/ed/f221e60a4cdc7996aae23643da44b12ef33f457c2a52d590236a6950ac8e/ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf", size = 134394 }, { url = "https://files.pythonhosted.org/packages/57/e4/f572d7e2502854f15291dfa94eebdc687e04db387559f026995c7697af34/ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c", size = 608071 }, { url = "https://files.pythonhosted.org/packages/7c/b2/389b345a60131593028b0263fddaa580edb4081697a3f3aa1f168f67519f/ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5", size = 562085 }, { url = "https://files.pythonhosted.org/packages/8d/c0/fd7196ca7a1c3867e7068ad1c4ff9230291af3f8adab2f9c2c202ecaf9cb/ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b", size = 658185 }, { url = "https://files.pythonhosted.org/packages/54/61/c18d378caadac66fa97da5d28758c751730dac7510b6a8b8b096da3fff9a/ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880", size = 692222 }, { url = "https://files.pythonhosted.org/packages/68/4c/f55fbf8510d087449b21b4cde4c05726c8dda5f9b25a8fad06d0c4319249/ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5", size = 100563 }, { url = "https://files.pythonhosted.org/packages/74/82/e9bb3a3a2268987b7bc472c5c26b420757e04db0d0408e6626d07e388e4c/ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15", size = 118400 }, ] [[package]] name = "ruff" version = "0.8.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/95/d0/8ff5b189d125f4260f2255d143bf2fa413b69c2610c405ace7a0a8ec81ec/ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f", size = 3313222 } wheels = [ { url = "https://files.pythonhosted.org/packages/a2/d6/1a6314e568db88acdbb5121ed53e2c52cebf3720d3437a76f82f923bf171/ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5", size = 10532605 }, { url = "https://files.pythonhosted.org/packages/89/a8/a957a8812e31facffb6a26a30be0b5b4af000a6e30c7d43a22a5232a3398/ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087", size = 10278243 }, { url = "https://files.pythonhosted.org/packages/a8/23/9db40fa19c453fabf94f7a35c61c58f20e8200b4734a20839515a19da790/ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209", size = 9917739 }, { url = "https://files.pythonhosted.org/packages/e2/a0/6ee2d949835d5701d832fc5acd05c0bfdad5e89cfdd074a171411f5ccad5/ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871", size = 10779153 }, { url = "https://files.pythonhosted.org/packages/7a/25/9c11dca9404ef1eb24833f780146236131a3c7941de394bc356912ef1041/ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1", size = 10304387 }, { url = "https://files.pythonhosted.org/packages/c8/b9/84c323780db1b06feae603a707d82dbbd85955c8c917738571c65d7d5aff/ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5", size = 11360351 }, { url = "https://files.pythonhosted.org/packages/6b/e1/9d4bbb2ace7aad14ded20e4674a48cda5b902aed7a1b14e6b028067060c4/ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d", size = 12022879 }, { url = "https://files.pythonhosted.org/packages/75/28/752ff6120c0e7f9981bc4bc275d540c7f36db1379ba9db9142f69c88db21/ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26", size = 11610354 }, { url = "https://files.pythonhosted.org/packages/ba/8c/967b61c2cc8ebd1df877607fbe462bc1e1220b4a30ae3352648aec8c24bd/ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1", size = 12813976 }, { url = "https://files.pythonhosted.org/packages/7f/29/e059f945d6bd2d90213387b8c360187f2fefc989ddcee6bbf3c241329b92/ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c", size = 11154564 }, { url = "https://files.pythonhosted.org/packages/55/47/cbd05e5a62f3fb4c072bc65c1e8fd709924cad1c7ec60a1000d1e4ee8307/ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa", size = 10760604 }, { url = "https://files.pythonhosted.org/packages/bb/ee/4c3981c47147c72647a198a94202633130cfda0fc95cd863a553b6f65c6a/ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540", size = 10391071 }, { url = "https://files.pythonhosted.org/packages/6b/e6/083eb61300214590b188616a8ac6ae1ef5730a0974240fb4bec9c17de78b/ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9", size = 10896657 }, { url = "https://files.pythonhosted.org/packages/77/bd/aacdb8285d10f1b943dbeb818968efca35459afc29f66ae3bd4596fbf954/ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5", size = 11228362 }, { url = "https://files.pythonhosted.org/packages/39/72/fcb7ad41947f38b4eaa702aca0a361af0e9c2bf671d7fd964480670c297e/ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790", size = 8803476 }, { url = "https://files.pythonhosted.org/packages/e4/ea/cae9aeb0f4822c44651c8407baacdb2e5b4dcd7b31a84e1c5df33aa2cc20/ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6", size = 9614463 }, { url = "https://files.pythonhosted.org/packages/eb/76/fbb4bd23dfb48fa7758d35b744413b650a9fd2ddd93bca77e30376864414/ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737", size = 8959621 }, ] [[package]] name = "service-identity" version = "24.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "cryptography" }, { name = "pyasn1" }, { name = "pyasn1-modules" }, ] sdist = { url = "https://files.pythonhosted.org/packages/07/a5/dfc752b979067947261dbbf2543470c58efe735c3c1301dd870ef27830ee/service_identity-24.2.0.tar.gz", hash = "sha256:b8683ba13f0d39c6cd5d625d2c5f65421d6d707b013b375c355751557cbe8e09", size = 39245 } wheels = [ { url = "https://files.pythonhosted.org/packages/08/2c/ca6dd598b384bc1ce581e24aaae0f2bed4ccac57749d5c3befbb5e742081/service_identity-24.2.0-py3-none-any.whl", hash = "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85", size = 11364 }, ] [[package]] name = "setuptools" version = "75.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ed/22/a438e0caa4576f8c383fa4d35f1cc01655a46c75be358960d815bfbb12bd/setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686", size = 1351577 } wheels = [ { url = "https://files.pythonhosted.org/packages/90/12/282ee9bce8b58130cb762fbc9beabd531549952cac11fc56add11dcb7ea0/setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd", size = 1251070 }, ] [[package]] name = "six" version = "1.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } wheels = [ { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, ] [[package]] name = "slotscheck" version = "0.16.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/27/d6/d94545d83dd2fd8d515c76461875d23ecf80b998d8c34963a2e850ef80ca/slotscheck-0.16.5.tar.gz", hash = "sha256:6cae3e73808121cf63c1bc638c3b5ae7e10f651323ad3cf38790ce005b77e221", size = 17073 } wheels = [ { url = "https://files.pythonhosted.org/packages/c0/49/826b5c7df3a908c79588f7bff560c93a1bd79b04034698433803f4d17400/slotscheck-0.16.5-py3-none-any.whl", hash = "sha256:b202def7a1d4559575a6a1926aabe461bf780c1584275eff2d3ee4465c52d8c6", size = 20273 }, ] [[package]] name = "smart-open" version = "7.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/d8/1481294b2d110b805c0f5d23ef34158b7d5d4283633c0d34c69ea89bb76b/smart_open-7.0.5.tar.gz", hash = "sha256:d3672003b1dbc85e2013e4983b88eb9a5ccfd389b0d4e5015f39a9ee5620ec18", size = 71693 } wheels = [ { url = "https://files.pythonhosted.org/packages/06/bc/706838af28a542458bffe74a5d0772ca7f207b5495cd9fccfce61ef71f2a/smart_open-7.0.5-py3-none-any.whl", hash = "sha256:8523ed805c12dff3eaa50e9c903a6cb0ae78800626631c5fe7ea073439847b89", size = 61387 }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] [[package]] name = "snowballstemmer" version = "2.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } wheels = [ { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, ] [[package]] name = "sortedcontainers" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, ] [[package]] name = "soupsieve" version = "2.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, ] [[package]] name = "sphinx" version = "7.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alabaster" }, { name = "babel" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "docutils" }, { name = "imagesize" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "packaging" }, { name = "pygments" }, { name = "requests" }, { name = "snowballstemmer" }, { name = "sphinxcontrib-applehelp" }, { name = "sphinxcontrib-devhelp" }, { name = "sphinxcontrib-htmlhelp" }, { name = "sphinxcontrib-jsmath" }, { name = "sphinxcontrib-qthelp" }, { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dc/01/688bdf9282241dca09fe6e3a1110eda399fa9b10d0672db609e37c2e7a39/sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f", size = 6828258 } wheels = [ { url = "https://files.pythonhosted.org/packages/48/17/325cf6a257d84751a48ae90752b3d8fe0be8f9535b6253add61c49d0d9bc/sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe", size = 3169543 }, ] [[package]] name = "sphinx-autobuild" version = "2021.3.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, { name = "livereload" }, { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/a5/2ed1b81e398bc14533743be41bf0ceaa49d671675f131c4d9ce74897c9c1/sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05", size = 206402 } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/7d/8fb7557b6c9298d2bcda57f4d070de443c6355dfb475582378e2aa16a02c/sphinx_autobuild-2021.3.14-py3-none-any.whl", hash = "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac", size = 9881 }, ] [[package]] name = "sphinx-autodoc-typehints" version = "2.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/f0/b750f1ea593df9ba152e99929807530604d06fae887e5a38ae1e0a31358a/sphinx_autodoc_typehints-2.0.1.tar.gz", hash = "sha256:60ed1e3b2c970acc0aa6e877be42d48029a9faec7378a17838716cacd8c10b12", size = 38816 } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/95/5baffb0ef1b8fd72d0a5a3ab531e82c5e810df3530c8f61857c69026b7ac/sphinx_autodoc_typehints-2.0.1-py3-none-any.whl", hash = "sha256:f73ae89b43a799e587e39266672c1075b2ef783aeb382d3ebed77c38a3fc0149", size = 19533 }, ] [[package]] name = "sphinx-click" version = "6.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "docutils" }, { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/0a/5b1e8d0579dbb4ca8114e456ca4a68020bfe8e15c7001f3856be4929ab83/sphinx_click-6.0.0.tar.gz", hash = "sha256:f5d664321dc0c6622ff019f1e1c84e58ce0cecfddeb510e004cf60c2a3ab465b", size = 29574 } wheels = [ { url = "https://files.pythonhosted.org/packages/d0/d7/8621c4726ad3f788a1db4c0c409044b16edc563f5c9542807b3724037555/sphinx_click-6.0.0-py3-none-any.whl", hash = "sha256:1e0a3c83bcb7c55497751b19d07ebe56b5d7b85eb76dd399cf9061b497adc317", size = 9922 }, ] [[package]] name = "sphinx-copybutton" version = "0.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039 } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343 }, ] [[package]] name = "sphinx-design" version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fd/d0/62a7cee178d30f7217c4badea17eeca020801c0053773098d9ff65636a60/sphinx_design-0.5.0.tar.gz", hash = "sha256:e8e513acea6f92d15c6de3b34e954458f245b8e761b45b63950f65373352ab00", size = 2152330 } wheels = [ { url = "https://files.pythonhosted.org/packages/17/52/a1e9d72ecf56047df714a3dd0840a5148e4e83c100e8e0046bcea60a1054/sphinx_design-0.5.0-py3-none-any.whl", hash = "sha256:1af1267b4cea2eedd6724614f19dcc88fe2e15aff65d06b2f6252cee9c4f4c1e", size = 2173865 }, ] [[package]] name = "sphinx-jinja2-compat" version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, { name = "markupsafe" }, { name = "standard-imghdr", marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/26/df/27282da6f8c549f765beca9de1a5fc56f9651ed87711a5cac1e914137753/sphinx_jinja2_compat-0.3.0.tar.gz", hash = "sha256:f3c1590b275f42e7a654e081db5e3e5fb97f515608422bde94015ddf795dfe7c", size = 4998 } wheels = [ { url = "https://files.pythonhosted.org/packages/6f/42/2fd09d672eaaa937d6893d8b747d07943f97a6e5e30653aee6ebd339b704/sphinx_jinja2_compat-0.3.0-py3-none-any.whl", hash = "sha256:b1e4006d8e1ea31013fa9946d1b075b0c8d2a42c6e3425e63542c1e9f8be9084", size = 7883 }, ] [[package]] name = "sphinx-paramlinks" version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/21/62d3a58ff7bd02bbb9245a63d1f0d2e0455522a11a78951d16088569fca8/sphinx-paramlinks-0.6.0.tar.gz", hash = "sha256:746a0816860aa3fff5d8d746efcbec4deead421f152687411db1d613d29f915e", size = 12363 } [[package]] name = "sphinx-prompt" version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "pygments" }, { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/00/c601097bfa180c0622bef4b3a8cbb91c10281b4fec2fa1cac835fa74001c/sphinx_prompt-1.7.0.tar.gz", hash = "sha256:f95c0b44d73621fc0b493f84b0c2866eb8741140ef0260c20a0f7578457f44ad", size = 4989 } wheels = [ { url = "https://files.pythonhosted.org/packages/60/94/89f33756b82dbe9453cd05176c43a43e7f2337816f48c8a234396f027493/sphinx_prompt-1.7.0-py3-none-any.whl", hash = "sha256:7ee415d07f90f7ce1577a2c4c7f2560694af008926a69b4c940f20737621b089", size = 5224 }, ] [[package]] name = "sphinx-tabs" version = "3.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "pygments" }, { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/27/32/ab475e252dc2b704e82a91141fa404cdd8901a5cf34958fd22afacebfccd/sphinx-tabs-3.4.5.tar.gz", hash = "sha256:ba9d0c1e3e37aaadd4b5678449eb08176770e0fc227e769b6ce747df3ceea531", size = 16070 } wheels = [ { url = "https://files.pythonhosted.org/packages/20/9f/4ac7dbb9f23a2ff5a10903a4f9e9f43e0ff051f63a313e989c962526e305/sphinx_tabs-3.4.5-py3-none-any.whl", hash = "sha256:92cc9473e2ecf1828ca3f6617d0efc0aa8acb06b08c56ba29d1413f2f0f6cf09", size = 9904 }, ] [[package]] name = "sphinx-toolbox" version = "3.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "apeye" }, { name = "autodocsumm" }, { name = "beautifulsoup4" }, { name = "cachecontrol", extra = ["filecache"] }, { name = "dict2css" }, { name = "docutils" }, { name = "domdf-python-tools" }, { name = "filelock" }, { name = "html5lib" }, { name = "ruamel-yaml" }, { name = "sphinx" }, { name = "sphinx-autodoc-typehints" }, { name = "sphinx-jinja2-compat" }, { name = "sphinx-prompt" }, { name = "sphinx-tabs" }, { name = "tabulate" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/30/80/f837e85c8c216cdeef9b60393e4b00c9092a1e3d734106e0021abbf5930c/sphinx_toolbox-3.8.1.tar.gz", hash = "sha256:a4b39a6ea24fc8f10e24f052199bda17837a0bf4c54163a56f521552395f5e1a", size = 111977 } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/d6/2a28ee4cbc158ae65afb2cfcb6895ef54d972ce1e167f8a63c135b14b080/sphinx_toolbox-3.8.1-py3-none-any.whl", hash = "sha256:53d8e77dd79e807d9ef18590c4b2960a5aa3c147415054b04c31a91afed8b88b", size = 194621 }, ] [[package]] name = "sphinxcontrib-applehelp" version = "1.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/32/df/45e827f4d7e7fcc84e853bcef1d836effd762d63ccb86f43ede4e98b478c/sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e", size = 24766 } wheels = [ { url = "https://files.pythonhosted.org/packages/06/c1/5e2cafbd03105ce50d8500f9b4e8a6e8d02e22d0475b574c3b3e9451a15f/sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228", size = 120601 }, ] [[package]] name = "sphinxcontrib-devhelp" version = "1.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/98/33/dc28393f16385f722c893cb55539c641c9aaec8d1bc1c15b69ce0ac2dbb3/sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4", size = 17398 } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/09/5de5ed43a521387f18bdf5f5af31d099605c992fd25372b2b9b825ce48ee/sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", size = 84690 }, ] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b3/47/64cff68ea3aa450c373301e5bebfbb9fce0a3e70aca245fcadd4af06cd75/sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff", size = 27967 } wheels = [ { url = "https://files.pythonhosted.org/packages/6e/ee/a1f5e39046cbb5f8bc8fba87d1ddf1c6643fbc9194e58d26e606de4b9074/sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903", size = 99833 }, ] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, ] [[package]] name = "sphinxcontrib-mermaid" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/97/69/bf039237ad260073e8c02f820b3e00dc34f3a2de20aff7861e6b19d2f8c5/sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146", size = 15153 } wheels = [ { url = "https://files.pythonhosted.org/packages/cd/c8/784b9ac6ea08aa594c1a4becbd0dbe77186785362e31fd633b8c6ae0197a/sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3", size = 9597 }, ] [[package]] name = "sphinxcontrib-qthelp" version = "1.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b1/8e/c4846e59f38a5f2b4a0e3b27af38f2fcf904d4bfd82095bf92de0b114ebd/sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", size = 21658 } wheels = [ { url = "https://files.pythonhosted.org/packages/2b/14/05f9206cf4e9cfca1afb5fd224c7cd434dcc3a433d6d9e4e0264d29c6cdb/sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6", size = 90609 }, ] [[package]] name = "sphinxcontrib-serializinghtml" version = "1.1.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b5/72/835d6fadb9e5d02304cf39b18f93d227cd93abd3c41ebf58e6853eeb1455/sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952", size = 21019 } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/77/5464ec50dd0f1c1037e3c93249b040c8fc8078fdda97530eeb02424b6eea/sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", size = 94021 }, ] [[package]] name = "sqlalchemy" version = "2.0.36" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'WIN32') or (python_full_version < '3.13' and platform_machine == 'aarch64') or (python_full_version < '3.13' and platform_machine == 'amd64') or (python_full_version < '3.13' and platform_machine == 'ppc64le') or (python_full_version < '3.13' and platform_machine == 'win32') or (python_full_version < '3.13' and platform_machine == 'x86_64')" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/65/9cbc9c4c3287bed2499e05033e207473504dc4df999ce49385fb1f8b058a/sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5", size = 9574485 } wheels = [ { url = "https://files.pythonhosted.org/packages/db/72/14ab694b8b3f0e35ef5beb74a8fea2811aa791ba1611c44dc90cdf46af17/SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72", size = 2092604 }, { url = "https://files.pythonhosted.org/packages/1e/59/333fcbca58b79f5b8b61853d6137530198823392151fa8fd9425f367519e/SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908", size = 2083796 }, { url = "https://files.pythonhosted.org/packages/6c/a0/ec3c188d2b0c1bc742262e76408d44104598d7247c23f5b06bb97ee21bfa/SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08", size = 3066165 }, { url = "https://files.pythonhosted.org/packages/07/15/68ef91de5b8b7f80fb2d2b3b31ed42180c6227fe0a701aed9d01d34f98ec/SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07", size = 3074428 }, { url = "https://files.pythonhosted.org/packages/e2/4c/9dfea5e63b87325eef6d9cdaac913459aa6a157a05a05ea6ff20004aee8e/SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5", size = 3030477 }, { url = "https://files.pythonhosted.org/packages/16/a5/fcfde8e74ea5f683b24add22463bfc21e431d4a5531c8a5b55bc6fbea164/SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44", size = 3055942 }, { url = "https://files.pythonhosted.org/packages/3c/ee/c22c415a771d791ae99146d72ffdb20e43625acd24835ea7fc157436d59f/SQLAlchemy-2.0.36-cp310-cp310-win32.whl", hash = "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa", size = 2064960 }, { url = "https://files.pythonhosted.org/packages/aa/af/ad9c25cadc79bd851bdb9d82b68af9bdb91ff05f56d0da2f8a654825974f/SQLAlchemy-2.0.36-cp310-cp310-win_amd64.whl", hash = "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5", size = 2089078 }, { url = "https://files.pythonhosted.org/packages/00/4e/5a67963fd7cbc1beb8bd2152e907419f4c940ef04600b10151a751fe9e06/SQLAlchemy-2.0.36-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c", size = 2093782 }, { url = "https://files.pythonhosted.org/packages/b3/24/30e33b6389ebb5a17df2a4243b091bc709fb3dfc9a48c8d72f8e037c943d/SQLAlchemy-2.0.36-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71", size = 2084180 }, { url = "https://files.pythonhosted.org/packages/10/1e/70e9ed2143a27065246be40f78637ad5160ea0f5fd32f8cab819a31ff54d/SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff", size = 3202469 }, { url = "https://files.pythonhosted.org/packages/b4/5f/95e0ed74093ac3c0db6acfa944d4d8ac6284ef5e1136b878a327ea1f975a/SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d", size = 3202464 }, { url = "https://files.pythonhosted.org/packages/91/95/2cf9b85a6bc2ee660e40594dffe04e777e7b8617fd0c6d77a0f782ea96c9/SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb", size = 3139508 }, { url = "https://files.pythonhosted.org/packages/92/ea/f0c01bc646456e4345c0fb5a3ddef457326285c2dc60435b0eb96b61bf31/SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8", size = 3159837 }, { url = "https://files.pythonhosted.org/packages/a6/93/c8edbf153ee38fe529773240877bf1332ed95328aceef6254288f446994e/SQLAlchemy-2.0.36-cp311-cp311-win32.whl", hash = "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f", size = 2064529 }, { url = "https://files.pythonhosted.org/packages/b1/03/d12b7c1d36fd80150c1d52e121614cf9377dac99e5497af8d8f5b2a8db64/SQLAlchemy-2.0.36-cp311-cp311-win_amd64.whl", hash = "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959", size = 2089874 }, { url = "https://files.pythonhosted.org/packages/b8/bf/005dc47f0e57556e14512d5542f3f183b94fde46e15ff1588ec58ca89555/SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4", size = 2092378 }, { url = "https://files.pythonhosted.org/packages/94/65/f109d5720779a08e6e324ec89a744f5f92c48bd8005edc814bf72fbb24e5/SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855", size = 2082778 }, { url = "https://files.pythonhosted.org/packages/60/f6/d9aa8c49c44f9b8c9b9dada1f12fa78df3d4c42aa2de437164b83ee1123c/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53", size = 3232191 }, { url = "https://files.pythonhosted.org/packages/8a/ab/81d4514527c068670cb1d7ab62a81a185df53a7c379bd2a5636e83d09ede/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a", size = 3243044 }, { url = "https://files.pythonhosted.org/packages/35/b4/f87c014ecf5167dc669199cafdb20a7358ff4b1d49ce3622cc48571f811c/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686", size = 3178511 }, { url = "https://files.pythonhosted.org/packages/ea/09/badfc9293bc3ccba6ede05e5f2b44a760aa47d84da1fc5a326e963e3d4d9/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588", size = 3205147 }, { url = "https://files.pythonhosted.org/packages/c8/60/70e681de02a13c4b27979b7b78da3058c49bacc9858c89ba672e030f03f2/SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e", size = 2062709 }, { url = "https://files.pythonhosted.org/packages/b7/ed/f6cd9395e41bfe47dd253d74d2dfc3cab34980d4e20c8878cb1117306085/SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5", size = 2088433 }, { url = "https://files.pythonhosted.org/packages/78/5c/236398ae3678b3237726819b484f15f5c038a9549da01703a771f05a00d6/SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef", size = 2087651 }, { url = "https://files.pythonhosted.org/packages/a8/14/55c47420c0d23fb67a35af8be4719199b81c59f3084c28d131a7767b0b0b/SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8", size = 2078132 }, { url = "https://files.pythonhosted.org/packages/3d/97/1e843b36abff8c4a7aa2e37f9bea364f90d021754c2de94d792c2d91405b/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b", size = 3164559 }, { url = "https://files.pythonhosted.org/packages/7b/c5/07f18a897b997f6d6b234fab2bf31dccf66d5d16a79fe329aefc95cd7461/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2", size = 3177897 }, { url = "https://files.pythonhosted.org/packages/b3/cd/e16f3cbefd82b5c40b33732da634ec67a5f33b587744c7ab41699789d492/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf", size = 3111289 }, { url = "https://files.pythonhosted.org/packages/15/85/5b8a3b0bc29c9928aa62b5c91fcc8335f57c1de0a6343873b5f372e3672b/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c", size = 3139491 }, { url = "https://files.pythonhosted.org/packages/a1/95/81babb6089938680dfe2cd3f88cd3fd39cccd1543b7cb603b21ad881bff1/SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436", size = 2060439 }, { url = "https://files.pythonhosted.org/packages/c1/ce/5f7428df55660d6879d0522adc73a3364970b5ef33ec17fa125c5dbcac1d/SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88", size = 2084574 }, { url = "https://files.pythonhosted.org/packages/db/da/443679a0b9e0a009f00d1542595c8a4d582ece1809704e703c4843f18768/SQLAlchemy-2.0.36-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545", size = 2097524 }, { url = "https://files.pythonhosted.org/packages/60/88/08249dc5651d976b64c257250ade16ec02444257b44e404e258f2862c201/SQLAlchemy-2.0.36-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24", size = 2088207 }, { url = "https://files.pythonhosted.org/packages/19/41/feb0216cced91211c9dd045f08fa020a3bd2e188110217d24b6b2a90b6a2/SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3", size = 3090690 }, { url = "https://files.pythonhosted.org/packages/80/fe/0055147b71de2007e716ddc686438aefb390b03fd2e382ff4a8588b78b58/SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687", size = 3097567 }, { url = "https://files.pythonhosted.org/packages/4e/55/52eaef72f071b89e2e965decc264d16b6031a39365a6593067053ba8bb97/SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346", size = 3044686 }, { url = "https://files.pythonhosted.org/packages/2d/c8/6fb6179ad2eb9baab61132786488fa10e5b588ef2b008f57f560ae7f39d1/SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1", size = 3067814 }, { url = "https://files.pythonhosted.org/packages/52/84/be7227a06c24418f131a877159eb962c62447883069390c868d1a782f01d/SQLAlchemy-2.0.36-cp38-cp38-win32.whl", hash = "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e", size = 2067569 }, { url = "https://files.pythonhosted.org/packages/df/4f/9c70c53a5bd6de5957db78578cb0491732da636032566d3e8748659513cb/SQLAlchemy-2.0.36-cp38-cp38-win_amd64.whl", hash = "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793", size = 2092329 }, { url = "https://files.pythonhosted.org/packages/43/10/c1c865afeb50270677942cda17ed78b55b0a0068e426d22284a625d7341f/SQLAlchemy-2.0.36-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa", size = 2095474 }, { url = "https://files.pythonhosted.org/packages/25/cb/78d7663ad1c82ca8b5cbc7532b8e3c9f80a53f1bdaafd8f5314525700a01/SQLAlchemy-2.0.36-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689", size = 2086708 }, { url = "https://files.pythonhosted.org/packages/5c/5b/f9b5cf759865b0dd8b20579b3d920ed87b6160fce75e2b7ed697ddbf0008/SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d", size = 3080607 }, { url = "https://files.pythonhosted.org/packages/18/f6/afaef83a3fbeff40b9289508b985c5630c0e8303d08106a0117447c680d9/SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06", size = 3088410 }, { url = "https://files.pythonhosted.org/packages/62/60/ec2b8c14b3c15b4a915ae821b455823fbafa6f38c4011b27c0a76f94928a/SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763", size = 3047623 }, { url = "https://files.pythonhosted.org/packages/40/a2/9f748bdaf769eceb780c438b3dd7a37b8b8cbc6573e2a3748b0d5c2e9d80/SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7", size = 3074096 }, { url = "https://files.pythonhosted.org/packages/01/f7/290d7193c81d1ff0f751bd9430f3762bee0f53efd0273aba7ba18eb10520/SQLAlchemy-2.0.36-cp39-cp39-win32.whl", hash = "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28", size = 2067304 }, { url = "https://files.pythonhosted.org/packages/6f/a0/dc1a808d6ac466b190ca570f7ce52a1761308279eab4a09367ccf2cd6bd7/SQLAlchemy-2.0.36-cp39-cp39-win_amd64.whl", hash = "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a", size = 2091520 }, { url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787 }, ] [[package]] name = "standard-imghdr" version = "3.10.14" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/09/d2/2eb5521072c9598886035c65c023f39f7384bcb73eed70794f469e34efac/standard_imghdr-3.10.14.tar.gz", hash = "sha256:2598fe2e7c540dbda34b233295e10957ab8dc8ac6f3bd9eaa8d38be167232e52", size = 5474 } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/d0/9852f70eb01f814843530c053542b72d30e9fbf74da7abb0107e71938389/standard_imghdr-3.10.14-py3-none-any.whl", hash = "sha256:cdf6883163349624dee9a81d2853a20260337c4cd41c04e99c082e01833a08e2", size = 5598 }, ] [[package]] name = "starlette" version = "0.41.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "anyio", version = "4.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 } wheels = [ { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 }, ] [[package]] name = "structlog" version = "24.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/78/a3/e811a94ac3853826805253c906faa99219b79951c7d58605e89c79e65768/structlog-24.4.0.tar.gz", hash = "sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4", size = 1348634 } wheels = [ { url = "https://files.pythonhosted.org/packages/bf/65/813fc133609ebcb1299be6a42e5aea99d6344afb35ccb43f67e7daaa3b92/structlog-24.4.0-py3-none-any.whl", hash = "sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610", size = 67180 }, ] [[package]] name = "tabulate" version = "0.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090 } wheels = [ { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 }, ] [[package]] name = "targ" version = "0.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, { name = "docstring-parser" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/8c/8ab5d5391e36e27a67c6921f952a7c18d0b0ec19f4f8a88f0a64da81552d/targ-0.4.0.tar.gz", hash = "sha256:dcdb57945bffe5bc59570d2e41bb1adc6280c5460332c5daf300729bc88d1aba", size = 9494 } wheels = [ { url = "https://files.pythonhosted.org/packages/50/1e/47625c6b3615035d3ef11da1edd6726e245610ed1cbb08d60acc939bd1ce/targ-0.4.0-py3-none-any.whl", hash = "sha256:5237524323661ffa899158d668468b5c94bb84e2d988bd216981932844da63eb", size = 7204 }, ] [[package]] name = "taskgroup" version = "0.0.0a4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0a/40/02753c40fa30dfdde7567c1daeefbf957dcf8c99e6534a80afb438adf07e/taskgroup-0.0.0a4.tar.gz", hash = "sha256:eb08902d221e27661950f2a0320ddf3f939f579279996f81fe30779bca3a159c", size = 8553 } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e5/0f8dac3d9e6314f60a725cdc9ec01f45591ddd720e59f6a4ff8bdcdf82cd/taskgroup-0.0.0a4-py2.py3-none-any.whl", hash = "sha256:5c1bd0e4c06114e7a4128583ab75c987597d5378a33948a3b74c662b90f61277", size = 9109 }, ] [[package]] name = "time-machine" version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/82d358c4d53555f031c2343d1c235b56b9f3b0a60ac3adc555778fe87506/time_machine-2.15.0.tar.gz", hash = "sha256:ebd2e63baa117ded04b978813fcd1279d3fc6be2149c9cac75c716b6f1db774c", size = 25067 } wheels = [ { url = "https://files.pythonhosted.org/packages/67/47/35413db37da55865fdbf60649bcb948cc2559f420ef4e91e77e4e24c71b8/time_machine-2.15.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:892d016789b59950989b2db188dcd46cf16d34e8daf2343e33b679b0c5fd1001", size = 20779 }, { url = "https://files.pythonhosted.org/packages/e0/c3/fda6d2336737d0331eb55357db1dc916af14c4fda77c69ad8b3733b003c4/time_machine-2.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4428bdae507996aa3fdeb4727bca09e26306fa64a502e7335207252684516cbf", size = 17040 }, { url = "https://files.pythonhosted.org/packages/36/e1/71200f24d668e5183e875a08ba5e557b6107c1b7d57fa6d54ac24ad10234/time_machine-2.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0302568338c8bd333ed0698231dbb781b70ead1a5579b4ac734b9bf88313229f", size = 34811 }, { url = "https://files.pythonhosted.org/packages/9e/2f/4b9289ea07978ad5c3469c872c7eeadf5f5b3a1dcebe2fb274c2486fc220/time_machine-2.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18fc4740073e67071472c48355775ec6d1b93af5c675524b7de2474e0dcd8741", size = 32820 }, { url = "https://files.pythonhosted.org/packages/5f/9e/9f838c91d2248d716281af60dfea4131438c6ad6d7405ebc6e47f8c25c3b/time_machine-2.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:768d33b484a35da93731cc99bdc926b539240a78673216cdc6306833d9072350", size = 34635 }, { url = "https://files.pythonhosted.org/packages/f3/10/1048b5ba6de55779563f005de5fbfb764727bf9678ad7701cea480b3816c/time_machine-2.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73a8c8160d2a170dadcad5b82fb5ee53236a19cec0996651cf4d21da0a2574d5", size = 34326 }, { url = "https://files.pythonhosted.org/packages/13/82/6b4df8e5abf754b0ccceeb59fa32486d28c65f67d4ada37ff8b1e9f52006/time_machine-2.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:09fd839a321a92aa8183206c383b9725eaf4e0a28a70e4cb87db292b352eeefb", size = 32639 }, { url = "https://files.pythonhosted.org/packages/cf/07/95e380c46136252401d97f613782a10061b3c11b61edaeb78e83aedc1a88/time_machine-2.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:838a6d117739f1ae6ecc45ec630fa694f41a85c0d07b1f3b1db2a6cc52c1808b", size = 34021 }, { url = "https://files.pythonhosted.org/packages/b6/0c/6595fa82bd70bc7e8065bfc6534e51a27c18978f7c158d6392c979cace2c/time_machine-2.15.0-cp310-cp310-win32.whl", hash = "sha256:d24d2ec74923b49bce7618e3e7762baa6be74e624d9829d5632321de102bf386", size = 19413 }, { url = "https://files.pythonhosted.org/packages/2f/3d/cb3c1cecfeb4b6423302ee1b2863617390500f67526f0fc1fb5641db05f6/time_machine-2.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:95c8e7036cf442480d0bf6f5fde371e1eb6dbbf5391d7bdb8db73bd8a732b538", size = 20280 }, { url = "https://files.pythonhosted.org/packages/22/aa/96aaac88738369fba43d5cb076bb09290b1a2cbd84210bcc0a9a519c7970/time_machine-2.15.0-cp310-cp310-win_arm64.whl", hash = "sha256:660810cd27a8a94cb5e845e8f28a95e70b01ff0c45466d394c4a0cba5a0ae279", size = 18392 }, { url = "https://files.pythonhosted.org/packages/ce/54/829ab196c3306eb4cee95e3c8e7d004e15877b36479de5d2ecc72fc1d3d4/time_machine-2.15.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:674097dd54a0bbd555e7927092c74428c4c07268ad52bca38cfccc3214707e50", size = 20448 }, { url = "https://files.pythonhosted.org/packages/e1/48/a06f8c7db768db501a60210a48f3d37b7b3d65ca85aa8dc08147eb204b4a/time_machine-2.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e83fd6112808d1d14d1a57397c6fa3bd71bb2f3b8800036e12366e3680819b9", size = 16897 }, { url = "https://files.pythonhosted.org/packages/e7/f8/73265927e3da54a417536dc3d8c9aad806b62b8133099a7ee12661aba1a3/time_machine-2.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b095a1de40ca1afaeae8df3f45e26b645094a1912e6e6871e725fcf06ecdb74a", size = 32789 }, { url = "https://files.pythonhosted.org/packages/8a/a4/bcf8ad40a4c6973a67aba5df7ed704dc34256835fb074cfb4d4caa0f93a5/time_machine-2.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4601fe7a6b74c6fd9207e614d9db2a20dd4befd4d314677a0feac13a67189707", size = 30911 }, { url = "https://files.pythonhosted.org/packages/13/87/a6de1b187f5468781e0e722c877323625227151cc8ffff363a7391b01149/time_machine-2.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245ef73f9927b7d4909d554a6a0284dbc5dee9730adea599e430b37c9e9fa203", size = 32644 }, { url = "https://files.pythonhosted.org/packages/33/1f/7378d5a032467891a1005e546532223b97c53440c6359b1133696a5cb1ef/time_machine-2.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:704abc7f3403584cca9c01c5809812e0bd70632ea4251389fae4f45e11aad94f", size = 32472 }, { url = "https://files.pythonhosted.org/packages/68/14/2fab61ad2c9a831925bce3d5e341fa2108ba062c2de0c190569e1ee6a1c9/time_machine-2.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6425001e50a0c82108caed438233066cea04d42a8fc9c49bfcf081a5b96e5b4e", size = 30882 }, { url = "https://files.pythonhosted.org/packages/8a/01/f5146b9956b548072000a87f3eb8dbb2591ada1516a5d889aa12b373948f/time_machine-2.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d4073b754f90b19f28d036ec5143d3fca3a75e4d4241d78790a6178b00bb373", size = 32176 }, { url = "https://files.pythonhosted.org/packages/ca/09/8a8488e6d3faf3cb68d078f27ca94aa3ba1bc08d5f804265c590208a70f5/time_machine-2.15.0-cp311-cp311-win32.whl", hash = "sha256:8817b0f7d7830215261b18db83c9c3ef1da6bb64da5c292d7c70b9a46e5a6745", size = 19305 }, { url = "https://files.pythonhosted.org/packages/75/33/d8411b197a08eedb3ce086022cdf4faf1f15738607a2d943fd5286f57fdd/time_machine-2.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:ddad27a62df2ea47b7b483009fbfcf167a71d702cbd8e2eefd9ddc1c93146658", size = 20210 }, { url = "https://files.pythonhosted.org/packages/8c/f5/e9b5d7be612403e570a42af5c2823506877e726f77f2d6ff272d72d0aed3/time_machine-2.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f021aa2dbd8fbfe54d3fa2258518129108b7496922b3bcff2cf5991078eec67", size = 18278 }, { url = "https://files.pythonhosted.org/packages/49/47/46bf332f4ecd7f35e197131b9c23daa39423cf71b814e36e9d5df3cf2380/time_machine-2.15.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a22f47c34ee1fcf7d93a8c5c93135499aac879d9d5d8f820bd28571a30fdabcd", size = 20436 }, { url = "https://files.pythonhosted.org/packages/f1/36/9990f16868ffdefe6b5aecfdfbcb11718230e414ca61a887fbee884f70e5/time_machine-2.15.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b684f8ecdeacd6baabc17b15ac1b054ca62029193e6c5367ef00b3516671de80", size = 16926 }, { url = "https://files.pythonhosted.org/packages/ca/2d/007955a0899cd079a400bc204c03edc76274de2471d94ca235ff587a6eba/time_machine-2.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f7add997684bc6141e1c80f6ba0c38ffe316ba277a4074e61b1b7b4f5a172bf", size = 17157 }, { url = "https://files.pythonhosted.org/packages/7a/e2/66d26450f9bfd1b019abdefbf0c62e760efc8992c7bf88d6c18f7ea6b94a/time_machine-2.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31af56399bf7c9ef76a3f7b6d9471dffa8f06ee373c194a374b69523f9061de9", size = 34005 }, { url = "https://files.pythonhosted.org/packages/46/2c/dc2c42200aee6b47a55274d984736f7507ecfbfd0345114ec511ec444bef/time_machine-2.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b94cba3edfc54bcb3ab5be616a2f50fa48be438e5af970824efdf882d1bc31", size = 31926 }, { url = "https://files.pythonhosted.org/packages/87/59/10d8faecbd233b0da831eb9973c3e650c83fb3d443edb607b6b26c9688ac/time_machine-2.15.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3862dda89bdb05f9d521b08fdcb24b19a7dd9f559ae324f4301ba7a07b6eea64", size = 33685 }, { url = "https://files.pythonhosted.org/packages/2e/a4/702ad9e328cbc7b3f1833dee4886d0994e52bc2d9640effa64bccc7740fa/time_machine-2.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e1790481a6b9ce38888f22ce30710244067898c3ac4805a0e061e381f3db3506", size = 33609 }, { url = "https://files.pythonhosted.org/packages/a0/9d/6009d28ad395a45b5bb91af31616494b4e61d6d9df252d0e5933cd3aa8f1/time_machine-2.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a731c03bc00552ee6cc685a59616d36003124e7e04c6ddf65c2c47f1c3d85480", size = 31737 }, { url = "https://files.pythonhosted.org/packages/36/22/b55df08cf48d46af93ee2f4310dd88c8519bc5f98afd24af57a81a5d5272/time_machine-2.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e6776840aea3ff5ab6924b50117957da62db51b109b3b491c0d5817a804b1a8e", size = 33253 }, { url = "https://files.pythonhosted.org/packages/52/d7/bb5e92f0b0268cd13baad874d82b0e964a847cf52740464abeec48dc1642/time_machine-2.15.0-cp312-cp312-win32.whl", hash = "sha256:9479530e3fce65f6149058071fa4df8150025f15b43b103445f619842981a87c", size = 19369 }, { url = "https://files.pythonhosted.org/packages/f4/33/276537ba292fc7ee67e6aef7566b2a6b313dbc4d479e5e80eed43094ef48/time_machine-2.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f3ab4185c1f72010846ca9fccb08349e23a2b52982a18d9870e848ce9f1c86", size = 20219 }, { url = "https://files.pythonhosted.org/packages/e0/0e/e8b75032248f59a2bc5c125d3a41242b1e577caa07585c42b22373d6466d/time_machine-2.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:c0473dfa8f17c6a9a250b2bd6a5b62af3aa7d22518f701649115f1085d5e35ab", size = 18297 }, { url = "https://files.pythonhosted.org/packages/7c/a1/ebe212530628aa29a86a771ca77cb2d1ead667382cfa89a3fb849e3f0108/time_machine-2.15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f50f10058b884d45cd8a50423bf561b1f9f9df7058abeb8b318700c8bcf4bb54", size = 20492 }, { url = "https://files.pythonhosted.org/packages/29/0d/2a19951729e50d8809e161e533585c0be5ae39c0cf40140877353847b9b5/time_machine-2.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:df6f618b98f0848fd8d07039541e10f23db679d8283f8719e870a98e1ef8e639", size = 16961 }, { url = "https://files.pythonhosted.org/packages/85/eb/33cf2173758b128f55c880c492e17b70f6c325e7bee879f9b0171cfe02a0/time_machine-2.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52468a0784544eba708c0ae6bc5e8c5dcfd685495a60f7f74028662c984bd9cd", size = 34051 }, { url = "https://files.pythonhosted.org/packages/e1/23/da9a7935a7be952ab6163caf976b6bad049f6e117f3a396ecc381b077cb6/time_machine-2.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c08800c28160f4d32ca510128b4e201a43c813e7a2dd53178fa79ebe050eba13", size = 31966 }, { url = "https://files.pythonhosted.org/packages/0b/0d/a8e3cbd91ffa98b0fa50b6e29d03151f37aa04cca4dd658e33cdf2b4731e/time_machine-2.15.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65d395211736d9844537a530287a7c64b9fda1d353e899a0e1723986a0859154", size = 33727 }, { url = "https://files.pythonhosted.org/packages/07/53/c084031980706517cfbae9f462e455d61c7cbf9b66a8a83bcc5b79d00836/time_machine-2.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b177d334a35bf2ce103bfe4e0e416e4ee824dd33386ea73fa7491c17cc61897", size = 33690 }, { url = "https://files.pythonhosted.org/packages/a0/30/5c87e8709ba00c893faf8a9bddf06abf317fdc6103fe78bdf99c53ab444f/time_machine-2.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9a6a9342fae113b12aab42c790880c549d9ba695b8deff27ee08096eedd67569", size = 31809 }, { url = "https://files.pythonhosted.org/packages/04/7b/92ac7c556cd123bf8b23dbae3cf4a273c276110b87d0c4b5600c2cec8e70/time_machine-2.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bcbb25029ee8756f10c6473cea5ef21707a1d9a8752cdf29fad3a5f34aa4a313", size = 33325 }, { url = "https://files.pythonhosted.org/packages/e9/71/36c74bab3d4e4385d31610b367da1535a36d17358df058e0920a7510e17c/time_machine-2.15.0-cp313-cp313-win32.whl", hash = "sha256:29b988b1f09f2a083b12b6b054787b799ae91ee15bb0e9de3e48f880e4d68674", size = 19397 }, { url = "https://files.pythonhosted.org/packages/a4/69/ea5976c43a673894f2fa85a05b28a610b474472f393e59722a6946f7070b/time_machine-2.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:d828721dcbcb94b904a6b25df67c2513ecd24cd9e36694f38b9f0fa71c7c6103", size = 20242 }, { url = "https://files.pythonhosted.org/packages/cc/c8/26367d0b8dfaf7445576fe0051bff61b8f5be752e7bf3e8807ed7fa3a343/time_machine-2.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:008bd668d933b1a029c81805bcdc0132390c2545b103cf8e6709e3adbc37989d", size = 18337 }, { url = "https://files.pythonhosted.org/packages/97/54/eeac8568cad4f3eb255cc78f1fa2c36147afd3fcba770283bf2b2a188b33/time_machine-2.15.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e99689f6c6b9ca6e2fc7a75d140e38c5a7985dab61fe1f4e506268f7e9844e05", size = 20674 }, { url = "https://files.pythonhosted.org/packages/5c/82/488341de4c03c0856aaf5db74f2a8fe18dcc7657401334c54c4aa6cb0fc6/time_machine-2.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:671e88a6209a1cf415dc0f8c67d2b2d3b55b436cc63801a518f9800ebd752959", size = 16990 }, { url = "https://files.pythonhosted.org/packages/9c/cc/0ca559e71be4eb05917d02364f4d356351b31dd0d6ff3c4c86fa4de0a03e/time_machine-2.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b2d28daf4cabc698aafb12135525d87dc1f2f893cbd29a8a6fe0d8d36d1342c", size = 35501 }, { url = "https://files.pythonhosted.org/packages/92/a0/14905a5feecc6d2e87ebe6dd2b044358422836ed173071cdc1245aa5ec88/time_machine-2.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cd9f057457d12604be18b623bcd5ae7d0b917ad66cb510ee1135d5f123666e2", size = 33430 }, { url = "https://files.pythonhosted.org/packages/19/a4/282b65b4d835dfd7b863777cc4206bec375285bda884dc22bd1264716f6a/time_machine-2.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dc6793e512a62ba9eab250134a2e67372c16ae9948e73d27c2ef355356e2e1", size = 35366 }, { url = "https://files.pythonhosted.org/packages/93/8e/f7db3f641f1ff86b98594c9cf8d71c8d292cc2bde06c1369ce4745494cc5/time_machine-2.15.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0630a32e9ebcf2fac3704365b31e271fef6eabd6fedfa404cd8dbd244f7fc84d", size = 34373 }, { url = "https://files.pythonhosted.org/packages/3d/9f/8c8ac57ccb29e692e0940e58515a9afb844d2d11b7f057a0fe153bfe4877/time_machine-2.15.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:617c9a92d8d8f60d5ef39e76596620503752a09f834a218e5b83be352fdd6c91", size = 32667 }, { url = "https://files.pythonhosted.org/packages/ec/e7/f0c6f9507b0bbfdec54d256b6efc9417ae1a01ce6320c2a42235b807cf86/time_machine-2.15.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3f7eadd820e792de33a9ec91f8178a2b9088e4e8b9a166953419ddc4ec5f7cfe", size = 34070 }, { url = "https://files.pythonhosted.org/packages/20/82/ac2d8343db8dade1372457d7a5694f069882d9eac110ddce2643ef0501aa/time_machine-2.15.0-cp38-cp38-win32.whl", hash = "sha256:b7b647684eb2e1fd1e5e6b101249d5fe9d6117c117b5e336ad8dd75af48d2d1f", size = 19396 }, { url = "https://files.pythonhosted.org/packages/3a/12/ac7bb1e932536fce359e021a62c2a5a30c4e470293d6f8b2fb47077562dc/time_machine-2.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b48abd7745caec1a78a16a048966cde14ff6ccb04d471a7201532648d3f77d14", size = 20266 }, { url = "https://files.pythonhosted.org/packages/ec/bf/d9689e1fa669e575c3ed57bf4f9205a9b5fbe703dc7ef89ba5ce9aa39a38/time_machine-2.15.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c2b1c91b437133c672e374857eccb1dd2c2d9f8477ae3b35138382d5ef19846", size = 20775 }, { url = "https://files.pythonhosted.org/packages/d8/4b/4314a7882b470c52cd527601107b1163e19d37fb1eb31eea0f8d73d0b178/time_machine-2.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:79bf1ef6850182e09d86e61fa31717da56014a3b2234afb025fca1f2a43ac07b", size = 17037 }, { url = "https://files.pythonhosted.org/packages/f1/b8/adf2f8b8e10f6f5e498b0cddd103f6520144af53fb27b5a01eca50812a92/time_machine-2.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:658ea8477fa020f08435fb7277635eb0b50cd5206b9d4cbe10e9a5466b01f855", size = 34511 }, { url = "https://files.pythonhosted.org/packages/e3/86/fda41a9e8115fd377f2d4d15c91a414f75cb8f2cd7f8bde974855a0f381f/time_machine-2.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c947135750d20f35acac290c34f1acf5771fc166a3fbc0e3816a97c756aaa5f5", size = 32533 }, { url = "https://files.pythonhosted.org/packages/cb/7e/1e2e69fee659f00715f12392cabea1920245504862eab2caac6e3f30de5b/time_machine-2.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dee3a0dd1866988c49a5d00564404db9bcdf49ca92f9c4e8b6c99609d64e698", size = 34348 }, { url = "https://files.pythonhosted.org/packages/41/da/8db2df73ebe9f23af25b05f1720b108a145805a8c83d5ff8248e2d3cbcfa/time_machine-2.15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c596920d6017702a36e3a43fd8110a84e87d6229f30b84bd5640cbae9b5145da", size = 34081 }, { url = "https://files.pythonhosted.org/packages/a7/c7/9202404f8885257c09c98d3e5186b989b6b482a2983dc24c81bd0333e668/time_machine-2.15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:014589d0edd4aa14f8d63985745565e8cbbe48461d6c004a96000b47f6b44e78", size = 32357 }, { url = "https://files.pythonhosted.org/packages/4d/79/482a69c31259c3c2efcd9e73ea4a0a4d315103836c1667875612288bca28/time_machine-2.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5ff655716cd13a242eef8cf5d368074e8b396ff86508a5933e7cff4f2b3eb3c2", size = 33742 }, { url = "https://files.pythonhosted.org/packages/f0/39/89725d12a3552bb9113528d8f9aa7188e1660b377b74e7d72e8ab5eeff06/time_machine-2.15.0-cp39-cp39-win32.whl", hash = "sha256:1168eebd7af7e6e3e2fd378c16ca917b97dd81c89a1f1f9e1daa985c81699d90", size = 19410 }, { url = "https://files.pythonhosted.org/packages/89/0c/50e86c4a7b72d2bdc658492b13e804f933814f86f34c4350758d1ab87586/time_machine-2.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:c344eb09fcfbf71e5b5847d4f188fec98e1c3a976125ef571eac5f1c39e7a5e5", size = 20281 }, { url = "https://files.pythonhosted.org/packages/f6/4d/f8ad3b0c50a268a9ea766c9533866bba6a7717a5324c84e356abb7347fa4/time_machine-2.15.0-cp39-cp39-win_arm64.whl", hash = "sha256:899f1a856b3bebb82b6cbc3c0014834b583b83f246b28e462a031ec1b766130b", size = 18387 }, ] [[package]] name = "toml" version = "0.10.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } wheels = [ { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } wheels = [ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] [[package]] name = "tornado" version = "6.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/59/45/a0daf161f7d6f36c3ea5fc0c2de619746cc3dd4c76402e9db545bd920f63/tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b", size = 501135 } wheels = [ { url = "https://files.pythonhosted.org/packages/26/7e/71f604d8cea1b58f82ba3590290b66da1e72d840aeb37e0d5f7291bd30db/tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1", size = 436299 }, { url = "https://files.pythonhosted.org/packages/96/44/87543a3b99016d0bf54fdaab30d24bf0af2e848f1d13d34a3a5380aabe16/tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803", size = 434253 }, { url = "https://files.pythonhosted.org/packages/cb/fb/fdf679b4ce51bcb7210801ef4f11fdac96e9885daa402861751353beea6e/tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec", size = 437602 }, { url = "https://files.pythonhosted.org/packages/4f/3b/e31aeffffc22b475a64dbeb273026a21b5b566f74dee48742817626c47dc/tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946", size = 436972 }, { url = "https://files.pythonhosted.org/packages/22/55/b78a464de78051a30599ceb6983b01d8f732e6f69bf37b4ed07f642ac0fc/tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf", size = 437173 }, { url = "https://files.pythonhosted.org/packages/79/5e/be4fb0d1684eb822c9a62fb18a3e44a06188f78aa466b2ad991d2ee31104/tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634", size = 437892 }, { url = "https://files.pythonhosted.org/packages/f5/33/4f91fdd94ea36e1d796147003b490fe60a0215ac5737b6f9c65e160d4fe0/tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73", size = 437334 }, { url = "https://files.pythonhosted.org/packages/2b/ae/c1b22d4524b0e10da2f29a176fb2890386f7bd1f63aacf186444873a88a0/tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c", size = 437261 }, { url = "https://files.pythonhosted.org/packages/b5/25/36dbd49ab6d179bcfc4c6c093a51795a4f3bed380543a8242ac3517a1751/tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482", size = 438463 }, { url = "https://files.pythonhosted.org/packages/61/cc/58b1adeb1bb46228442081e746fcdbc4540905c87e8add7c277540934edb/tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", size = 438907 }, ] [[package]] name = "tree-sitter" version = "0.21.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/39/9e/b7cb190aa08e4ea387f2b1531da03efb4b8b033426753c0b97e3698645f6/tree-sitter-0.21.3.tar.gz", hash = "sha256:b5de3028921522365aa864d95b3c41926e0ba6a85ee5bd000e10dc49b0766988", size = 155688 } wheels = [ { url = "https://files.pythonhosted.org/packages/60/b3/5507348eee41af3abf537607779c87de9141fc41af5aa547ff5c1a87fcf6/tree_sitter-0.21.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:351f302b6615230c9dac9829f0ba20a94362cd658206ca9a7b2d58d73373dfb0", size = 133430 }, { url = "https://files.pythonhosted.org/packages/a5/24/05a76f662445ebdebd00e12bc4c9ebf974a559bb7f4863dc1def775add39/tree_sitter-0.21.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:766e79ae1e61271e7fdfecf35b6401ad9b47fc07a0965ad78e7f97fddfdf47a6", size = 126087 }, { url = "https://files.pythonhosted.org/packages/64/74/2c9dc1acb4c43b9c15765ab0fb4babb76e49e8198889dfc730bc1e906a82/tree_sitter-0.21.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c4d3d4d4b44857e87de55302af7f2d051c912c466ef20e8f18158e64df3542a", size = 485385 }, { url = "https://files.pythonhosted.org/packages/06/a3/9f65bcb73947bf4d16e816fa547e1edfe411a530ff3370c1ae10c5bc21d8/tree_sitter-0.21.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84eedb06615461b9e2847be7c47b9c5f2195d7d66d31b33c0a227eff4e0a0199", size = 496711 }, { url = "https://files.pythonhosted.org/packages/88/68/c916a2669ff92f8e66d519d5df20d36e0eac0dd178a77169ae8c8a7c3e08/tree_sitter-0.21.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9d33ea425df8c3d6436926fe2991429d59c335431bf4e3c71e77c17eb508be5a", size = 481965 }, { url = "https://files.pythonhosted.org/packages/42/6e/b8bf5f75fc6485a429e2b6f71aaed2666dba483b875fbd76a7b007f85073/tree_sitter-0.21.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fae1ee0ff6d85e2fd5cd8ceb9fe4af4012220ee1e4cbe813305a316caf7a6f63", size = 492782 }, { url = "https://files.pythonhosted.org/packages/16/bf/c6ee8d9287846912f427e1d8f1f7266e612a1d6ba161517c793e83f620cf/tree_sitter-0.21.3-cp310-cp310-win_amd64.whl", hash = "sha256:bb41be86a987391f9970571aebe005ccd10222f39c25efd15826583c761a37e5", size = 109732 }, { url = "https://files.pythonhosted.org/packages/63/b5/72657d5874d7f0a722c0288f04e5e2bc33d7715b13a858885b6593047dce/tree_sitter-0.21.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54b22c3c2aab3e3639a4b255d9df8455da2921d050c4829b6a5663b057f10db5", size = 133429 }, { url = "https://files.pythonhosted.org/packages/d3/64/c5d397efbb6d0dbed4254cd2ca389ed186a2e1e7e32661059f6eeaaf6424/tree_sitter-0.21.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab6e88c1e2d5e84ff0f9e5cd83f21b8e5074ad292a2cf19df3ba31d94fbcecd4", size = 126088 }, { url = "https://files.pythonhosted.org/packages/ba/88/941669acc140f94e6c6196d6d8676ac4cd57c3b3fbc1ee61bb11c1b2da71/tree_sitter-0.21.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3fd34ed4cd5db445bc448361b5da46a2a781c648328dc5879d768f16a46771", size = 487879 }, { url = "https://files.pythonhosted.org/packages/29/4e/798154f2846d620bf9fa3bc244e056d4858f2108f834656bf9f1219d4f30/tree_sitter-0.21.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fabc7182f6083269ce3cfcad202fe01516aa80df64573b390af6cd853e8444a1", size = 498776 }, { url = "https://files.pythonhosted.org/packages/6e/d1/05ea77487bc7a3946d0e80fb6c5cb61515953f5e7a4f6804b98e113ed4b0/tree_sitter-0.21.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f874c3f7d2a2faf5c91982dc7d88ff2a8f183a21fe475c29bee3009773b0558", size = 483348 }, { url = "https://files.pythonhosted.org/packages/42/fa/bf938e7c6afbc368d503deeda060891c3dba57e2d1166e4b884271f55616/tree_sitter-0.21.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ee61ee3b7a4eedf9d8f1635c68ba4a6fa8c46929601fc48a907c6cfef0cfbcb2", size = 493757 }, { url = "https://files.pythonhosted.org/packages/1d/a7/98da36a6eab22f5729989c9e0137b1b04cbe368d1e024fccd72c0b00719b/tree_sitter-0.21.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b7256c723642de1c05fbb776b27742204a2382e337af22f4d9e279d77df7aa2", size = 109735 }, { url = "https://files.pythonhosted.org/packages/81/e1/cceb06eae617a6bf5eeeefa9813d9fd57d89b50f526ce02486a336bcd2a9/tree_sitter-0.21.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:669b3e5a52cb1e37d60c7b16cc2221c76520445bb4f12dd17fd7220217f5abf3", size = 133640 }, { url = "https://files.pythonhosted.org/packages/f6/ce/ac14e5cbb0f30b7bd338122491ee2b8e6c0408cfe26741cbd66fa9b53d35/tree_sitter-0.21.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2aa2a5099a9f667730ff26d57533cc893d766667f4d8a9877e76a9e74f48f0d3", size = 125954 }, { url = "https://files.pythonhosted.org/packages/c2/df/76dbf830126e566c48db0d1bf2bef3f9d8cac938302a9b0f762ded8206c2/tree_sitter-0.21.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3e06ae2a517cf6f1abb682974f76fa760298e6d5a3ecf2cf140c70f898adf0", size = 490092 }, { url = "https://files.pythonhosted.org/packages/ec/87/0c3593552cb0d09ab6271d37fc0e6a9476919d2a975661d709d4b3289fc7/tree_sitter-0.21.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af992dfe08b4fefcfcdb40548d0d26d5d2e0a0f2d833487372f3728cd0772b48", size = 502155 }, { url = "https://files.pythonhosted.org/packages/05/92/b2cb22cf52c18fcc95662897f380cf230c443dfc9196b872aad5948b7bb3/tree_sitter-0.21.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c7cbab1dd9765138505c4a55e2aa857575bac4f1f8a8b0457744a4fefa1288e6", size = 486020 }, { url = "https://files.pythonhosted.org/packages/4a/ea/69b543538a46d763f3e787234d1617b718ab90f32ffa676ca856f1d9540e/tree_sitter-0.21.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1e66aeb457d1529370fcb0997ae5584c6879e0e662f1b11b2f295ea57e22f54", size = 496348 }, { url = "https://files.pythonhosted.org/packages/eb/4f/df4ea84476443021707b537217c32147ccccbc3e10c17b216a969991e1b3/tree_sitter-0.21.3-cp312-cp312-win_amd64.whl", hash = "sha256:013c750252dc3bd0e069d82e9658de35ed50eecf31c6586d0de7f942546824c5", size = 109771 }, { url = "https://files.pythonhosted.org/packages/a9/08/3a2faaca576eca4ba9d25ff54f5e3c9fe7bdde7b748208dad7c033106f77/tree_sitter-0.21.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4986a8cb4acebd168474ec2e5db440e59c7888819b3449a43ce8b17ed0331b07", size = 133680 }, { url = "https://files.pythonhosted.org/packages/d4/9c/0465da56c8ca82a366d08f62be21df61f86116fd63dd70fb9bb94c67b21c/tree_sitter-0.21.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6e217fee2e7be7dbce4496caa3d1c466977d7e81277b677f954d3c90e3272ec2", size = 126286 }, { url = "https://files.pythonhosted.org/packages/81/aa/28c6e2ee7506b6627ba5a35dcfa9042043a32d14cecb5bc7d8fa6f0a1441/tree_sitter-0.21.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f32a88afff4f2bc0f20632b0a2aa35fa9ae7d518f083409eca253518e0950929", size = 493104 }, { url = "https://files.pythonhosted.org/packages/ec/19/5e964315acf83dc84ddf7d1abadda3d7950a743247cb331c6dea0664fd9f/tree_sitter-0.21.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3652ac9e47cdddf213c5d5d6854194469097e62f7181c0a9aa8435449a163a9", size = 503895 }, { url = "https://files.pythonhosted.org/packages/55/fe/bb1ef3feb6e11ff7feefe423fbb833ede7b42f20dd7742cfa7b163d6127a/tree_sitter-0.21.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:60b4df3298ff467bc01e2c0f6c2fb43aca088038202304bf8e41edd9fa348f45", size = 490926 }, { url = "https://files.pythonhosted.org/packages/b8/05/dffdba16b6d2295c24e51a812481fe27907213415b306f99b84c1b518506/tree_sitter-0.21.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:00e4d0c99dff595398ef5e88a1b1ddd53adb13233fb677c1fd8e497fb2361629", size = 499248 }, { url = "https://files.pythonhosted.org/packages/cf/67/8a83076e07e57f809bf308973aac5c21f8a2e5757ddd3429a14986a76405/tree_sitter-0.21.3-cp38-cp38-win_amd64.whl", hash = "sha256:50c91353a26946e4dd6779837ecaf8aa123aafa2d3209f261ab5280daf0962f5", size = 110055 }, { url = "https://files.pythonhosted.org/packages/29/ea/2f848cf720dce4835388f30dca377bee9054461c51cdd0340c82e2d2fe32/tree_sitter-0.21.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b17b8648b296ccc21a88d72ca054b809ee82d4b14483e419474e7216240ea278", size = 133664 }, { url = "https://files.pythonhosted.org/packages/9e/33/db09a0b0f7ca96bbf53e2a5c024bc1f0a138babb2f4472b4954627778d79/tree_sitter-0.21.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f2f057fd01d3a95cbce6794c6e9f6db3d376cb3bb14e5b0528d77f0ec21d6478", size = 126419 }, { url = "https://files.pythonhosted.org/packages/4f/22/e01759e2303dc9db719f9985d6e980ddd24ac2a9136d5d06ee7c7e13f25a/tree_sitter-0.21.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:839759de30230ffd60687edbb119b31521d5ac016749358e5285816798bb804a", size = 487811 }, { url = "https://files.pythonhosted.org/packages/0a/5d/afef7068b7eb3f5b50b4c0ae0fff43511f37263876dbb487158b22f89bfb/tree_sitter-0.21.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df40aa29cb7e323898194246df7a03b9676955a0ac1f6bce06bc4903a70b5f7", size = 498473 }, { url = "https://files.pythonhosted.org/packages/44/f1/18534c57d8e0bbaae6fe0d00d7e4ae3394b0be0b2b94b6aba370cdbd3be6/tree_sitter-0.21.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1d9be27dde007b569fa78ff9af5fe40d2532c998add9997a9729e348bb78fa59", size = 484418 }, { url = "https://files.pythonhosted.org/packages/b0/57/f2b16d902e808ccf99740da2cf199043e12783692fe631e857259fbe279f/tree_sitter-0.21.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c4ac87735e6f98fe085244c7c020f0177d13d4c117db72ba041faa980d25d69d", size = 494421 }, { url = "https://files.pythonhosted.org/packages/44/cc/ceef3adeeb998578eaf199632b64e34a6cb2f8186e3856c9796d4671a84e/tree_sitter-0.21.3-cp39-cp39-win_amd64.whl", hash = "sha256:fbbd137f7d9a5309fb4cb82e2c3250ba101b0dd08a8abdce815661e6cf2cbc19", size = 110032 }, ] [[package]] name = "trio" version = "0.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "cffi", marker = "(python_full_version < '3.13' and implementation_name != 'pypy' and os_name == 'nt') or (implementation_name != 'pypy' and os_name == 'nt' and sys_platform != 'linux')" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "outcome" }, { name = "sniffio" }, { name = "sortedcontainers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/17/d1/a83dee5be404da7afe5a71783a33b8907bacb935a6dc8c69ab785e4a3eed/trio-0.27.0.tar.gz", hash = "sha256:1dcc95ab1726b2da054afea8fd761af74bad79bd52381b84eae408e983c76831", size = 568064 } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/83/ec3196c360afffbc5b342ead48d1eb7393dd74fa70bca75d33905a86f211/trio-0.27.0-py3-none-any.whl", hash = "sha256:68eabbcf8f457d925df62da780eff15ff5dc68fd6b367e2dde59f7aaf2a0b884", size = 481734 }, ] [[package]] name = "twisted" version = "24.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "automat" }, { name = "constantly" }, { name = "hyperlink" }, { name = "incremental" }, { name = "typing-extensions" }, { name = "zope-interface" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/0f/2d0b0dcd52a849db64ff63619aead94ae1091fe4d4d7e100371efe513585/twisted-24.10.0.tar.gz", hash = "sha256:02951299672595fea0f70fa2d5f7b5e3d56836157eda68859a6ad6492d36756e", size = 3525999 } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/7c/f80f6853d702782edb357190c42c3973f13c547a5f68ab1b17e6415061b8/twisted-24.10.0-py3-none-any.whl", hash = "sha256:67aa7c8aa94387385302acf44ade12967c747858c8bcce0f11d38077a11c5326", size = 3188753 }, ] [package.optional-dependencies] tls = [ { name = "idna" }, { name = "pyopenssl" }, { name = "service-identity" }, ] [[package]] name = "txaio" version = "23.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/51/91/bc9fd5aa84703f874dea27313b11fde505d343f3ef3ad702bddbe20bfd6e/txaio-23.1.1.tar.gz", hash = "sha256:f9a9216e976e5e3246dfd112ad7ad55ca915606b60b84a757ac769bd404ff704", size = 53704 } wheels = [ { url = "https://files.pythonhosted.org/packages/7d/6c/a53cc9a97c2da76d9cd83c03f377468599a28f2d4ad9fc71c3b99640e71e/txaio-23.1.1-py2.py3-none-any.whl", hash = "sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490", size = 30512 }, ] [[package]] name = "types-beautifulsoup4" version = "4.12.0.20241020" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-html5lib" }, ] sdist = { url = "https://files.pythonhosted.org/packages/45/ae/5a7571c649cdd9f3c07d16790467a4fe1191f12a3ad7eecd1097cb8b1d9f/types-beautifulsoup4-4.12.0.20241020.tar.gz", hash = "sha256:158370d08d0cd448bd11b132a50ff5279237a5d4b5837beba074de152a513059", size = 11682 } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/43/0f96cdf27d7da7dea729af3476b7be997205765209651a42a4e1895bab72/types_beautifulsoup4-4.12.0.20241020-py3-none-any.whl", hash = "sha256:c95e66ce15a4f5f0835f7fbc5cd886321ae8294f977c495424eaf4225307fd30", size = 12170 }, ] [[package]] name = "types-cffi" version = "1.16.0.20240331" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-setuptools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/91/c8/81e5699160b91f0f91eea852d84035c412bfb4b3a29389701044400ab379/types-cffi-1.16.0.20240331.tar.gz", hash = "sha256:b8b20d23a2b89cfed5f8c5bc53b0cb8677c3aac6d970dbc771e28b9c698f5dee", size = 11318 } wheels = [ { url = "https://files.pythonhosted.org/packages/69/7a/98f5d2493a652cec05d3b09be59202d202004a41fca9c70d224782611365/types_cffi-1.16.0.20240331-py3-none-any.whl", hash = "sha256:a363e5ea54a4eb6a4a105d800685fde596bc318089b025b27dee09849fe41ff0", size = 14550 }, ] [[package]] name = "types-html5lib" version = "1.1.11.20241018" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b6/9d/f6fbcc8246f5e46845b4f989c4e17e6fb3ce572f7065b185e515bf8a3be7/types-html5lib-1.1.11.20241018.tar.gz", hash = "sha256:98042555ff78d9e3a51c77c918b1041acbb7eb6c405408d8a9e150ff5beccafa", size = 11370 } wheels = [ { url = "https://files.pythonhosted.org/packages/ba/7c/f862b1dc31268ef10fe95b43dcdf216ba21a592fafa2d124445cd6b92e93/types_html5lib-1.1.11.20241018-py3-none-any.whl", hash = "sha256:3f1e064d9ed2c289001ae6392c84c93833abb0816165c6ff0abfc304a779f403", size = 17292 }, ] [[package]] name = "types-psutil" version = "6.1.0.20241102" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6d/03/015b10b717747922457b54ecd3e2ac3b174d87667b74108f66ccf1c75636/types-psutil-6.1.0.20241102.tar.gz", hash = "sha256:8cbe086b9c29f5c0aa55c4422498c07a8e506f096205761dba088905198551dc", size = 15447 } wheels = [ { url = "https://files.pythonhosted.org/packages/4d/93/b84f9e33febb4745206882c24434d67dc04115ac04e61132c337c797a65f/types_psutil-6.1.0.20241102-py3-none-any.whl", hash = "sha256:61f836f8ba48f28f0d290d3bcd902f9130ce5057a1676e6ecbefb6141e2743f4", size = 18653 }, ] [[package]] name = "types-pyopenssl" version = "24.1.0.20240722" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "types-cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458 } wheels = [ { url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499 }, ] [[package]] name = "types-pyyaml" version = "6.0.12.20240917" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/92/7d/a95df0a11f95c8f48d7683f03e4aed1a2c0fc73e9de15cca4d38034bea1a/types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587", size = 12381 } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/2c/c1d81d680997d24b0542aa336f0a65bd7835e5224b7670f33a7d617da379/types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570", size = 15264 }, ] [[package]] name = "types-redis" version = "4.6.0.20241004" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "types-pyopenssl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/95/c054d3ac940e8bac4ca216470c80c26688a0e79e09f520a942bb27da3386/types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", size = 49679 } wheels = [ { url = "https://files.pythonhosted.org/packages/55/82/7d25dce10aad92d2226b269bce2f85cfd843b4477cd50245d7d40ecf8f89/types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed", size = 58737 }, ] [[package]] name = "types-setuptools" version = "75.6.0.20241126" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c2/d2/15ede73bc3faf647af2c7bfefa90dde563a4b6bb580b1199f6255463c272/types_setuptools-75.6.0.20241126.tar.gz", hash = "sha256:7bf25ad4be39740e469f9268b6beddda6e088891fa5a27e985c6ce68bf62ace0", size = 48569 } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/a0/898a1363592d372d4103b76b7c723d84fcbde5fa4ed0c3a29102805ed7db/types_setuptools-75.6.0.20241126-py3-none-any.whl", hash = "sha256:aaae310a0e27033c1da8457d4d26ac673b0c8a0de7272d6d4708e263f2ea3b9b", size = 72732 }, ] [[package]] name = "typing-extensions" version = "4.12.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] [[package]] name = "tzdata" version = "2024.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 } wheels = [ { url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 }, ] [[package]] name = "urllib3" version = "2.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } wheels = [ { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, ] [[package]] name = "uvicorn" version = "0.32.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6a/3c/21dba3e7d76138725ef307e3d7ddd29b763119b3aa459d02cc05fefcff75/uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175", size = 77630 } wheels = [ { url = "https://files.pythonhosted.org/packages/50/c1/2d27b0a15826c2b71dcf6e2f5402181ef85acf439617bb2f1453125ce1f3/uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", size = 63828 }, ] [package.optional-dependencies] standard = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "httptools" }, { name = "python-dotenv" }, { name = "pyyaml" }, { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, { name = "watchfiles" }, { name = "websockets" }, ] [[package]] name = "uvloop" version = "0.21.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } wheels = [ { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019 }, { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898 }, { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735 }, { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126 }, { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789 }, { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523 }, { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, { url = "https://files.pythonhosted.org/packages/b5/7b/85a2c8231eac451ef9caecba8715295820c9f94fb51c4f5b2e39c79a5c11/uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414", size = 1433814 }, { url = "https://files.pythonhosted.org/packages/78/c9/10272e791562be6cfc4ee127883087de6443fede8f010b019ca0fdf841c1/uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206", size = 797954 }, { url = "https://files.pythonhosted.org/packages/62/23/29da7a6d3fba8dfe375ea48a8c3a3e5562b770d24008d79a7a6e0150d7c1/uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe", size = 4302867 }, { url = "https://files.pythonhosted.org/packages/9a/46/72fb3fbb457cd68632542ecc7fa191a17dac501f70b7f3786a18912bbe0e/uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79", size = 4303228 }, { url = "https://files.pythonhosted.org/packages/1a/80/3f57f2458460501b709aec7c7e7f303b81b38ca35f786b41bf402b3349e8/uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a", size = 4163776 }, { url = "https://files.pythonhosted.org/packages/4f/9f/07c88dd3e76171e7808ff63719af12ee8bb6ea56fe40ea274da606ae5ade/uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc", size = 4292873 }, { url = "https://files.pythonhosted.org/packages/3c/a4/646a9d0edff7cde25fc1734695d3dfcee0501140dd0e723e4df3f0a50acb/uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b", size = 1439646 }, { url = "https://files.pythonhosted.org/packages/01/2e/e128c66106af9728f86ebfeeb52af27ecd3cb09336f3e2f3e06053707a15/uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2", size = 800931 }, { url = "https://files.pythonhosted.org/packages/2d/1a/9fbc2b1543d0df11f7aed1632f64bdf5ecc4053cf98cdc9edb91a65494f9/uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0", size = 3829660 }, { url = "https://files.pythonhosted.org/packages/b8/c0/392e235e4100ae3b95b5c6dac77f82b529d2760942b1e7e0981e5d8e895d/uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75", size = 3827185 }, { url = "https://files.pythonhosted.org/packages/e1/24/a5da6aba58f99aed5255eca87d58d1760853e8302d390820cc29058408e3/uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd", size = 3705833 }, { url = "https://files.pythonhosted.org/packages/1a/5c/6ba221bb60f1e6474474102e17e38612ec7a06dc320e22b687ab563d877f/uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff", size = 3804696 }, ] [[package]] name = "valkey" version = "6.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/45/f7/b552b7a67017e6233cd8a3b783ce8c4b548e29df98daedd7fb4c4c2cc8f8/valkey-6.0.2.tar.gz", hash = "sha256:dc2e91512b82d1da0b91ab0cdbd8c97c0c0250281728cb32f9398760df9caeae", size = 4602149 } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/cb/b1eac0fe9cbdbba0a5cf189f5778fe54ba7d7c9f26c2f62ca8d759b38f52/valkey-6.0.2-py3-none-any.whl", hash = "sha256:dbbdd65439ee0dc5689502c54f1899504cc7268e85cb7fe8935f062178ff5805", size = 260101 }, ] [package.optional-dependencies] libvalkey = [ { name = "libvalkey" }, ] [[package]] name = "virtualenv" version = "20.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bf/75/53316a5a8050069228a2f6d11f32046cfa94fbb6cc3f08703f59b873de2e/virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa", size = 7650368 } wheels = [ { url = "https://files.pythonhosted.org/packages/10/f9/0919cf6f1432a8c4baa62511f8f8da8225432d22e83e3476f5be1a1edc6e/virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0", size = 4276702 }, ] [[package]] name = "watchfiles" version = "0.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "anyio", version = "4.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c8/27/2ba23c8cc85796e2d41976439b08d52f691655fdb9401362099502d1f0cf/watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", size = 37870 } wheels = [ { url = "https://files.pythonhosted.org/packages/89/a1/631c12626378b9f1538664aa221feb5c60dfafbd7f60b451f8d0bdbcdedd/watchfiles-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0", size = 375096 }, { url = "https://files.pythonhosted.org/packages/f7/5c/f27c979c8a10aaa2822286c1bffdce3db731cd1aa4224b9f86623e94bbfe/watchfiles-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c", size = 367425 }, { url = "https://files.pythonhosted.org/packages/74/0d/1889e5649885484d29f6c792ef274454d0a26b20d6ed5fdba5409335ccb6/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361", size = 437705 }, { url = "https://files.pythonhosted.org/packages/85/8a/01d9a22e839f0d1d547af11b1fcac6ba6f889513f1b2e6f221d9d60d9585/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3", size = 433636 }, { url = "https://files.pythonhosted.org/packages/62/32/a93db78d340c7ef86cde469deb20e36c6b2a873edee81f610e94bbba4e06/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571", size = 451069 }, { url = "https://files.pythonhosted.org/packages/99/c2/e9e2754fae3c2721c9a7736f92dab73723f1968ed72535fff29e70776008/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd", size = 469306 }, { url = "https://files.pythonhosted.org/packages/4c/45/f317d9e3affb06c3c27c478de99f7110143e87f0f001f0f72e18d0e1ddce/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a", size = 476187 }, { url = "https://files.pythonhosted.org/packages/ac/d3/f1f37248abe0114916921e638f71c7d21fe77e3f2f61750e8057d0b68ef2/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e", size = 425743 }, { url = "https://files.pythonhosted.org/packages/2b/e8/c7037ea38d838fd81a59cd25761f106ee3ef2cfd3261787bee0c68908171/watchfiles-0.24.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c", size = 612327 }, { url = "https://files.pythonhosted.org/packages/a0/c5/0e6e228aafe01a7995fbfd2a4edb221bb11a2744803b65a5663fb85e5063/watchfiles-0.24.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188", size = 595096 }, { url = "https://files.pythonhosted.org/packages/63/d5/4780e8bf3de3b4b46e7428a29654f7dc041cad6b19fd86d083e4b6f64bbe/watchfiles-0.24.0-cp310-none-win32.whl", hash = "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735", size = 264149 }, { url = "https://files.pythonhosted.org/packages/fe/1b/5148898ba55fc9c111a2a4a5fb67ad3fa7eb2b3d7f0618241ed88749313d/watchfiles-0.24.0-cp310-none-win_amd64.whl", hash = "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04", size = 277542 }, { url = "https://files.pythonhosted.org/packages/85/02/366ae902cd81ca5befcd1854b5c7477b378f68861597cef854bd6dc69fbe/watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", size = 375579 }, { url = "https://files.pythonhosted.org/packages/bc/67/d8c9d256791fe312fea118a8a051411337c948101a24586e2df237507976/watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", size = 367726 }, { url = "https://files.pythonhosted.org/packages/b1/dc/a8427b21ef46386adf824a9fec4be9d16a475b850616cfd98cf09a97a2ef/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", size = 437735 }, { url = "https://files.pythonhosted.org/packages/3a/21/0b20bef581a9fbfef290a822c8be645432ceb05fb0741bf3c032e0d90d9a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", size = 433644 }, { url = "https://files.pythonhosted.org/packages/1c/e8/d5e5f71cc443c85a72e70b24269a30e529227986096abe091040d6358ea9/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", size = 450928 }, { url = "https://files.pythonhosted.org/packages/61/ee/bf17f5a370c2fcff49e1fec987a6a43fd798d8427ea754ce45b38f9e117a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", size = 469072 }, { url = "https://files.pythonhosted.org/packages/a3/34/03b66d425986de3fc6077e74a74c78da298f8cb598887f664a4485e55543/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", size = 475517 }, { url = "https://files.pythonhosted.org/packages/70/eb/82f089c4f44b3171ad87a1b433abb4696f18eb67292909630d886e073abe/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", size = 425480 }, { url = "https://files.pythonhosted.org/packages/53/20/20509c8f5291e14e8a13104b1808cd7cf5c44acd5feaecb427a49d387774/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", size = 612322 }, { url = "https://files.pythonhosted.org/packages/df/2b/5f65014a8cecc0a120f5587722068a975a692cadbe9fe4ea56b3d8e43f14/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", size = 595094 }, { url = "https://files.pythonhosted.org/packages/18/98/006d8043a82c0a09d282d669c88e587b3a05cabdd7f4900e402250a249ac/watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", size = 264191 }, { url = "https://files.pythonhosted.org/packages/8a/8b/badd9247d6ec25f5f634a9b3d0d92e39c045824ec7e8afcedca8ee52c1e2/watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", size = 277527 }, { url = "https://files.pythonhosted.org/packages/af/19/35c957c84ee69d904299a38bae3614f7cede45f07f174f6d5a2f4dbd6033/watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", size = 266253 }, { url = "https://files.pythonhosted.org/packages/35/82/92a7bb6dc82d183e304a5f84ae5437b59ee72d48cee805a9adda2488b237/watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", size = 374137 }, { url = "https://files.pythonhosted.org/packages/87/91/49e9a497ddaf4da5e3802d51ed67ff33024597c28f652b8ab1e7c0f5718b/watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", size = 367733 }, { url = "https://files.pythonhosted.org/packages/0d/d8/90eb950ab4998effea2df4cf3a705dc594f6bc501c5a353073aa990be965/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", size = 437322 }, { url = "https://files.pythonhosted.org/packages/6c/a2/300b22e7bc2a222dd91fce121cefa7b49aa0d26a627b2777e7bdfcf1110b/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", size = 433409 }, { url = "https://files.pythonhosted.org/packages/99/44/27d7708a43538ed6c26708bcccdde757da8b7efb93f4871d4cc39cffa1cc/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", size = 452142 }, { url = "https://files.pythonhosted.org/packages/b0/ec/c4e04f755be003129a2c5f3520d2c47026f00da5ecb9ef1e4f9449637571/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", size = 469414 }, { url = "https://files.pythonhosted.org/packages/c5/4e/cdd7de3e7ac6432b0abf282ec4c1a1a2ec62dfe423cf269b86861667752d/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", size = 472962 }, { url = "https://files.pythonhosted.org/packages/27/69/e1da9d34da7fc59db358424f5d89a56aaafe09f6961b64e36457a80a7194/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", size = 425705 }, { url = "https://files.pythonhosted.org/packages/e8/c1/24d0f7357be89be4a43e0a656259676ea3d7a074901f47022f32e2957798/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", size = 612851 }, { url = "https://files.pythonhosted.org/packages/c7/af/175ba9b268dec56f821639c9893b506c69fd999fe6a2e2c51de420eb2f01/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", size = 594868 }, { url = "https://files.pythonhosted.org/packages/44/81/1f701323a9f70805bc81c74c990137123344a80ea23ab9504a99492907f8/watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", size = 264109 }, { url = "https://files.pythonhosted.org/packages/b4/0b/32cde5bc2ebd9f351be326837c61bdeb05ad652b793f25c91cac0b48a60b/watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", size = 277055 }, { url = "https://files.pythonhosted.org/packages/4b/81/daade76ce33d21dbec7a15afd7479de8db786e5f7b7d249263b4ea174e08/watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", size = 266169 }, { url = "https://files.pythonhosted.org/packages/30/dc/6e9f5447ae14f645532468a84323a942996d74d5e817837a5c8ce9d16c69/watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", size = 373764 }, { url = "https://files.pythonhosted.org/packages/79/c0/c3a9929c372816c7fc87d8149bd722608ea58dc0986d3ef7564c79ad7112/watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", size = 367873 }, { url = "https://files.pythonhosted.org/packages/2e/11/ff9a4445a7cfc1c98caf99042df38964af12eed47d496dd5d0d90417349f/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", size = 438381 }, { url = "https://files.pythonhosted.org/packages/48/a3/763ba18c98211d7bb6c0f417b2d7946d346cdc359d585cc28a17b48e964b/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", size = 432809 }, { url = "https://files.pythonhosted.org/packages/30/4c/616c111b9d40eea2547489abaf4ffc84511e86888a166d3a4522c2ba44b5/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", size = 451801 }, { url = "https://files.pythonhosted.org/packages/b6/be/d7da83307863a422abbfeb12903a76e43200c90ebe5d6afd6a59d158edea/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", size = 468886 }, { url = "https://files.pythonhosted.org/packages/1d/d3/3dfe131ee59d5e90b932cf56aba5c996309d94dafe3d02d204364c23461c/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", size = 472973 }, { url = "https://files.pythonhosted.org/packages/42/6c/279288cc5653a289290d183b60a6d80e05f439d5bfdfaf2d113738d0f932/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", size = 425282 }, { url = "https://files.pythonhosted.org/packages/d6/d7/58afe5e85217e845edf26d8780c2d2d2ae77675eeb8d1b8b8121d799ce52/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", size = 612540 }, { url = "https://files.pythonhosted.org/packages/6d/d5/b96eeb9fe3fda137200dd2f31553670cbc731b1e13164fd69b49870b76ec/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", size = 593625 }, { url = "https://files.pythonhosted.org/packages/c1/e5/c326fe52ee0054107267608d8cea275e80be4455b6079491dfd9da29f46f/watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", size = 263899 }, { url = "https://files.pythonhosted.org/packages/a6/8b/8a7755c5e7221bb35fe4af2dc44db9174f90ebf0344fd5e9b1e8b42d381e/watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", size = 276622 }, { url = "https://files.pythonhosted.org/packages/17/1c/c0b5f4347011b60e2dbde671a5050944f3aaf0eb2ffc0fb5c7adf2516229/watchfiles-0.24.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318", size = 375982 }, { url = "https://files.pythonhosted.org/packages/c5/b2/d417b982be5ace395f1aad32cd8e0dcf194e431dfbfeee88941b6da6735a/watchfiles-0.24.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05", size = 369757 }, { url = "https://files.pythonhosted.org/packages/68/27/f3a1147af79085da95a879d7e6f953380da17a90b50d990749ae287550ca/watchfiles-0.24.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c", size = 439397 }, { url = "https://files.pythonhosted.org/packages/31/de/4a677766880efee555cc56a4c6bf6993a7748901243cd2511419acc37a6c/watchfiles-0.24.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83", size = 433878 }, { url = "https://files.pythonhosted.org/packages/f4/7f/30fbf661dea01cf3d5ab4962ee4b52854b9fbbd7fe4918bc60c212bb5b60/watchfiles-0.24.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c", size = 451495 }, { url = "https://files.pythonhosted.org/packages/c8/65/cda4b9ed13087d57f78ed386c4bdc2369c114dd871a32fa6e2419f6e81b8/watchfiles-0.24.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b", size = 470115 }, { url = "https://files.pythonhosted.org/packages/4c/72/9b2ba3bb3a7233fb3d21900cd3f9005cfaa53884f496239541ec885b9861/watchfiles-0.24.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b", size = 476814 }, { url = "https://files.pythonhosted.org/packages/3d/78/90a881916a4a3bafd5e82202d51406444d3720cfc03b326427a8e455d89d/watchfiles-0.24.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91", size = 426747 }, { url = "https://files.pythonhosted.org/packages/56/52/5a2c6e0694013a53f596d4a66642de48dc1a53a635ad61bc6504abf5c87c/watchfiles-0.24.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b", size = 613135 }, { url = "https://files.pythonhosted.org/packages/fc/dc/8f177e6074e756d961d5dcb5ef65528b02ab09028d0380db4579a30af78f/watchfiles-0.24.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22", size = 596318 }, { url = "https://files.pythonhosted.org/packages/e6/16/d89e06188ed6672dc69eeef7af2ea158328bd59d45032a94daaaed2a6516/watchfiles-0.24.0-cp38-none-win32.whl", hash = "sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1", size = 264384 }, { url = "https://files.pythonhosted.org/packages/e6/cc/2f2f27fc403193bedaaae5485cd04fd31064f5cdec885162bd0e390be68a/watchfiles-0.24.0-cp38-none-win_amd64.whl", hash = "sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1", size = 277740 }, { url = "https://files.pythonhosted.org/packages/93/90/15b3b1cc19799c217e4369ca15dbf9ba1d0f821d61b27173a54800002478/watchfiles-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886", size = 375920 }, { url = "https://files.pythonhosted.org/packages/74/e2/ec7766a5b20ba5e37397ef8d32ff39a90daf7e4677410efe077c386338b6/watchfiles-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f", size = 369382 }, { url = "https://files.pythonhosted.org/packages/a2/82/915b3a3295292f860181dc9c7d922834ac7265c8915052d396d40ccf4617/watchfiles-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855", size = 438556 }, { url = "https://files.pythonhosted.org/packages/9b/76/750eab8e7baecedca05e712b9571ac5eb240e494e8d4c78b359da2939619/watchfiles-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b", size = 433677 }, { url = "https://files.pythonhosted.org/packages/1a/69/7c55142bafcdad9fec58433b7c1f162b59c48f0a3732a9441252147b13c7/watchfiles-0.24.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430", size = 451845 }, { url = "https://files.pythonhosted.org/packages/d7/a6/124b0043a8936adf96fffa1d73217b205cc254b4a5a313b0a6ea33a44b7b/watchfiles-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3", size = 469931 }, { url = "https://files.pythonhosted.org/packages/7a/14/29ffa6c7a695fb46a7ff835a5524976dbc07577215647fb35a61ea099c75/watchfiles-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a", size = 476577 }, { url = "https://files.pythonhosted.org/packages/9d/8b/fea47dd852c644bd933108877293d0a1c56953c584e8b971530a9042c153/watchfiles-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9", size = 426432 }, { url = "https://files.pythonhosted.org/packages/0e/11/cfa073f1d9fa18867c2b4220ba445044fd48101ac481f8cbfea1c208ea88/watchfiles-0.24.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca", size = 613173 }, { url = "https://files.pythonhosted.org/packages/77/44/05c8959304f96fbcd68b6c131c59df7bd3d7f0c2a7410324b7f63b1f9fe6/watchfiles-0.24.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e", size = 596184 }, { url = "https://files.pythonhosted.org/packages/9b/85/033ecdb5eccb77770d6f24f9fa055067ffa962313a1383645afc711a3cd8/watchfiles-0.24.0-cp39-none-win32.whl", hash = "sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da", size = 264345 }, { url = "https://files.pythonhosted.org/packages/16/6e/5ded97365346eceaf7fa32d4e2d16f4f97b11d648026b2903c2528c544f8/watchfiles-0.24.0-cp39-none-win_amd64.whl", hash = "sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f", size = 277760 }, { url = "https://files.pythonhosted.org/packages/df/94/1ad200e937ec91b2a9d6b39ae1cf9c2b1a9cc88d5ceb43aa5c6962eb3c11/watchfiles-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f", size = 376986 }, { url = "https://files.pythonhosted.org/packages/ee/fd/d9e020d687ccf90fe95efc513fbb39a8049cf5a3ff51f53c59fcf4c47a5d/watchfiles-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b", size = 369445 }, { url = "https://files.pythonhosted.org/packages/43/cb/c0279b35053555d10ef03559c5aebfcb0c703d9c70a7b4e532df74b9b0e8/watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4", size = 439383 }, { url = "https://files.pythonhosted.org/packages/8b/c4/08b3c2cda45db5169148a981c2100c744a4a222fa7ae7644937c0c002069/watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a", size = 426804 }, { url = "https://files.pythonhosted.org/packages/02/81/9c9a1e6a83d3c320d2f89eca2b71854ae727eca8ab298ad5da00e36c69e2/watchfiles-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be", size = 377076 }, { url = "https://files.pythonhosted.org/packages/f1/05/9ef4158f15c417a420d907a6eb887c81953565d0268262195766a844a6d9/watchfiles-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5", size = 371717 }, { url = "https://files.pythonhosted.org/packages/81/1b/8d036da7a9e4d490385b6979804229fb7ac9b591e4d84f159edf2b3f7cc2/watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777", size = 440973 }, { url = "https://files.pythonhosted.org/packages/fb/fc/885015d4a17ada85508e406c10d638808e7bfbb5622a2e342c868ede18c0/watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e", size = 428343 }, ] [[package]] name = "webencodings" version = "0.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 }, ] [[package]] name = "websockets" version = "13.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549 } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/94/d15dbfc6a5eb636dbc754303fba18208f2e88cf97e733e1d64fb9cb5c89e/websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee", size = 157815 }, { url = "https://files.pythonhosted.org/packages/30/02/c04af33f4663945a26f5e8cf561eb140c35452b50af47a83c3fbcfe62ae1/websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7", size = 155466 }, { url = "https://files.pythonhosted.org/packages/35/e8/719f08d12303ea643655e52d9e9851b2dadbb1991d4926d9ce8862efa2f5/websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6", size = 155716 }, { url = "https://files.pythonhosted.org/packages/91/e1/14963ae0252a8925f7434065d25dcd4701d5e281a0b4b460a3b5963d2594/websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b", size = 164806 }, { url = "https://files.pythonhosted.org/packages/ec/fa/ab28441bae5e682a0f7ddf3d03440c0c352f930da419301f4a717f675ef3/websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa", size = 163810 }, { url = "https://files.pythonhosted.org/packages/44/77/dea187bd9d16d4b91566a2832be31f99a40d0f5bfa55eeb638eb2c3bc33d/websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700", size = 164125 }, { url = "https://files.pythonhosted.org/packages/cf/d9/3af14544e83f1437eb684b399e6ba0fa769438e869bf5d83d74bc197fae8/websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c", size = 164532 }, { url = "https://files.pythonhosted.org/packages/1c/8a/6d332eabe7d59dfefe4b8ba6f46c8c5fabb15b71c8a8bc3d2b65de19a7b6/websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0", size = 163948 }, { url = "https://files.pythonhosted.org/packages/1a/91/a0aeadbaf3017467a1ee03f8fb67accdae233fe2d5ad4b038c0a84e357b0/websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f", size = 163898 }, { url = "https://files.pythonhosted.org/packages/71/31/a90fb47c63e0ae605be914b0b969d7c6e6ffe2038cd744798e4b3fbce53b/websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe", size = 158706 }, { url = "https://files.pythonhosted.org/packages/93/ca/9540a9ba80da04dc7f36d790c30cae4252589dbd52ccdc92e75b0be22437/websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a", size = 159141 }, { url = "https://files.pythonhosted.org/packages/b2/f0/cf0b8a30d86b49e267ac84addbebbc7a48a6e7bb7c19db80f62411452311/websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19", size = 157813 }, { url = "https://files.pythonhosted.org/packages/bf/e7/22285852502e33071a8cf0ac814f8988480ec6db4754e067b8b9d0e92498/websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5", size = 155469 }, { url = "https://files.pythonhosted.org/packages/68/d4/c8c7c1e5b40ee03c5cc235955b0fb1ec90e7e37685a5f69229ad4708dcde/websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd", size = 155717 }, { url = "https://files.pythonhosted.org/packages/c9/e4/c50999b9b848b1332b07c7fd8886179ac395cb766fda62725d1539e7bc6c/websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02", size = 165379 }, { url = "https://files.pythonhosted.org/packages/bc/49/4a4ad8c072f18fd79ab127650e47b160571aacfc30b110ee305ba25fffc9/websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7", size = 164376 }, { url = "https://files.pythonhosted.org/packages/af/9b/8c06d425a1d5a74fd764dd793edd02be18cf6fc3b1ccd1f29244ba132dc0/websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096", size = 164753 }, { url = "https://files.pythonhosted.org/packages/d5/5b/0acb5815095ff800b579ffc38b13ab1b915b317915023748812d24e0c1ac/websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084", size = 165051 }, { url = "https://files.pythonhosted.org/packages/30/93/c3891c20114eacb1af09dedfcc620c65c397f4fd80a7009cd12d9457f7f5/websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3", size = 164489 }, { url = "https://files.pythonhosted.org/packages/28/09/af9e19885539759efa2e2cd29b8b3f9eecef7ecefea40d46612f12138b36/websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9", size = 164438 }, { url = "https://files.pythonhosted.org/packages/b6/08/6f38b8e625b3d93de731f1d248cc1493327f16cb45b9645b3e791782cff0/websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f", size = 158710 }, { url = "https://files.pythonhosted.org/packages/fb/39/ec8832ecb9bb04a8d318149005ed8cee0ba4e0205835da99e0aa497a091f/websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557", size = 159137 }, { url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821 }, { url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480 }, { url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715 }, { url = "https://files.pythonhosted.org/packages/9f/d0/8612029ea04c5c22bf7af2fd3d63876c4eaeef9b97e86c11972a43aa0e6c/websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0", size = 165647 }, { url = "https://files.pythonhosted.org/packages/56/04/1681ed516fa19ca9083f26d3f3a302257e0911ba75009533ed60fbb7b8d1/websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6", size = 164592 }, { url = "https://files.pythonhosted.org/packages/38/6f/a96417a49c0ed132bb6087e8e39a37db851c70974f5c724a4b2a70066996/websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9", size = 165012 }, { url = "https://files.pythonhosted.org/packages/40/8b/fccf294919a1b37d190e86042e1a907b8f66cff2b61e9befdbce03783e25/websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68", size = 165311 }, { url = "https://files.pythonhosted.org/packages/c1/61/f8615cf7ce5fe538476ab6b4defff52beb7262ff8a73d5ef386322d9761d/websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14", size = 164692 }, { url = "https://files.pythonhosted.org/packages/5c/f1/a29dd6046d3a722d26f182b783a7997d25298873a14028c4760347974ea3/websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", size = 164686 }, { url = "https://files.pythonhosted.org/packages/0f/99/ab1cdb282f7e595391226f03f9b498f52109d25a2ba03832e21614967dfa/websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", size = 158712 }, { url = "https://files.pythonhosted.org/packages/46/93/e19160db48b5581feac8468330aa11b7292880a94a37d7030478596cc14e/websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", size = 159145 }, { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828 }, { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487 }, { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721 }, { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609 }, { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556 }, { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993 }, { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360 }, { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745 }, { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732 }, { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709 }, { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144 }, { url = "https://files.pythonhosted.org/packages/83/69/59872420e5bce60db166d6fba39ee24c719d339fb0ae48cb2ce580129882/websockets-13.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c7934fd0e920e70468e676fe7f1b7261c1efa0d6c037c6722278ca0228ad9d0d", size = 157811 }, { url = "https://files.pythonhosted.org/packages/bb/f7/0610032e0d3981758fdd6ee7c68cc02ebf668a762c5178d3d91748228849/websockets-13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:149e622dc48c10ccc3d2760e5f36753db9cacf3ad7bc7bbbfd7d9c819e286f23", size = 155471 }, { url = "https://files.pythonhosted.org/packages/55/2f/c43173a72ea395263a427a36d25bce2675f41c809424466a13c61a9a2d61/websockets-13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a569eb1b05d72f9bce2ebd28a1ce2054311b66677fcd46cf36204ad23acead8c", size = 155713 }, { url = "https://files.pythonhosted.org/packages/92/7e/8fa930c6426a56c47910792717787640329e4a0e37cdfda20cf89da67126/websockets-13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95df24ca1e1bd93bbca51d94dd049a984609687cb2fb08a7f2c56ac84e9816ea", size = 164995 }, { url = "https://files.pythonhosted.org/packages/27/29/50ed4c68a3f606565a2db4b13948ae7b6f6c53aa9f8f258d92be6698d276/websockets-13.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8dbb1bf0c0a4ae8b40bdc9be7f644e2f3fb4e8a9aca7145bfa510d4a374eeb7", size = 164057 }, { url = "https://files.pythonhosted.org/packages/3c/0e/60da63b1c53c47f389f79312b3356cb305600ffad1274d7ec473128d4e6b/websockets-13.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:035233b7531fb92a76beefcbf479504db8c72eb3bff41da55aecce3a0f729e54", size = 164340 }, { url = "https://files.pythonhosted.org/packages/20/ef/d87c5fc0aa7fafad1d584b6459ddfe062edf0d0dd64800a02e67e5de048b/websockets-13.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e4450fc83a3df53dec45922b576e91e94f5578d06436871dce3a6be38e40f5db", size = 164222 }, { url = "https://files.pythonhosted.org/packages/f2/c4/7916e1f6b5252d3dcb9121b67d7fdbb2d9bf5067a6d8c88885ba27a9e69c/websockets-13.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:463e1c6ec853202dd3657f156123d6b4dad0c546ea2e2e38be2b3f7c5b8e7295", size = 163647 }, { url = "https://files.pythonhosted.org/packages/de/df/2ebebb807f10993c35c10cbd3628a7944b66bd5fb6632a561f8666f3a68e/websockets-13.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6d6855bbe70119872c05107e38fbc7f96b1d8cb047d95c2c50869a46c65a8e96", size = 163590 }, { url = "https://files.pythonhosted.org/packages/b5/82/d48911f56bb993c11099a1ff1d4041d9d1481d50271100e8ee62bc28f365/websockets-13.1-cp38-cp38-win32.whl", hash = "sha256:204e5107f43095012b00f1451374693267adbb832d29966a01ecc4ce1db26faf", size = 158701 }, { url = "https://files.pythonhosted.org/packages/8b/b3/945aacb21fc89ad150403cbaa974c9e846f098f16d9f39a3dd6094f9beb1/websockets-13.1-cp38-cp38-win_amd64.whl", hash = "sha256:485307243237328c022bc908b90e4457d0daa8b5cf4b3723fd3c4a8012fce4c6", size = 159146 }, { url = "https://files.pythonhosted.org/packages/61/26/5f7a7fb03efedb4f90ed61968338bfe7c389863b0ceda239b94ae61c5ae4/websockets-13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d", size = 157810 }, { url = "https://files.pythonhosted.org/packages/0e/d4/9b4814a07dffaa7a79d71b4944d10836f9adbd527a113f6675734ef3abed/websockets-13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7", size = 155467 }, { url = "https://files.pythonhosted.org/packages/1a/1a/2abdc7ce3b56429ae39d6bfb48d8c791f5a26bbcb6f44aabcf71ffc3fda2/websockets-13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a", size = 155714 }, { url = "https://files.pythonhosted.org/packages/2a/98/189d7cf232753a719b2726ec55e7922522632248d5d830adf078e3f612be/websockets-13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa", size = 164587 }, { url = "https://files.pythonhosted.org/packages/a5/2b/fb77cedf3f9f55ef8605238c801eef6b9a5269b01a396875a86896aea3a6/websockets-13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa", size = 163588 }, { url = "https://files.pythonhosted.org/packages/a3/b7/070481b83d2d5ac0f19233d9f364294e224e6478b0762f07fa7f060e0619/websockets-13.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79", size = 163894 }, { url = "https://files.pythonhosted.org/packages/eb/be/d6e1cff7d441cfe5eafaacc5935463e5f14c8b1c0d39cb8afde82709b55a/websockets-13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17", size = 164315 }, { url = "https://files.pythonhosted.org/packages/8b/5e/ffa234473e46ab2d3f9fd9858163d5db3ecea1439e4cb52966d78906424b/websockets-13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6", size = 163714 }, { url = "https://files.pythonhosted.org/packages/cc/92/cea9eb9d381ca57065a5eb4ec2ce7a291bd96c85ce742915c3c9ffc1069f/websockets-13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5", size = 163673 }, { url = "https://files.pythonhosted.org/packages/a4/f1/279104fff239bfd04c12b1e58afea227d72fd1acf431e3eed3f6ac2c96d2/websockets-13.1-cp39-cp39-win32.whl", hash = "sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c", size = 158702 }, { url = "https://files.pythonhosted.org/packages/25/0b/b87370ff141375c41f7dd67941728e4b3682ebb45882591516c792a2ebee/websockets-13.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d", size = 159146 }, { url = "https://files.pythonhosted.org/packages/2d/75/6da22cb3ad5b8c606963f9a5f9f88656256fecc29d420b4b2bf9e0c7d56f/websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238", size = 155499 }, { url = "https://files.pythonhosted.org/packages/c0/ba/22833d58629088fcb2ccccedfae725ac0bbcd713319629e97125b52ac681/websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5", size = 155737 }, { url = "https://files.pythonhosted.org/packages/95/54/61684fe22bdb831e9e1843d972adadf359cf04ab8613285282baea6a24bb/websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9", size = 157095 }, { url = "https://files.pythonhosted.org/packages/fc/f5/6652fb82440813822022a9301a30afde85e5ff3fb2aebb77f34aabe2b4e8/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6", size = 156701 }, { url = "https://files.pythonhosted.org/packages/67/33/ae82a7b860fa8a08aba68818bdf7ff61f04598aa5ab96df4cd5a3e418ca4/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a", size = 156654 }, { url = "https://files.pythonhosted.org/packages/63/0b/a1b528d36934f833e20f6da1032b995bf093d55cb416b9f2266f229fb237/websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23", size = 159192 }, { url = "https://files.pythonhosted.org/packages/5e/a1/5ae6d0ef2e61e2b77b3b4678949a634756544186620a728799acdf5c3482/websockets-13.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9156c45750b37337f7b0b00e6248991a047be4aa44554c9886fe6bdd605aab3b", size = 155433 }, { url = "https://files.pythonhosted.org/packages/0d/2f/addd33f85600d210a445f817ff0d79d2b4d0eb6f3c95b9f35531ebf8f57c/websockets-13.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:80c421e07973a89fbdd93e6f2003c17d20b69010458d3a8e37fb47874bd67d51", size = 155733 }, { url = "https://files.pythonhosted.org/packages/74/0b/f8ec74ac3b14a983289a1b42dc2c518a0e2030b486d0549d4f51ca11e7c9/websockets-13.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82d0ba76371769d6a4e56f7e83bb8e81846d17a6190971e38b5de108bde9b0d7", size = 157093 }, { url = "https://files.pythonhosted.org/packages/ad/4c/aa5cc2f718ee4d797411202f332c8281f04c42d15f55b02f7713320f7a03/websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9875a0143f07d74dc5e1ded1c4581f0d9f7ab86c78994e2ed9e95050073c94d", size = 156701 }, { url = "https://files.pythonhosted.org/packages/1f/4b/7c5b2d0d0f0f1a54f27c60107cf1f201bee1f88c5508f87408b470d09a9c/websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11e38ad8922c7961447f35c7b17bffa15de4d17c70abd07bfbe12d6faa3e027", size = 156648 }, { url = "https://files.pythonhosted.org/packages/f3/63/35f3fb073884a9fd1ce5413b2dcdf0d9198b03dac6274197111259cbde06/websockets-13.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4059f790b6ae8768471cddb65d3c4fe4792b0ab48e154c9f0a04cefaabcd5978", size = 159188 }, { url = "https://files.pythonhosted.org/packages/59/fd/e4bf9a7159dba6a16c59ae9e670e3e8ad9dcb6791bc0599eb86de32d50a9/websockets-13.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e", size = 155499 }, { url = "https://files.pythonhosted.org/packages/74/42/d48ede93cfe0c343f3b552af08efc60778d234989227b16882eed1b8b189/websockets-13.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09", size = 155731 }, { url = "https://files.pythonhosted.org/packages/f6/f2/2ef6bff1c90a43b80622a17c0852b48c09d3954ab169266ad7b15e17cdcb/websockets-13.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842", size = 157093 }, { url = "https://files.pythonhosted.org/packages/d1/14/6f20bbaeeb350f155edf599aad949c554216f90e5d4ae7373d1f2e5931fb/websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb", size = 156701 }, { url = "https://files.pythonhosted.org/packages/c7/86/38279dfefecd035e22b79c38722d4f87c4b6196f1556b7a631d0a3095ca7/websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20", size = 156649 }, { url = "https://files.pythonhosted.org/packages/f6/c5/12c6859a2eaa8c53f59a647617a27f1835a226cd7106c601067c53251d98/websockets-13.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678", size = 159187 }, { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134 }, ] [[package]] name = "wrapt" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/a1/fc03dca9b0432725c2e8cdbf91a349d2194cf03d8523c124faebe581de09/wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801", size = 55542 } wheels = [ { url = "https://files.pythonhosted.org/packages/99/f9/85220321e9bb1a5f72ccce6604395ae75fcb463d87dad0014dc1010bd1f1/wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8", size = 38766 }, { url = "https://files.pythonhosted.org/packages/ff/71/ff624ff3bde91ceb65db6952cdf8947bc0111d91bd2359343bc2fa7c57fd/wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d", size = 83262 }, { url = "https://files.pythonhosted.org/packages/9f/0a/814d4a121a643af99cfe55a43e9e6dd08f4a47cdac8e8f0912c018794715/wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df", size = 74990 }, { url = "https://files.pythonhosted.org/packages/cd/c7/b8c89bf5ca5c4e6a2d0565d149d549cdb4cffb8916d1d1b546b62fb79281/wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d", size = 82712 }, { url = "https://files.pythonhosted.org/packages/19/7c/5977aefa8460906c1ff914fd42b11cf6c09ded5388e46e1cc6cea4ab15e9/wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea", size = 81705 }, { url = "https://files.pythonhosted.org/packages/ae/e7/233402d7bd805096bb4a8ec471f5a141421a01de3c8c957cce569772c056/wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb", size = 74636 }, { url = "https://files.pythonhosted.org/packages/93/81/b6c32d8387d9cfbc0134f01585dee7583315c3b46dfd3ae64d47693cd078/wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301", size = 81299 }, { url = "https://files.pythonhosted.org/packages/d1/c3/1fae15d453468c98f09519076f8d401b476d18d8d94379e839eed14c4c8b/wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22", size = 36425 }, { url = "https://files.pythonhosted.org/packages/c6/f4/77e0886c95556f2b4caa8908ea8eb85f713fc68296a2113f8c63d50fe0fb/wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575", size = 38748 }, { url = "https://files.pythonhosted.org/packages/0e/40/def56538acddc2f764c157d565b9f989072a1d2f2a8e384324e2e104fc7d/wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a", size = 38766 }, { url = "https://files.pythonhosted.org/packages/89/e2/8c299f384ae4364193724e2adad99f9504599d02a73ec9199bf3f406549d/wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed", size = 83730 }, { url = "https://files.pythonhosted.org/packages/29/ef/fcdb776b12df5ea7180d065b28fa6bb27ac785dddcd7202a0b6962bbdb47/wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489", size = 75470 }, { url = "https://files.pythonhosted.org/packages/55/b5/698bd0bf9fbb3ddb3a2feefbb7ad0dea1205f5d7d05b9cbab54f5db731aa/wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9", size = 83168 }, { url = "https://files.pythonhosted.org/packages/ce/07/701a5cee28cb4d5df030d4b2649319e36f3d9fdd8000ef1d84eb06b9860d/wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339", size = 82307 }, { url = "https://files.pythonhosted.org/packages/42/92/c48ba92cda6f74cb914dc3c5bba9650dc80b790e121c4b987f3a46b028f5/wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d", size = 75101 }, { url = "https://files.pythonhosted.org/packages/8a/0a/9276d3269334138b88a2947efaaf6335f61d547698e50dff672ade24f2c6/wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b", size = 81835 }, { url = "https://files.pythonhosted.org/packages/b9/4c/39595e692753ef656ea94b51382cc9aea662fef59d7910128f5906486f0e/wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346", size = 36412 }, { url = "https://files.pythonhosted.org/packages/63/bb/c293a67fb765a2ada48f48cd0f2bb957da8161439da4c03ea123b9894c02/wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a", size = 38744 }, { url = "https://files.pythonhosted.org/packages/85/82/518605474beafff11f1a34759f6410ab429abff9f7881858a447e0d20712/wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569", size = 38904 }, { url = "https://files.pythonhosted.org/packages/80/6c/17c3b2fed28edfd96d8417c865ef0b4c955dc52c4e375d86f459f14340f1/wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504", size = 88622 }, { url = "https://files.pythonhosted.org/packages/4a/11/60ecdf3b0fd3dca18978d89acb5d095a05f23299216e925fcd2717c81d93/wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451", size = 80920 }, { url = "https://files.pythonhosted.org/packages/d2/50/dbef1a651578a3520d4534c1e434989e3620380c1ad97e309576b47f0ada/wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1", size = 89170 }, { url = "https://files.pythonhosted.org/packages/44/a2/78c5956bf39955288c9e0dd62e807b308c3aa15a0f611fbff52aa8d6b5ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106", size = 86748 }, { url = "https://files.pythonhosted.org/packages/99/49/2ee413c78fc0bdfebe5bee590bf3becdc1fab0096a7a9c3b5c9666b2415f/wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada", size = 79734 }, { url = "https://files.pythonhosted.org/packages/c0/8c/4221b7b270e36be90f0930fe15a4755a6ea24093f90b510166e9ed7861ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4", size = 87552 }, { url = "https://files.pythonhosted.org/packages/4c/6b/1aaccf3efe58eb95e10ce8e77c8909b7a6b0da93449a92c4e6d6d10b3a3d/wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635", size = 36647 }, { url = "https://files.pythonhosted.org/packages/b3/4f/243f88ac49df005b9129194c6511b3642818b3e6271ddea47a15e2ee4934/wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7", size = 38830 }, { url = "https://files.pythonhosted.org/packages/67/9c/38294e1bb92b055222d1b8b6591604ca4468b77b1250f59c15256437644f/wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181", size = 38904 }, { url = "https://files.pythonhosted.org/packages/78/b6/76597fb362cbf8913a481d41b14b049a8813cd402a5d2f84e57957c813ae/wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393", size = 88608 }, { url = "https://files.pythonhosted.org/packages/bc/69/b500884e45b3881926b5f69188dc542fb5880019d15c8a0df1ab1dfda1f7/wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4", size = 80879 }, { url = "https://files.pythonhosted.org/packages/52/31/f4cc58afe29eab8a50ac5969963010c8b60987e719c478a5024bce39bc42/wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b", size = 89119 }, { url = "https://files.pythonhosted.org/packages/aa/9c/05ab6bf75dbae7a9d34975fb6ee577e086c1c26cde3b6cf6051726d33c7c/wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721", size = 86778 }, { url = "https://files.pythonhosted.org/packages/0e/6c/4b8d42e3db355603d35fe5c9db79c28f2472a6fd1ccf4dc25ae46739672a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90", size = 79793 }, { url = "https://files.pythonhosted.org/packages/69/23/90e3a2ee210c0843b2c2a49b3b97ffcf9cad1387cb18cbeef9218631ed5a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a", size = 87606 }, { url = "https://files.pythonhosted.org/packages/5f/06/3683126491ca787d8d71d8d340e775d40767c5efedb35039d987203393b7/wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045", size = 36651 }, { url = "https://files.pythonhosted.org/packages/f1/bc/3bf6d2ca0d2c030d324ef9272bea0a8fdaff68f3d1fa7be7a61da88e51f7/wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838", size = 38835 }, { url = "https://files.pythonhosted.org/packages/ce/b5/251165c232d87197a81cd362eeb5104d661a2dd3aa1f0b33e4bf61dda8b8/wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b", size = 40146 }, { url = "https://files.pythonhosted.org/packages/89/33/1e1bdd3e866eeb73d8c4755db1ceb8a80d5bd51ee4648b3f2247adec4e67/wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379", size = 113444 }, { url = "https://files.pythonhosted.org/packages/9f/7c/94f53b065a43f5dc1fbdd8b80fd8f41284315b543805c956619c0b8d92f0/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d", size = 101246 }, { url = "https://files.pythonhosted.org/packages/62/5d/640360baac6ea6018ed5e34e6e80e33cfbae2aefde24f117587cd5efd4b7/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f", size = 109320 }, { url = "https://files.pythonhosted.org/packages/e3/cf/6c7a00ae86a2e9482c91170aefe93f4ccda06c1ac86c4de637c69133da59/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c", size = 110193 }, { url = "https://files.pythonhosted.org/packages/cd/cc/aa718df0d20287e8f953ce0e2f70c0af0fba1d3c367db7ee8bdc46ea7003/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b", size = 100460 }, { url = "https://files.pythonhosted.org/packages/f7/16/9f3ac99fe1f6caaa789d67b4e3c562898b532c250769f5255fa8b8b93983/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab", size = 106347 }, { url = "https://files.pythonhosted.org/packages/64/85/c77a331b2c06af49a687f8b926fc2d111047a51e6f0b0a4baa01ff3a673a/wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf", size = 37971 }, { url = "https://files.pythonhosted.org/packages/05/9b/b2469f8be9efed24283fd7b9eeb8e913e9bc0715cf919ea8645e428ab7af/wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a", size = 40755 }, { url = "https://files.pythonhosted.org/packages/71/da/1c12502da116b379e511c39d95cdc8f886ace2f3478217cde9494d38ca58/wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13", size = 38712 }, { url = "https://files.pythonhosted.org/packages/8a/b0/66f3e53c77257a505aaf7ef6d1b75ff7c8bb6a9da3d96f6aaa5810cd2f33/wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f", size = 86199 }, { url = "https://files.pythonhosted.org/packages/08/4e/313f99f271557cc85b6ba086fb9a0d785d0373f237f30d0b4a4d14c7daed/wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c", size = 78073 }, { url = "https://files.pythonhosted.org/packages/e1/62/5b50c324082081337c2b38daf4bae1de66e87eb126c754b0fa153b3525af/wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d", size = 85555 }, { url = "https://files.pythonhosted.org/packages/eb/d2/31bb2c9362d84153d7597a471b22250783bf86be1a01c1acaba3bf7a0e01/wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce", size = 83892 }, { url = "https://files.pythonhosted.org/packages/eb/54/f43889a2c787f2b8ac989461c0d2011f0ff69811ebf9b84796cc671aed63/wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627", size = 76869 }, { url = "https://files.pythonhosted.org/packages/aa/37/0fbed8e67bd10b6f8835047abb6f42b8870689af45d7ae581946f1685468/wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f", size = 83564 }, { url = "https://files.pythonhosted.org/packages/ef/3c/40db3a234871eda0a7eb48001d025474ed9fde85fd992eefda154ebc4632/wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea", size = 36407 }, { url = "https://files.pythonhosted.org/packages/67/71/b9ce92b7820e9bd8e2c727d806a2e4e8c9d2a3e839ffadde2d0e44d84c0b/wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed", size = 38706 }, { url = "https://files.pythonhosted.org/packages/89/03/518069f0708573c02cbba3a3e452be3642dc7d984d0a03a47e0850e2fb05/wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1", size = 38765 }, { url = "https://files.pythonhosted.org/packages/60/01/12dd81522f8c1c953e98e2cbf356ff44fbb06ef0f7523cd622ac06ad7f03/wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c", size = 83012 }, { url = "https://files.pythonhosted.org/packages/c4/2d/9853fe0009271b2841f839eb0e707c6b4307d169375f26c58812ecf4fd71/wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578", size = 74759 }, { url = "https://files.pythonhosted.org/packages/94/5c/03c911442b01b50e364572581430e12f82c3f5ea74d302907c1449d7ba36/wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33", size = 82540 }, { url = "https://files.pythonhosted.org/packages/52/e0/ef637448514295a6b3a01cf1dff417e081e7b8cf1eb712839962459af1f6/wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad", size = 81461 }, { url = "https://files.pythonhosted.org/packages/7f/44/8b7d417c3aae3a35ccfe361375ee3e452901c91062e5462e1aeef98255e8/wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9", size = 74380 }, { url = "https://files.pythonhosted.org/packages/af/a9/e65406a9c3a99162055efcb6bf5e0261924381228c0a7608066805da03df/wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0", size = 81057 }, { url = "https://files.pythonhosted.org/packages/55/0c/111d42fb658a2f9ed7024cd5e57c08521d61646a256a3946db7d500c1551/wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88", size = 36415 }, { url = "https://files.pythonhosted.org/packages/00/33/e7b14a7c06cedfaae064f34e95c95350de7cc10187ac173743e30a956b30/wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977", size = 38742 }, { url = "https://files.pythonhosted.org/packages/4b/d9/a8ba5e9507a9af1917285d118388c5eb7a81834873f45df213a6fe923774/wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371", size = 23592 }, ] [[package]] name = "wsproto" version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425 } wheels = [ { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226 }, ] [[package]] name = "zipp" version = "3.20.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199 } wheels = [ { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200 }, ] [[package]] name = "zope-interface" version = "7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960 } wheels = [ { url = "https://files.pythonhosted.org/packages/76/71/e6177f390e8daa7e75378505c5ab974e0bf59c1d3b19155638c7afbf4b2d/zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2", size = 208243 }, { url = "https://files.pythonhosted.org/packages/52/db/7e5f4226bef540f6d55acfd95cd105782bc6ee044d9b5587ce2c95558a5e/zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a", size = 208759 }, { url = "https://files.pythonhosted.org/packages/28/ea/fdd9813c1eafd333ad92464d57a4e3a82b37ae57c19497bcffa42df673e4/zope.interface-7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550f1c6588ecc368c9ce13c44a49b8d6b6f3ca7588873c679bd8fd88a1b557b6", size = 254922 }, { url = "https://files.pythonhosted.org/packages/3b/d3/0000a4d497ef9fbf4f66bb6828b8d0a235e690d57c333be877bec763722f/zope.interface-7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ef9e2f865721553c6f22a9ff97da0f0216c074bd02b25cf0d3af60ea4d6931d", size = 249367 }, { url = "https://files.pythonhosted.org/packages/3e/e5/0b359e99084f033d413419eff23ee9c2bd33bca2ca9f4e83d11856f22d10/zope.interface-7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27f926f0dcb058211a3bb3e0e501c69759613b17a553788b2caeb991bed3b61d", size = 254488 }, { url = "https://files.pythonhosted.org/packages/7b/90/12d50b95f40e3b2fc0ba7f7782104093b9fd62806b13b98ef4e580f2ca61/zope.interface-7.2-cp310-cp310-win_amd64.whl", hash = "sha256:144964649eba4c5e4410bb0ee290d338e78f179cdbfd15813de1a664e7649b3b", size = 211947 }, { url = "https://files.pythonhosted.org/packages/98/7d/2e8daf0abea7798d16a58f2f3a2bf7588872eee54ac119f99393fdd47b65/zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2", size = 208776 }, { url = "https://files.pythonhosted.org/packages/a0/2a/0c03c7170fe61d0d371e4c7ea5b62b8cb79b095b3d630ca16719bf8b7b18/zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22", size = 209296 }, { url = "https://files.pythonhosted.org/packages/49/b4/451f19448772b4a1159519033a5f72672221e623b0a1bd2b896b653943d8/zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7", size = 260997 }, { url = "https://files.pythonhosted.org/packages/65/94/5aa4461c10718062c8f8711161faf3249d6d3679c24a0b81dd6fc8ba1dd3/zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c", size = 255038 }, { url = "https://files.pythonhosted.org/packages/9f/aa/1a28c02815fe1ca282b54f6705b9ddba20328fabdc37b8cf73fc06b172f0/zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a", size = 259806 }, { url = "https://files.pythonhosted.org/packages/a7/2c/82028f121d27c7e68632347fe04f4a6e0466e77bb36e104c8b074f3d7d7b/zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1", size = 212305 }, { url = "https://files.pythonhosted.org/packages/68/0b/c7516bc3bad144c2496f355e35bd699443b82e9437aa02d9867653203b4a/zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7", size = 208959 }, { url = "https://files.pythonhosted.org/packages/a2/e9/1463036df1f78ff8c45a02642a7bf6931ae4a38a4acd6a8e07c128e387a7/zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465", size = 209357 }, { url = "https://files.pythonhosted.org/packages/07/a8/106ca4c2add440728e382f1b16c7d886563602487bdd90004788d45eb310/zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89", size = 264235 }, { url = "https://files.pythonhosted.org/packages/fc/ca/57286866285f4b8a4634c12ca1957c24bdac06eae28fd4a3a578e30cf906/zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54", size = 259253 }, { url = "https://files.pythonhosted.org/packages/96/08/2103587ebc989b455cf05e858e7fbdfeedfc3373358320e9c513428290b1/zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d", size = 264702 }, { url = "https://files.pythonhosted.org/packages/5f/c7/3c67562e03b3752ba4ab6b23355f15a58ac2d023a6ef763caaca430f91f2/zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5", size = 212466 }, { url = "https://files.pythonhosted.org/packages/c6/3b/e309d731712c1a1866d61b5356a069dd44e5b01e394b6cb49848fa2efbff/zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98", size = 208961 }, { url = "https://files.pythonhosted.org/packages/49/65/78e7cebca6be07c8fc4032bfbb123e500d60efdf7b86727bb8a071992108/zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d", size = 209356 }, { url = "https://files.pythonhosted.org/packages/11/b1/627384b745310d082d29e3695db5f5a9188186676912c14b61a78bbc6afe/zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c", size = 264196 }, { url = "https://files.pythonhosted.org/packages/b8/f6/54548df6dc73e30ac6c8a7ff1da73ac9007ba38f866397091d5a82237bd3/zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398", size = 259237 }, { url = "https://files.pythonhosted.org/packages/b6/66/ac05b741c2129fdf668b85631d2268421c5cd1a9ff99be1674371139d665/zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b", size = 264696 }, { url = "https://files.pythonhosted.org/packages/0a/2f/1bccc6f4cc882662162a1158cda1a7f616add2ffe322b28c99cb031b4ffc/zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd", size = 212472 }, { url = "https://files.pythonhosted.org/packages/e6/0f/4e296d4b36ceb5464b671443ac4084d586d47698610025c4731ff2d30eae/zope.interface-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d3a8ffec2a50d8ec470143ea3d15c0c52d73df882eef92de7537e8ce13475e8a", size = 208349 }, { url = "https://files.pythonhosted.org/packages/f5/8c/49548aaa4f691615d703b5bee88ea67f68eac8f94c9fb6f1b2f4ae631354/zope.interface-7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:31d06db13a30303c08d61d5fb32154be51dfcbdb8438d2374ae27b4e069aac40", size = 208806 }, { url = "https://files.pythonhosted.org/packages/fe/f9/5a66f98f8c21d644d94f95b9484564f76e175034de3a57a45ba72238ce10/zope.interface-7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e204937f67b28d2dca73ca936d3039a144a081fc47a07598d44854ea2a106239", size = 257897 }, { url = "https://files.pythonhosted.org/packages/9e/dd/5505c6fa2dd3a6b76176c07bc85ad0c24f218a3e7c929272384a5eb5f18a/zope.interface-7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:224b7b0314f919e751f2bca17d15aad00ddbb1eadf1cb0190fa8175edb7ede62", size = 252095 }, { url = "https://files.pythonhosted.org/packages/8a/cd/f1e8303b81151cd6ba6e229db5fe601f9867a7b29f24a95eeba255970099/zope.interface-7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf95683cde5bc7d0e12d8e7588a3eb754d7c4fa714548adcd96bdf90169f021", size = 257367 }, { url = "https://files.pythonhosted.org/packages/7c/a3/c4e9d23ca87d3f922e38ba4f4edcbf6114c10ff9ee0b7b4d5023b0f1f3f5/zope.interface-7.2-cp38-cp38-win_amd64.whl", hash = "sha256:7dc5016e0133c1a1ec212fc87a4f7e7e562054549a99c73c8896fa3a9e80cbc7", size = 211957 }, { url = "https://files.pythonhosted.org/packages/8c/2c/1f49dc8b4843c4f0848d8e43191aed312bad946a1563d1bf9e46cf2816ee/zope.interface-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bd449c306ba006c65799ea7912adbbfed071089461a19091a228998b82b1fdb", size = 208349 }, { url = "https://files.pythonhosted.org/packages/ed/7d/83ddbfc8424c69579a90fc8edc2b797223da2a8083a94d8dfa0e374c5ed4/zope.interface-7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a19a6cc9c6ce4b1e7e3d319a473cf0ee989cbbe2b39201d7c19e214d2dfb80c7", size = 208799 }, { url = "https://files.pythonhosted.org/packages/36/22/b1abd91854c1be03f5542fe092e6a745096d2eca7704d69432e119100583/zope.interface-7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cd1790b48c16db85d51fbbd12d20949d7339ad84fd971427cf00d990c1f137", size = 254267 }, { url = "https://files.pythonhosted.org/packages/2a/dd/fcd313ee216ad0739ae00e6126bc22a0af62a74f76a9ca668d16cd276222/zope.interface-7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52e446f9955195440e787596dccd1411f543743c359eeb26e9b2c02b077b0519", size = 248614 }, { url = "https://files.pythonhosted.org/packages/88/d4/4ba1569b856870527cec4bf22b91fe704b81a3c1a451b2ccf234e9e0666f/zope.interface-7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad9913fd858274db8dd867012ebe544ef18d218f6f7d1e3c3e6d98000f14b75", size = 253800 }, { url = "https://files.pythonhosted.org/packages/69/da/c9cfb384c18bd3a26d9fc6a9b5f32ccea49ae09444f097eaa5ca9814aff9/zope.interface-7.2-cp39-cp39-win_amd64.whl", hash = "sha256:1090c60116b3da3bfdd0c03406e2f14a1ff53e5771aebe33fec1edc0a350175d", size = 211980 }, ]